From 5e0baa1e49ffa01c2d8284739d1322fe6ffddede Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 15 Feb 2026 01:20:59 +0800 Subject: [PATCH 1/8] Add design doc: symbolic expression system for reduction overhead --- docs/plans/2026-02-15-symbolic-expr-design.md | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/plans/2026-02-15-symbolic-expr-design.md diff --git a/docs/plans/2026-02-15-symbolic-expr-design.md b/docs/plans/2026-02-15-symbolic-expr-design.md new file mode 100644 index 00000000..f24a65cf --- /dev/null +++ b/docs/plans/2026-02-15-symbolic-expr-design.md @@ -0,0 +1,194 @@ +# Symbolic Expression System for Reduction Overhead + +**Date:** 2026-02-15 +**Status:** Approved + +## Goal + +Replace the current `Polynomial`/`Monomial`/`poly!` system with a general-purpose symbolic expression DSL that supports exponentials, logarithms, min/max, floor/ceil, and arbitrary arithmetic — not just polynomials. + +## Motivation + +The current `Polynomial` type only supports sums of monomials (coefficient × product of variables with integer exponents). It cannot represent: +- Exponential overhead: `1.44 ^ num_vertices` +- Logarithmic factors: `num_vertices * log2(num_edges)` +- Min/max: `max(num_vertices, num_edges)` +- Floor/ceil: `ceil(num_vertices / 2)` + +These are needed to accurately model reduction overhead between problems. + +## Design + +### 1. Expression AST + +```rust +#[derive(Clone, Debug)] +pub enum Expr { + Num(f64), + Var(Box), + BinOp { op: BinOp, lhs: Box, rhs: Box }, + Neg(Box), + Call { func: Func, args: Vec }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BinOp { Add, Sub, Mul, Div, Pow } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Func { Log2, Log10, Ln, Exp, Sqrt, Min, Max, Floor, Ceil, Abs } +``` + +**Key decisions:** +- `Box` for variable names (avoids `String` allocation overhead on clone) +- No `PartialEq` on `Expr` (f64 makes it dangerous; NaN != NaN) +- `Func` enum for built-ins (compile-time exhaustiveness, no runtime string matching) +- Separate `Neg` variant (cleaner than encoding as `0 - x`) +- No user-defined functions (not needed for this use case) + +### 2. Evaluation + +```rust +pub enum EvalError { + UnknownVar(Box), + DivideByZero, + Arity { func: Func, expected: usize, got: usize }, + Domain { func: Func, detail: Box }, +} +``` + +- Recursive tree walk with `ProblemSize` providing variable bindings +- Returns `Result` +- **No NaN guarantee:** domain violations return `EvalError::Domain` instead of producing NaN + - `log(-1)` → Domain error + - Negative base + non-integer exponent → Domain error + - `0 / 0` → DivideByZero +- `^` is right-associative: `a ^ b ^ c` = `a ^ (b ^ c)` + +### 3. Parser + +Pratt parser (precedence climbing), ~200-300 lines. + +**Tokens:** `Num(f64)`, `Ident(Box)`, `+`, `-`, `*`, `/`, `^`, `(`, `)`, `,`, `Eof` + +**Precedence (lowest to highest):** + +| Level | Operators | Associativity | +|-------|-----------|---------------| +| 1 | `+`, `-` | Left | +| 2 | `*`, `/` | Left | +| 3 | unary `-` | Prefix | +| 4 | `^` | Right | + +Function calls parsed as part of primary expressions. + +**Grammar (for documentation; implementation uses Pratt):** +``` +expr = term (('+' | '-') term)* +term = unary (('*' | '/') unary)* +unary = '-' unary | power +power = primary ('^' power)? +primary = NUM | IDENT '(' args ')' | IDENT | '(' expr ')' +args = expr (',' expr)* +``` + +**Ident resolution:** If followed by `(`, matched against `Func` variants (ASCII case-insensitive). Unknown function → `ParseError::UnknownFunction`. + +**No implicit multiplication:** `2 num_vertices` is a parse error; must write `2 * num_vertices`. + +**Tokenizer rules:** +- Numbers: `42`, `1.5`, `.5` — no scientific notation +- Idents: `[a-zA-Z_][a-zA-Z0-9_]*` +- Whitespace: skipped silently + +**Parse errors with spans:** +```rust +pub struct Span { pub start: usize, pub end: usize } + +pub enum ParseError { + UnexpectedToken { expected: &'static str, got: Box, span: Span }, + UnexpectedEof { expected: &'static str }, + UnknownFunction { name: Box, span: Span }, + InvalidNumber { lexeme: Box, span: Span }, +} +``` + +### 4. Display and Serialization + +**Display** uses minimal parenthesization: +- Parenthesize child when its precedence < parent precedence +- For right-associative `^`: parenthesize left child if same precedence +- Integer-valued floats: `3.0` → `"3"`, `1.5` → `"1.5"` +- Round-trip invariant: `parse(expr.to_string())` ≡ `expr` semantically + +**Serde:** `Expr` serializes as its display string, deserializes by parsing. This means JSON contains human-readable expressions like `"1.44 ^ num_vertices"`. + +### 5. Integration with ReductionOverhead + +**`ReductionOverhead` stores `Expr`:** +```rust +pub struct ReductionOverhead { + pub output_size: Vec<(&'static str, Expr)>, +} +``` + +**Constructor takes string pairs:** +```rust +impl ReductionOverhead { + pub fn new(specs: Vec<(&'static str, &'static str)>) -> Self { + // Parses each expression string immediately. + // Panics on parse error (developer bug — these are static literals). + } +} +``` + +**`evaluate_output_size` returns `Result`:** +```rust +pub fn evaluate_output_size(&self, input: &ProblemSize) -> Result +``` + +Float → usize conversion: `round()` (matching current behavior), error on non-finite/negative/overflow. + +**Reduction macro usage changes from:** +```rust +#[reduction(overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]) +})] +``` + +**To:** +```rust +#[reduction(overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", "num_vertices"), + ("num_edges", "num_edges"), + ]) +})] +``` + +The `#[reduction]` proc macro itself needs no changes (it passes through the token stream). + +**JSON export format:** +```json +[ + {"field": "num_vertices", "expression": "num_vertices ^ 2"}, + {"field": "num_edges", "expression": "1.44 ^ num_vertices"} +] +``` + +### 6. File Organization + +| Action | File | Description | +|---------|------|-------------| +| **New** | `src/expr.rs` | `Expr`, `BinOp`, `Func`, parser, evaluator, Display, Serde (~400-500 lines) | +| **New** | `src/unit_tests/expr.rs` | Parser, eval, display round-trip, error case tests | +| **Delete** | `src/polynomial.rs` | Replaced by `src/expr.rs` | +| **Delete** | `src/unit_tests/polynomial.rs` | Replaced by `src/unit_tests/expr.rs` | +| **Modify** | `src/lib.rs` | `mod polynomial` → `mod expr`, update re-exports | +| **Modify** | `src/rules/registry.rs` | `Polynomial` → `Expr`, `new()` takes `(&str, &str)` pairs | +| **Modify** | `src/export.rs` | Remove `MonomialJson`, use expression strings | +| **Modify** | `src/rules/cost.rs` | Propagate `Result` from `evaluate_output_size` | +| **Modify** | `src/rules/graph.rs` | Propagate `Result` from `evaluate_output_size` | +| **Modify** | `src/rules/*.rs` (all reductions) | `poly!(x)` → `"x"` string literals | From 4801944663fbc42a4332a1e8db4588d9e96cf075 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 15 Feb 2026 01:25:36 +0800 Subject: [PATCH 2/8] Add implementation plan: symbolic expression system --- docs/plans/2026-02-15-symbolic-expr-plan.md | 1164 +++++++++++++++++++ 1 file changed, 1164 insertions(+) create mode 100644 docs/plans/2026-02-15-symbolic-expr-plan.md diff --git a/docs/plans/2026-02-15-symbolic-expr-plan.md b/docs/plans/2026-02-15-symbolic-expr-plan.md new file mode 100644 index 00000000..384c5fc4 --- /dev/null +++ b/docs/plans/2026-02-15-symbolic-expr-plan.md @@ -0,0 +1,1164 @@ +# Symbolic Expression System — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace `Polynomial`/`Monomial`/`poly!` with a general-purpose symbolic expression DSL supporting exponentials, logs, min/max, floor/ceil. + +**Architecture:** New `src/expr.rs` module with AST, Pratt parser, evaluator, and Display. `ReductionOverhead` switches from `Polynomial` to `Expr` parsed from string literals. All 30 reduction files migrate from `poly!()` to string specs. + +**Tech Stack:** Pure Rust, no new dependencies. Pratt parser hand-written. + +**Design doc:** `docs/plans/2026-02-15-symbolic-expr-design.md` + +--- + +### Task 1: Core AST and Evaluator + +**Files:** +- Create: `src/expr.rs` +- Create: `src/unit_tests/expr.rs` + +**Step 1: Write failing tests for AST construction and evaluation** + +Create `src/unit_tests/expr.rs`: + +```rust +use super::*; +use crate::types::ProblemSize; + +#[test] +fn test_eval_num() { + let expr = Expr::Num(42.0); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 42.0); +} + +#[test] +fn test_eval_var() { + let expr = Expr::Var("n".into()); + let size = ProblemSize::new(vec![("n", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 10.0); +} + +#[test] +fn test_eval_unknown_var() { + let expr = Expr::Var("missing".into()); + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::UnknownVar(_)))); +} + +#[test] +fn test_eval_add() { + let expr = Expr::binop(BinOp::Add, Expr::Num(3.0), Expr::Num(4.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_sub() { + let expr = Expr::binop(BinOp::Sub, Expr::Num(10.0), Expr::Num(3.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_mul() { + let expr = Expr::binop(BinOp::Mul, Expr::Num(3.0), Expr::Var("n".into())); + let size = ProblemSize::new(vec![("n", 5)]); + assert_eq!(expr.evaluate(&size).unwrap(), 15.0); +} + +#[test] +fn test_eval_div() { + let expr = Expr::binop(BinOp::Div, Expr::Num(10.0), Expr::Num(4.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 2.5); +} + +#[test] +fn test_eval_div_by_zero() { + let expr = Expr::binop(BinOp::Div, Expr::Num(1.0), Expr::Num(0.0)); + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::DivideByZero))); +} + +#[test] +fn test_eval_pow() { + let expr = Expr::binop(BinOp::Pow, Expr::Num(2.0), Expr::Num(10.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 1024.0); +} + +#[test] +fn test_eval_pow_fractional_base_negative() { + // negative base with non-integer exponent -> domain error + let expr = Expr::binop(BinOp::Pow, Expr::Num(-2.0), Expr::Num(0.5)); + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::Domain { .. }))); +} + +#[test] +fn test_eval_neg() { + let expr = Expr::Neg(Box::new(Expr::Num(5.0))); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), -5.0); +} + +#[test] +fn test_eval_log2() { + let expr = Expr::Call { func: Func::Log2, args: vec![Expr::Num(8.0)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_log2_negative() { + let expr = Expr::Call { func: Func::Log2, args: vec![Expr::Num(-1.0)] }; + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::Domain { .. }))); +} + +#[test] +fn test_eval_sqrt() { + let expr = Expr::Call { func: Func::Sqrt, args: vec![Expr::Num(25.0)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 5.0); +} + +#[test] +fn test_eval_min() { + let expr = Expr::Call { func: Func::Min, args: vec![Expr::Num(3.0), Expr::Num(7.0)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_max() { + let expr = Expr::Call { func: Func::Max, args: vec![Expr::Num(3.0), Expr::Num(7.0)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_floor() { + let expr = Expr::Call { func: Func::Floor, args: vec![Expr::Num(3.7)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_ceil() { + let expr = Expr::Call { func: Func::Ceil, args: vec![Expr::Num(3.2)] }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 4.0); +} + +#[test] +fn test_eval_arity_error() { + let expr = Expr::Call { func: Func::Log2, args: vec![Expr::Num(1.0), Expr::Num(2.0)] }; + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::Arity { .. }))); +} + +#[test] +fn test_eval_complex() { + // 3 * n ^ 2 + 1.44 ^ m + let expr = Expr::binop( + BinOp::Add, + Expr::binop(BinOp::Mul, Expr::Num(3.0), Expr::binop(BinOp::Pow, Expr::Var("n".into()), Expr::Num(2.0))), + Expr::binop(BinOp::Pow, Expr::Num(1.44), Expr::Var("m".into())), + ); + let size = ProblemSize::new(vec![("n", 4), ("m", 3)]); + let result = expr.evaluate(&size).unwrap(); + let expected = 3.0 * 16.0 + 1.44_f64.powi(3); + assert!((result - expected).abs() < 1e-10); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test expr --lib` +Expected: compilation error (module doesn't exist) + +**Step 3: Implement AST types and evaluator** + +Create `src/expr.rs` with: + +```rust +//! Symbolic expression system for reduction overhead. +//! +//! Provides a DSL for expressing how problem sizes transform during reductions. +//! Supports arithmetic, exponentiation, and built-in math functions. + +use crate::types::ProblemSize; +use std::fmt; + +/// A symbolic expression over named variables. +#[derive(Clone, Debug)] +pub enum Expr { + /// Numeric literal. + Num(f64), + /// Named variable (e.g., `num_vertices`). + Var(Box), + /// Binary operation. + BinOp { op: BinOp, lhs: Box, rhs: Box }, + /// Unary negation. + Neg(Box), + /// Built-in function call. + Call { func: Func, args: Vec }, +} + +/// Binary operators. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BinOp { Add, Sub, Mul, Div, Pow } + +/// Built-in functions. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Func { Log2, Log10, Ln, Exp, Sqrt, Min, Max, Floor, Ceil, Abs } + +/// Evaluation error. +#[derive(Debug)] +pub enum EvalError { + UnknownVar(Box), + DivideByZero, + Arity { func: Func, expected: usize, got: usize }, + Domain { func: Func, detail: Box }, +} + +impl Expr { + /// Convenience constructor for binary operations. + pub fn binop(op: BinOp, lhs: Expr, rhs: Expr) -> Self { + Expr::BinOp { op, lhs: Box::new(lhs), rhs: Box::new(rhs) } + } + + /// Evaluate the expression given variable bindings from `ProblemSize`. + pub fn evaluate(&self, size: &ProblemSize) -> Result { + match self { + Expr::Num(v) => Ok(*v), + Expr::Var(name) => size + .get(name) + .map(|v| v as f64) + .ok_or_else(|| EvalError::UnknownVar(name.clone())), + Expr::Neg(inner) => Ok(-inner.evaluate(size)?), + Expr::BinOp { op, lhs, rhs } => { + let l = lhs.evaluate(size)?; + let r = rhs.evaluate(size)?; + match op { + BinOp::Add => Ok(l + r), + BinOp::Sub => Ok(l - r), + BinOp::Mul => Ok(l * r), + BinOp::Div => { + if r == 0.0 { return Err(EvalError::DivideByZero); } + Ok(l / r) + } + BinOp::Pow => { + if l < 0.0 && r.fract() != 0.0 { + return Err(EvalError::Domain { + func: Func::Sqrt, // closest built-in + detail: "negative base with non-integer exponent".into(), + }); + } + let result = l.powf(r); + if result.is_nan() || result.is_infinite() { + return Err(EvalError::Domain { + func: Func::Exp, + detail: format!("{l} ^ {r} produced non-finite result").into(), + }); + } + Ok(result) + } + } + } + Expr::Call { func, args } => eval_func(*func, args, size), + } + } +} + +fn eval_func(func: Func, args: &[Expr], size: &ProblemSize) -> Result { + // Check arity + let (min_args, max_args) = match func { + Func::Min | Func::Max => (2, 2), + _ => (1, 1), + }; + if args.len() < min_args || args.len() > max_args { + return Err(EvalError::Arity { func, expected: min_args, got: args.len() }); + } + + let a = args[0].evaluate(size)?; + match func { + Func::Log2 => { + if a <= 0.0 { return Err(EvalError::Domain { func, detail: "log2 of non-positive".into() }); } + Ok(a.log2()) + } + Func::Log10 => { + if a <= 0.0 { return Err(EvalError::Domain { func, detail: "log10 of non-positive".into() }); } + Ok(a.log10()) + } + Func::Ln => { + if a <= 0.0 { return Err(EvalError::Domain { func, detail: "ln of non-positive".into() }); } + Ok(a.ln()) + } + Func::Exp => { + let result = a.exp(); + if result.is_infinite() { + return Err(EvalError::Domain { func, detail: "exp overflow".into() }); + } + Ok(result) + } + Func::Sqrt => { + if a < 0.0 { return Err(EvalError::Domain { func, detail: "sqrt of negative".into() }); } + Ok(a.sqrt()) + } + Func::Abs => Ok(a.abs()), + Func::Floor => Ok(a.floor()), + Func::Ceil => Ok(a.ceil()), + Func::Min => { let b = args[1].evaluate(size)?; Ok(a.min(b)) } + Func::Max => { let b = args[1].evaluate(size)?; Ok(a.max(b)) } + } +} +``` + +**Step 4: Wire up the module and test file** + +In `src/expr.rs`, add at the bottom: +```rust +#[cfg(test)] +#[path = "unit_tests/expr.rs"] +mod tests; +``` + +In `src/lib.rs`, add: `pub mod expr;` (keep `polynomial` for now — we'll remove it later). + +**Step 5: Run tests to verify they pass** + +Run: `cargo test expr --lib` +Expected: all tests pass + +**Step 6: Commit** + +```bash +git add src/expr.rs src/unit_tests/expr.rs src/lib.rs +git commit -m "feat: add symbolic expression AST and evaluator" +``` + +--- + +### Task 2: Parser (Tokenizer + Pratt) + +**Files:** +- Modify: `src/expr.rs` +- Modify: `src/unit_tests/expr.rs` + +**Step 1: Write failing parser tests** + +Add to `src/unit_tests/expr.rs`: + +```rust +#[test] +fn test_parse_num() { + let expr = Expr::parse("42").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 42.0); +} + +#[test] +fn test_parse_float() { + let expr = Expr::parse("1.44").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 1.44); +} + +#[test] +fn test_parse_var() { + let expr = Expr::parse("num_vertices").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 10.0); +} + +#[test] +fn test_parse_add() { + let expr = Expr::parse("3 + 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 7.0); +} + +#[test] +fn test_parse_precedence_mul_add() { + // 2 + 3 * 4 = 14 (not 20) + let expr = Expr::parse("2 + 3 * 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 14.0); +} + +#[test] +fn test_parse_precedence_pow() { + // 2 ^ 3 ^ 2 = 2 ^ 9 = 512 (right-associative) + let expr = Expr::parse("2 ^ 3 ^ 2").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 512.0); +} + +#[test] +fn test_parse_unary_neg() { + let expr = Expr::parse("-5").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), -5.0); +} + +#[test] +fn test_parse_neg_pow() { + // -2 ^ 2 = -(2^2) = -4 + let expr = Expr::parse("-2 ^ 2").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), -4.0); +} + +#[test] +fn test_parse_parens() { + let expr = Expr::parse("(2 + 3) * 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 20.0); +} + +#[test] +fn test_parse_function_log2() { + let expr = Expr::parse("log2(8)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 3.0); +} + +#[test] +fn test_parse_function_max() { + let expr = Expr::parse("max(3, 7)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 7.0); +} + +#[test] +fn test_parse_function_case_insensitive() { + let expr = Expr::parse("Log2(8)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 3.0); +} + +#[test] +fn test_parse_complex_expression() { + // 3 * num_vertices ^ 2 + 1.44 ^ num_edges + let expr = Expr::parse("3 * num_vertices ^ 2 + 1.44 ^ num_edges").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 4), ("num_edges", 3)]); + let expected = 3.0 * 16.0 + 1.44_f64.powi(3); + assert!((expr.evaluate(&size).unwrap() - expected).abs() < 1e-10); +} + +#[test] +fn test_parse_nested_functions() { + let expr = Expr::parse("floor(log2(16))").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 4.0); +} + +#[test] +fn test_parse_unknown_function() { + let result = Expr::parse("foo(3)"); + assert!(matches!(result, Err(ParseError::UnknownFunction { .. }))); +} + +#[test] +fn test_parse_unexpected_eof() { + let result = Expr::parse("3 +"); + assert!(result.is_err()); +} + +#[test] +fn test_parse_empty() { + let result = Expr::parse(""); + assert!(result.is_err()); +} + +#[test] +fn test_parse_leading_dot() { + let expr = Expr::parse(".5").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 0.5); +} + +#[test] +fn test_parse_subtraction() { + let expr = Expr::parse("10 - 3 - 2").unwrap(); + // left-associative: (10 - 3) - 2 = 5 + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 5.0); +} + +#[test] +fn test_parse_division() { + let expr = Expr::parse("num_vertices / 2").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 5.0); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test expr --lib` +Expected: `Expr::parse` doesn't exist + +**Step 3: Implement tokenizer and parser** + +Add to `src/expr.rs`: + +- `Span` struct +- `ParseError` enum +- `Token` enum (private) +- `Lexer` struct (private) — iterates chars, produces tokens with spans +- `Parser` struct (private) — Pratt parser consuming tokens +- `Expr::parse(input: &str) -> Result` public entry point + +The Pratt parser uses binding powers: +- `+`, `-`: left bp = 1, right bp = 2 +- `*`, `/`: left bp = 3, right bp = 4 +- prefix `-`: right bp = 5 +- `^`: left bp = 7, right bp = 6 (right-assoc: left > right) + +Function names resolved via a match on the lowercased ident. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test expr --lib` +Expected: all pass + +**Step 5: Commit** + +```bash +git add src/expr.rs src/unit_tests/expr.rs +git commit -m "feat: add expression parser with Pratt precedence climbing" +``` + +--- + +### Task 3: Display and Serde + +**Files:** +- Modify: `src/expr.rs` +- Modify: `src/unit_tests/expr.rs` + +**Step 1: Write failing tests for Display and round-tripping** + +Add to `src/unit_tests/expr.rs`: + +```rust +#[test] +fn test_display_num_integer() { + let expr = Expr::Num(3.0); + assert_eq!(expr.to_string(), "3"); +} + +#[test] +fn test_display_num_float() { + let expr = Expr::Num(1.44); + assert_eq!(expr.to_string(), "1.44"); +} + +#[test] +fn test_display_var() { + let expr = Expr::Var("num_vertices".into()); + assert_eq!(expr.to_string(), "num_vertices"); +} + +#[test] +fn test_display_add() { + let expr = Expr::parse("a + b").unwrap(); + assert_eq!(expr.to_string(), "a + b"); +} + +#[test] +fn test_display_precedence() { + let expr = Expr::parse("a + b * c").unwrap(); + assert_eq!(expr.to_string(), "a + b * c"); +} + +#[test] +fn test_display_parens_needed() { + let expr = Expr::parse("(a + b) * c").unwrap(); + assert_eq!(expr.to_string(), "(a + b) * c"); +} + +#[test] +fn test_display_pow() { + let expr = Expr::parse("1.44 ^ n").unwrap(); + assert_eq!(expr.to_string(), "1.44 ^ n"); +} + +#[test] +fn test_display_neg() { + let expr = Expr::parse("-x").unwrap(); + assert_eq!(expr.to_string(), "-x"); +} + +#[test] +fn test_display_neg_compound() { + let expr = Expr::parse("-(a + b)").unwrap(); + assert_eq!(expr.to_string(), "-(a + b)"); +} + +#[test] +fn test_display_func() { + let expr = Expr::parse("log2(n)").unwrap(); + assert_eq!(expr.to_string(), "log2(n)"); +} + +#[test] +fn test_display_func_two_args() { + let expr = Expr::parse("max(a, b)").unwrap(); + assert_eq!(expr.to_string(), "max(a, b)"); +} + +#[test] +fn test_roundtrip_complex() { + let cases = vec![ + "3 * n ^ 2 + 1.44 ^ m", + "log2(n) * m", + "max(n, m) + 1", + "floor(n / 2)", + "-(a + b) * c", + "a ^ b ^ c", + "a - b - c", + ]; + let size = ProblemSize::new(vec![("n", 4), ("m", 3), ("a", 2), ("b", 3), ("c", 5)]); + for case in cases { + let expr1 = Expr::parse(case).unwrap(); + let displayed = expr1.to_string(); + let expr2 = Expr::parse(&displayed).unwrap(); + let v1 = expr1.evaluate(&size).unwrap(); + let v2 = expr2.evaluate(&size).unwrap(); + assert!((v1 - v2).abs() < 1e-10, "Round-trip failed for {case}: displayed as {displayed}"); + } +} + +#[test] +fn test_serde_roundtrip() { + let expr = Expr::parse("3 * n ^ 2 + 1").unwrap(); + let json = serde_json::to_string(&expr).unwrap(); + let back: Expr = serde_json::from_str(&json).unwrap(); + let size = ProblemSize::new(vec![("n", 5)]); + assert_eq!(expr.evaluate(&size).unwrap(), back.evaluate(&size).unwrap()); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test expr --lib` +Expected: `Display` not implemented, serde not implemented + +**Step 3: Implement Display** + +Add `impl fmt::Display for Expr` to `src/expr.rs`: +- Use a helper that takes parent precedence context +- Parenthesize when child precedence < parent precedence +- For `^`, parenthesize LHS if same precedence (right-assoc) +- Integer floats display without decimal point +- Function names: lowercase canonical form + +Also add `impl fmt::Display for EvalError` and `impl fmt::Display for ParseError`. + +**Step 4: Implement Serde** + +Add custom `Serialize`/`Deserialize` for `Expr`: +- `Serialize`: delegates to `Display` (serializes as string) +- `Deserialize`: calls `Expr::parse` (deserializes from string) + +**Step 5: Run tests to verify they pass** + +Run: `cargo test expr --lib` +Expected: all pass + +**Step 6: Commit** + +```bash +git add src/expr.rs src/unit_tests/expr.rs +git commit -m "feat: add Display and Serde for Expr (string round-trip)" +``` + +--- + +### Task 4: Migrate ReductionOverhead and Registry + +**Files:** +- Modify: `src/rules/registry.rs` +- Modify: `src/unit_tests/rules/registry.rs` + +**Step 1: Write updated tests** + +Update `src/unit_tests/rules/registry.rs` — change from `poly!` to string specs: + +```rust +use crate::rules::registry::ReductionOverhead; +use crate::types::ProblemSize; + +#[test] +fn test_reduction_overhead_evaluate() { + let overhead = ReductionOverhead::new(vec![("n", "3 * m"), ("m", "m ^ 2")]); + let input = ProblemSize::new(vec![("m", 4)]); + let output = overhead.evaluate_output_size(&input).unwrap(); + assert_eq!(output.get("n"), Some(12)); + assert_eq!(output.get("m"), Some(16)); +} +``` + +Update the `ReductionEntry` test similarly — use `ReductionOverhead::new(vec![("n", "2 * n")])`. + +**Step 2: Run tests to verify they fail** + +Run: `cargo test registry --lib` +Expected: type mismatch (still expects `Polynomial`) + +**Step 3: Update `src/rules/registry.rs`** + +Replace: +```rust +use crate::polynomial::Polynomial; +``` +with: +```rust +use crate::expr::Expr; +``` + +Change `ReductionOverhead`: +```rust +pub struct ReductionOverhead { + pub output_size: Vec<(&'static str, Expr)>, +} + +impl ReductionOverhead { + pub fn new(specs: Vec<(&'static str, &'static str)>) -> Self { + Self { + output_size: specs + .into_iter() + .map(|(field, expr_str)| { + let expr = Expr::parse(expr_str).unwrap_or_else(|e| { + panic!("invalid overhead expression for '{field}': {e}") + }); + (field, expr) + }) + .collect(), + } + } + + pub fn evaluate_output_size(&self, input: &ProblemSize) -> Result { + let mut fields = Vec::new(); + for (name, expr) in &self.output_size { + let val = expr.evaluate(input)?; + let rounded = val.round(); + if !rounded.is_finite() || rounded < 0.0 || rounded > usize::MAX as f64 { + return Err(crate::expr::EvalError::Domain { + func: crate::expr::Func::Floor, + detail: format!("overhead for '{name}' produced out-of-range value: {val}").into(), + }); + } + fields.push((*name, rounded as usize)); + } + Ok(ProblemSize::new(fields)) + } +} +``` + +Keep `Default` impl producing empty `output_size`. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test registry --lib` +Expected: pass + +**Step 5: Commit** + +```bash +git add src/rules/registry.rs src/unit_tests/rules/registry.rs +git commit -m "refactor: ReductionOverhead uses Expr parsed from strings" +``` + +--- + +### Task 5: Migrate Export System + +**Files:** +- Modify: `src/export.rs` +- Modify: `src/unit_tests/export.rs` + +**Step 1: Write updated tests** + +Update `src/unit_tests/export.rs` — `overhead_to_json` now returns `Vec` where each entry has `field` and `expression` (a string): + +```rust +use crate::export::overhead_to_json; +use crate::rules::registry::ReductionOverhead; + +#[test] +fn test_overhead_to_json_empty() { + let overhead = ReductionOverhead::default(); + let entries = overhead_to_json(&overhead); + assert!(entries.is_empty()); +} + +#[test] +fn test_overhead_to_json_single_field() { + let overhead = ReductionOverhead::new(vec![("num_vertices", "n + m")]); + let entries = overhead_to_json(&overhead); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].field, "num_vertices"); + assert_eq!(entries[0].expression, "n + m"); +} + +#[test] +fn test_overhead_to_json_multiple_fields() { + let overhead = ReductionOverhead::new(vec![ + ("num_vertices", "n ^ 2"), + ("num_edges", "1.44 ^ n"), + ]); + let entries = overhead_to_json(&overhead); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].expression, "n ^ 2"); + assert_eq!(entries[1].expression, "1.44 ^ n"); +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test export --lib` +Expected: `MonomialJson` still expected + +**Step 3: Update `src/export.rs`** + +Remove `MonomialJson`. Simplify `OverheadEntry`: +```rust +#[derive(Serialize, Clone, Debug)] +pub struct OverheadEntry { + pub field: String, + pub expression: String, +} +``` + +Simplify `overhead_to_json`: +```rust +pub fn overhead_to_json(overhead: &ReductionOverhead) -> Vec { + overhead + .output_size + .iter() + .map(|(field, expr)| OverheadEntry { + field: field.to_string(), + expression: expr.to_string(), + }) + .collect() +} +``` + +Also update `ReductionData.overhead` field type from `Vec` — this should already work since `OverheadEntry` is still `Serialize`. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test export --lib` +Expected: pass + +**Step 5: Commit** + +```bash +git add src/export.rs src/unit_tests/export.rs +git commit -m "refactor: simplify export overhead to expression strings" +``` + +--- + +### Task 6: Migrate Cost Functions + +**Files:** +- Modify: `src/rules/cost.rs` +- Modify: `src/unit_tests/rules/cost.rs` + +**Step 1: Update tests** + +In `src/unit_tests/rules/cost.rs`, change the helper: +```rust +fn test_overhead() -> ReductionOverhead { + ReductionOverhead::new(vec![ + ("n", "2 * n"), + ("m", "m"), + ]) +} +``` + +Remove the `use crate::polynomial::Polynomial;` import. + +Existing test assertions should still hold since the values are the same. + +**Step 2: Run tests to verify they fail** + +Run: `cargo test cost --lib` +Expected: fail on `Polynomial` import + +**Step 3: Update `src/rules/cost.rs`** + +`evaluate_output_size` now returns `Result`. For cost functions, we should unwrap since overhead expressions are known-good at this point. Change each call from: +```rust +overhead.evaluate_output_size(size).get(self.0).unwrap_or(0) as f64 +``` +to: +```rust +overhead.evaluate_output_size(size) + .expect("overhead evaluation failed") + .get(self.0).unwrap_or(0) as f64 +``` + +Apply same pattern to `MinimizeWeighted`, `MinimizeMax`, `MinimizeLexicographic`. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test cost --lib` +Expected: pass + +**Step 5: Commit** + +```bash +git add src/rules/cost.rs src/unit_tests/rules/cost.rs +git commit -m "refactor: migrate cost functions to Expr-based overhead" +``` + +--- + +### Task 7: Migrate Graph Module + +**Files:** +- Modify: `src/rules/graph.rs` + +**Step 1: Update evaluate_output_size call** + +At line ~420, change: +```rust +let new_size = edge.overhead.evaluate_output_size(¤t_size); +``` +to: +```rust +let new_size = edge.overhead.evaluate_output_size(¤t_size) + .expect("overhead evaluation failed during path finding"); +``` + +At line ~712, the `to_json` method uses `poly.to_string()` — since `Expr` also implements `Display`, this already works. Just verify the field name change: the `OverheadFieldJson.formula` field now comes from `Expr::to_string()` which should produce equivalent output. + +**Step 2: Run: `cargo test graph --lib`** + +Expected: pass (no behavioral change) + +**Step 3: Commit** + +```bash +git add src/rules/graph.rs +git commit -m "refactor: migrate graph module to Expr-based overhead" +``` + +--- + +### Task 8: Migrate All 30 Reduction Files (Simple Ones) + +**Files:** +- Modify: 28 reduction files that use `poly!()` macro + +These all follow the same mechanical transformation. The `poly!` expressions map to string literals: + +| Old `poly!` syntax | New string literal | +|---|---| +| `poly!(num_vertices)` | `"num_vertices"` | +| `poly!(num_vertices ^ 2)` | `"num_vertices ^ 2"` | +| `poly!(3 * num_vars)` | `"3 * num_vars"` | +| `poly!(num_vertices * num_edges)` | `"num_vertices * num_edges"` | +| `poly!(3 * num_vars) + poly!(num_clauses)` | `"3 * num_vars + num_clauses"` | +| `poly!(num_clauses) + poly!(num_literals)` | `"num_clauses + num_literals"` | +| `poly!(num_clauses).scale(-5.0)` | `"-5 * num_clauses"` | +| `poly!(2 * num_vars) + poly!(5 * num_literals) + poly!(num_clauses).scale(-5.0) + poly!(3)` | `"2 * num_vars + 5 * num_literals - 5 * num_clauses + 3"` | + +**Step 1: Migrate files alphabetically** + +For each file, replace the `ReductionOverhead::new(vec![...])` body. Remove any `use crate::polynomial::Polynomial;` or `use crate::poly;` imports. The `#[reduction(overhead = { ... })]` wrapper stays the same. + +Example — `src/rules/minimumvertexcover_maximumindependentset.rs`: + +Old: +```rust +ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), +]) +``` + +New: +```rust +ReductionOverhead::new(vec![ + ("num_vertices", "num_vertices"), + ("num_edges", "num_edges"), +]) +``` + +**Step 2: Run `cargo test --lib` after each batch of ~5 files** + +Expected: pass + +**Step 3: Commit after all simple files** + +```bash +git add src/rules/*.rs +git commit -m "refactor: migrate 28 reduction files from poly! to string expressions" +``` + +--- + +### Task 9: Migrate Complex Reduction Files + +**Files:** +- Modify: `src/rules/travelingsalesman_ilp.rs` +- Modify: `src/rules/factoring_ilp.rs` + +These two files construct `Polynomial`/`Monomial` structs directly. + +**Step 1: Migrate `travelingsalesman_ilp.rs`** + +Old (manual Monomial/Polynomial construction): +```rust +("num_vars", Polynomial::var_pow("num_vertices", 2) + Polynomial { + terms: vec![Monomial { + coefficient: 2.0, + variables: vec![("num_vertices", 1), ("num_edges", 1)], + }] +}), +("num_constraints", Polynomial::var_pow("num_vertices", 3) + Polynomial { + terms: vec![ + Monomial { coefficient: -1.0, variables: vec![("num_vertices", 2)] }, + Monomial { coefficient: 2.0, variables: vec![("num_vertices", 1)] }, + Monomial { coefficient: 4.0, variables: vec![("num_vertices", 1), ("num_edges", 1)] }, + ] +}), +``` + +New: +```rust +("num_vars", "num_vertices ^ 2 + 2 * num_vertices * num_edges"), +("num_constraints", "num_vertices ^ 3 - num_vertices ^ 2 + 2 * num_vertices + 4 * num_vertices * num_edges"), +``` + +Remove `use crate::polynomial::{Monomial, Polynomial};`. + +**Step 2: Migrate `factoring_ilp.rs`** + +Old: +```rust +("num_vars", Polynomial { terms: vec![ + Monomial::var("num_bits_first").scale(2.0), + Monomial::var("num_bits_second").scale(2.0), + Monomial { coefficient: 1.0, variables: vec![("num_bits_first", 1), ("num_bits_second", 1)] }, +] }), +("num_constraints", Polynomial { terms: vec![ + Monomial { coefficient: 3.0, variables: vec![("num_bits_first", 1), ("num_bits_second", 1)] }, + Monomial::var("num_bits_first"), + Monomial::var("num_bits_second"), + Monomial::constant(1.0), +] }), +``` + +New: +```rust +("num_vars", "2 * num_bits_first + 2 * num_bits_second + num_bits_first * num_bits_second"), +("num_constraints", "3 * num_bits_first * num_bits_second + num_bits_first + num_bits_second + 1"), +``` + +Remove `use crate::polynomial::{Monomial, Polynomial};`. + +**Step 3: Run `cargo test --lib`** + +Expected: pass + +**Step 4: Commit** + +```bash +git add src/rules/travelingsalesman_ilp.rs src/rules/factoring_ilp.rs +git commit -m "refactor: migrate complex reduction overheads to string expressions" +``` + +--- + +### Task 10: Delete Polynomial Module and poly! Macro + +**Files:** +- Delete: `src/polynomial.rs` +- Delete: `src/unit_tests/polynomial.rs` +- Modify: `src/lib.rs` — remove `pub mod polynomial;` +- Modify: `src/rules/mod.rs` — remove any `poly!` re-export if present + +**Step 1: Remove `pub mod polynomial;` from `src/lib.rs`** + +**Step 2: Delete `src/polynomial.rs` and `src/unit_tests/polynomial.rs`** + +**Step 3: Search for any remaining references** + +Run: `grep -r "polynomial\|poly!" src/ --include="*.rs"` +Expected: no matches (only in test data or docs) + +**Step 4: Run full test suite** + +Run: `make test` +Expected: all pass + +**Step 5: Commit** + +```bash +git rm src/polynomial.rs src/unit_tests/polynomial.rs +git add src/lib.rs +git commit -m "refactor: remove Polynomial/Monomial/poly! (replaced by Expr)" +``` + +--- + +### Task 11: Update Examples + +**Files:** +- Modify: all `examples/reduction_*.rs` files (~30 files) + +These files call `overhead_to_json(&overhead)`. The function signature is unchanged, but the output format changed (from `MonomialJson` array to `expression` string). The `ReductionData` struct's `overhead` field type is `Vec` which is still `Serialize`. + +**Step 1: Verify examples compile** + +Run: `cargo build --examples` +Expected: pass (no source changes needed — examples call `overhead_to_json` which still works) + +If any example directly constructs `MonomialJson` or `Polynomial`, update it. + +**Step 2: Regenerate example JSON outputs** + +Run: `make examples` +Expected: JSON files regenerated with new `expression` string format + +**Step 3: Verify tests** + +Run: `make test` +Expected: all pass + +**Step 4: Commit** + +```bash +git add examples/ docs/paper/examples/ +git commit -m "chore: regenerate example JSON with expression string format" +``` + +--- + +### Task 12: Run Full Verification + +**Step 1: Format check** + +Run: `make fmt-check` +Expected: pass + +**Step 2: Clippy** + +Run: `make clippy` +Expected: no warnings + +**Step 3: Full test suite** + +Run: `make test` +Expected: all pass + +**Step 4: Build docs (includes reduction graph export)** + +Run: `make doc` +Expected: builds successfully, reduction_graph.json regenerated with expression strings + +**Step 5: Fix any issues found** + +**Step 6: Final commit if needed** + +```bash +git add -A +git commit -m "chore: fix formatting and clippy warnings" +``` From 9e29ee3b455feefb1dd4ddc4774abb6a4114f9fa Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 15 Feb 2026 01:33:02 +0800 Subject: [PATCH 3/8] feat: add symbolic expression system with AST, evaluator, Pratt parser, Display, and Serde --- src/expr.rs | 707 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/unit_tests/expr.rs | 420 ++++++++++++++++++++++++ 3 files changed, 1129 insertions(+) create mode 100644 src/expr.rs create mode 100644 src/unit_tests/expr.rs diff --git a/src/expr.rs b/src/expr.rs new file mode 100644 index 00000000..2610d212 --- /dev/null +++ b/src/expr.rs @@ -0,0 +1,707 @@ +//! Symbolic expression system for reduction overhead. +//! +//! Provides a DSL for expressing how problem sizes transform during reductions. +//! Supports arithmetic, exponentiation, and built-in math functions. + +use crate::types::ProblemSize; +use std::fmt; + +/// A symbolic expression over named variables. +#[derive(Clone, Debug)] +pub enum Expr { + /// Numeric literal. + Num(f64), + /// Named variable (e.g., `num_vertices`). + Var(Box), + /// Binary operation. + BinOp { + op: BinOp, + lhs: Box, + rhs: Box, + }, + /// Unary negation. + Neg(Box), + /// Built-in function call. + Call { func: Func, args: Vec }, +} + +/// Binary operators. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BinOp { + Add, + Sub, + Mul, + Div, + Pow, +} + +/// Built-in functions. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Func { + Log2, + Log10, + Ln, + Exp, + Sqrt, + Min, + Max, + Floor, + Ceil, + Abs, +} + +/// Evaluation error. +#[derive(Debug)] +pub enum EvalError { + UnknownVar(Box), + DivideByZero, + Arity { + func: Func, + expected: usize, + got: usize, + }, + Domain { + func: Func, + detail: Box, + }, +} + +impl fmt::Display for EvalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EvalError::UnknownVar(name) => write!(f, "unknown variable: {name}"), + EvalError::DivideByZero => write!(f, "division by zero"), + EvalError::Arity { + func, + expected, + got, + } => write!(f, "{func:?} expects {expected} args, got {got}"), + EvalError::Domain { func, detail } => write!(f, "{func:?}: {detail}"), + } + } +} + +impl std::error::Error for EvalError {} + +// ── AST construction ── + +impl Expr { + /// Convenience constructor for binary operations. + pub fn binop(op: BinOp, lhs: Expr, rhs: Expr) -> Self { + Expr::BinOp { + op, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + } + } +} + +// ── Evaluator ── + +impl Expr { + /// Evaluate the expression given variable bindings from `ProblemSize`. + pub fn evaluate(&self, size: &ProblemSize) -> Result { + match self { + Expr::Num(v) => Ok(*v), + Expr::Var(name) => size + .get(name) + .map(|v| v as f64) + .ok_or_else(|| EvalError::UnknownVar(name.clone())), + Expr::Neg(inner) => Ok(-inner.evaluate(size)?), + Expr::BinOp { op, lhs, rhs } => { + let l = lhs.evaluate(size)?; + let r = rhs.evaluate(size)?; + match op { + BinOp::Add => Ok(l + r), + BinOp::Sub => Ok(l - r), + BinOp::Mul => Ok(l * r), + BinOp::Div => { + if r == 0.0 { + return Err(EvalError::DivideByZero); + } + Ok(l / r) + } + BinOp::Pow => { + if l < 0.0 && r.fract() != 0.0 { + return Err(EvalError::Domain { + func: Func::Sqrt, + detail: "negative base with non-integer exponent".into(), + }); + } + let result = l.powf(r); + if result.is_nan() || result.is_infinite() { + return Err(EvalError::Domain { + func: Func::Exp, + detail: format!("{l} ^ {r} produced non-finite result").into(), + }); + } + Ok(result) + } + } + } + Expr::Call { func, args } => eval_func(*func, args, size), + } + } +} + +fn eval_func(func: Func, args: &[Expr], size: &ProblemSize) -> Result { + let (min_args, max_args) = match func { + Func::Min | Func::Max => (2, 2), + _ => (1, 1), + }; + if args.len() < min_args || args.len() > max_args { + return Err(EvalError::Arity { + func, + expected: min_args, + got: args.len(), + }); + } + + let a = args[0].evaluate(size)?; + match func { + Func::Log2 => { + if a <= 0.0 { + return Err(EvalError::Domain { + func, + detail: "log2 of non-positive".into(), + }); + } + Ok(a.log2()) + } + Func::Log10 => { + if a <= 0.0 { + return Err(EvalError::Domain { + func, + detail: "log10 of non-positive".into(), + }); + } + Ok(a.log10()) + } + Func::Ln => { + if a <= 0.0 { + return Err(EvalError::Domain { + func, + detail: "ln of non-positive".into(), + }); + } + Ok(a.ln()) + } + Func::Exp => { + let result = a.exp(); + if result.is_infinite() { + return Err(EvalError::Domain { + func, + detail: "exp overflow".into(), + }); + } + Ok(result) + } + Func::Sqrt => { + if a < 0.0 { + return Err(EvalError::Domain { + func, + detail: "sqrt of negative".into(), + }); + } + Ok(a.sqrt()) + } + Func::Abs => Ok(a.abs()), + Func::Floor => Ok(a.floor()), + Func::Ceil => Ok(a.ceil()), + Func::Min => { + let b = args[1].evaluate(size)?; + Ok(a.min(b)) + } + Func::Max => { + let b = args[1].evaluate(size)?; + Ok(a.max(b)) + } + } +} + +// ── Parser ── + +/// A source span for error reporting. +#[derive(Clone, Copy, Debug)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +/// Parse error. +#[derive(Debug)] +pub enum ParseError { + UnexpectedChar { ch: char, pos: usize }, + UnexpectedEof, + UnexpectedToken { span: Span, detail: String }, + UnknownFunction { name: String, span: Span }, + TrailingInput { span: Span }, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::UnexpectedChar { ch, pos } => { + write!(f, "unexpected character '{ch}' at position {pos}") + } + ParseError::UnexpectedEof => write!(f, "unexpected end of input"), + ParseError::UnexpectedToken { span, detail } => { + write!(f, "{detail} at {}..{}", span.start, span.end) + } + ParseError::UnknownFunction { name, span } => { + write!( + f, + "unknown function '{name}' at {}..{}", + span.start, span.end + ) + } + ParseError::TrailingInput { span } => { + write!(f, "trailing input at {}..{}", span.start, span.end) + } + } + } +} + +impl std::error::Error for ParseError {} + +#[derive(Clone, Debug)] +enum Token { + Num(f64), + Ident(String), + Plus, + Minus, + Star, + Slash, + Caret, + LParen, + RParen, + Comma, + Eof, +} + +#[derive(Clone, Debug)] +struct SpannedToken { + token: Token, + span: Span, +} + +struct Lexer<'a> { + input: &'a [u8], + pos: usize, +} + +impl<'a> Lexer<'a> { + fn new(input: &'a str) -> Self { + Self { + input: input.as_bytes(), + pos: 0, + } + } + + fn skip_whitespace(&mut self) { + while self.pos < self.input.len() && self.input[self.pos].is_ascii_whitespace() { + self.pos += 1; + } + } + + fn next_token(&mut self) -> Result { + self.skip_whitespace(); + let start = self.pos; + + if self.pos >= self.input.len() { + return Ok(SpannedToken { + token: Token::Eof, + span: Span { start, end: start }, + }); + } + + let ch = self.input[self.pos] as char; + match ch { + '+' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Plus, + span: Span { + start, + end: self.pos, + }, + }) + } + '-' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Minus, + span: Span { + start, + end: self.pos, + }, + }) + } + '*' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Star, + span: Span { + start, + end: self.pos, + }, + }) + } + '/' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Slash, + span: Span { + start, + end: self.pos, + }, + }) + } + '^' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Caret, + span: Span { + start, + end: self.pos, + }, + }) + } + '(' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::LParen, + span: Span { + start, + end: self.pos, + }, + }) + } + ')' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::RParen, + span: Span { + start, + end: self.pos, + }, + }) + } + ',' => { + self.pos += 1; + Ok(SpannedToken { + token: Token::Comma, + span: Span { + start, + end: self.pos, + }, + }) + } + c if c.is_ascii_digit() || c == '.' => self.lex_number(start), + c if c.is_ascii_alphabetic() || c == '_' => self.lex_ident(start), + _ => Err(ParseError::UnexpectedChar { ch, pos: start }), + } + } + + fn lex_number(&mut self, start: usize) -> Result { + while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() { + self.pos += 1; + } + if self.pos < self.input.len() && self.input[self.pos] == b'.' { + self.pos += 1; + while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() { + self.pos += 1; + } + } + let s = std::str::from_utf8(&self.input[start..self.pos]).unwrap(); + let val: f64 = s.parse().map_err(|_| ParseError::UnexpectedChar { + ch: s.chars().next().unwrap_or('?'), + pos: start, + })?; + Ok(SpannedToken { + token: Token::Num(val), + span: Span { + start, + end: self.pos, + }, + }) + } + + fn lex_ident(&mut self, start: usize) -> Result { + while self.pos < self.input.len() + && (self.input[self.pos].is_ascii_alphanumeric() || self.input[self.pos] == b'_') + { + self.pos += 1; + } + let s = std::str::from_utf8(&self.input[start..self.pos]).unwrap(); + Ok(SpannedToken { + token: Token::Ident(s.to_string()), + span: Span { + start, + end: self.pos, + }, + }) + } +} + +struct Parser { + tokens: Vec, + pos: usize, +} + +impl Parser { + fn new(input: &str) -> Result { + let mut lexer = Lexer::new(input); + let mut tokens = Vec::new(); + loop { + let tok = lexer.next_token()?; + let is_eof = matches!(tok.token, Token::Eof); + tokens.push(tok); + if is_eof { + break; + } + } + Ok(Self { tokens, pos: 0 }) + } + + fn peek(&self) -> &SpannedToken { + &self.tokens[self.pos] + } + + fn advance(&mut self) -> &SpannedToken { + let tok = &self.tokens[self.pos]; + if self.pos < self.tokens.len() - 1 { + self.pos += 1; + } + tok + } + + fn expect_rparen(&mut self) -> Result<(), ParseError> { + match &self.peek().token { + Token::RParen => { + self.advance(); + Ok(()) + } + _ => Err(ParseError::UnexpectedToken { + span: self.peek().span, + detail: "expected ')'".to_string(), + }), + } + } + + fn parse_expr(&mut self, min_bp: u8) -> Result { + let mut lhs = self.parse_prefix()?; + + loop { + let (op, l_bp, r_bp) = match &self.peek().token { + Token::Plus => (BinOp::Add, 1, 2), + Token::Minus => (BinOp::Sub, 1, 2), + Token::Star => (BinOp::Mul, 3, 4), + Token::Slash => (BinOp::Div, 3, 4), + Token::Caret => (BinOp::Pow, 7, 6), // right-assoc + _ => break, + }; + + if l_bp < min_bp { + break; + } + + self.advance(); + let rhs = self.parse_expr(r_bp)?; + lhs = Expr::binop(op, lhs, rhs); + } + + Ok(lhs) + } + + fn parse_prefix(&mut self) -> Result { + let tok = self.peek().clone(); + match &tok.token { + Token::Num(v) => { + let v = *v; + self.advance(); + Ok(Expr::Num(v)) + } + Token::Minus => { + self.advance(); + let inner = self.parse_expr(5)?; // unary minus bp + Ok(Expr::Neg(Box::new(inner))) + } + Token::LParen => { + self.advance(); + let inner = self.parse_expr(0)?; + self.expect_rparen()?; + Ok(inner) + } + Token::Ident(name) => { + let name = name.clone(); + let span = tok.span; + self.advance(); + + // Check if followed by '(' — function call + if matches!(self.peek().token, Token::LParen) { + self.advance(); // consume '(' + let func = resolve_func(&name, span)?; + let mut args = Vec::new(); + if !matches!(self.peek().token, Token::RParen) { + args.push(self.parse_expr(0)?); + while matches!(self.peek().token, Token::Comma) { + self.advance(); + args.push(self.parse_expr(0)?); + } + } + self.expect_rparen()?; + Ok(Expr::Call { func, args }) + } else { + Ok(Expr::Var(name.into())) + } + } + Token::Eof => Err(ParseError::UnexpectedEof), + _ => Err(ParseError::UnexpectedToken { + span: tok.span, + detail: "expected expression".to_string(), + }), + } + } +} + +fn resolve_func(name: &str, span: Span) -> Result { + match name.to_ascii_lowercase().as_str() { + "log2" => Ok(Func::Log2), + "log10" => Ok(Func::Log10), + "ln" => Ok(Func::Ln), + "exp" => Ok(Func::Exp), + "sqrt" => Ok(Func::Sqrt), + "min" => Ok(Func::Min), + "max" => Ok(Func::Max), + "floor" => Ok(Func::Floor), + "ceil" => Ok(Func::Ceil), + "abs" => Ok(Func::Abs), + _ => Err(ParseError::UnknownFunction { + name: name.to_string(), + span, + }), + } +} + +impl Expr { + /// Parse an expression from a string. + pub fn parse(input: &str) -> Result { + if input.trim().is_empty() { + return Err(ParseError::UnexpectedEof); + } + let mut parser = Parser::new(input)?; + let expr = parser.parse_expr(0)?; + if !matches!(parser.peek().token, Token::Eof) { + return Err(ParseError::TrailingInput { + span: parser.peek().span, + }); + } + Ok(expr) + } +} + +// ── Display ── + +impl fmt::Display for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + display_expr(self, f, 0) + } +} + +/// Display with parent context to decide parenthesization. +/// `parent_bp` is the binding power of the parent context. +fn display_expr(expr: &Expr, f: &mut fmt::Formatter<'_>, parent_bp: u8) -> fmt::Result { + match expr { + Expr::Num(v) => { + let rounded = v.round() as i64; + if (*v - rounded as f64).abs() < 1e-10 && v.is_finite() { + write!(f, "{rounded}") + } else { + write!(f, "{v}") + } + } + Expr::Var(name) => write!(f, "{name}"), + Expr::Neg(inner) => { + write!(f, "-")?; + // Wrap compound inner expressions + let needs_parens = matches!(inner.as_ref(), Expr::BinOp { .. }); + if needs_parens { + write!(f, "(")?; + display_expr(inner, f, 0)?; + write!(f, ")") + } else { + display_expr(inner, f, 5) + } + } + Expr::BinOp { op, lhs, rhs } => { + let (l_bp, r_bp) = match op { + BinOp::Add | BinOp::Sub => (1, 2), + BinOp::Mul | BinOp::Div => (3, 4), + BinOp::Pow => (7, 6), + }; + + let needs_parens = l_bp < parent_bp; + if needs_parens { + write!(f, "(")?; + } + + display_expr(lhs, f, l_bp)?; + let op_str = match op { + BinOp::Add => " + ", + BinOp::Sub => " - ", + BinOp::Mul => " * ", + BinOp::Div => " / ", + BinOp::Pow => " ^ ", + }; + write!(f, "{op_str}")?; + display_expr(rhs, f, r_bp)?; + + if needs_parens { + write!(f, ")")?; + } + Ok(()) + } + Expr::Call { func, args } => { + let name = match func { + Func::Log2 => "log2", + Func::Log10 => "log10", + Func::Ln => "ln", + Func::Exp => "exp", + Func::Sqrt => "sqrt", + Func::Min => "min", + Func::Max => "max", + Func::Floor => "floor", + Func::Ceil => "ceil", + Func::Abs => "abs", + }; + write!(f, "{name}(")?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + display_expr(arg, f, 0)?; + } + write!(f, ")") + } + } +} + +// ── Serde ── + +impl serde::Serialize for Expr { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for Expr { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Expr::parse(&s).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +#[path = "unit_tests/expr.rs"] +mod tests; diff --git a/src/lib.rs b/src/lib.rs index 30dd2a65..86d070aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ pub mod config; pub mod error; pub mod export; +pub mod expr; +pub mod graph_types; pub mod io; pub mod models; pub(crate) mod polynomial; diff --git a/src/unit_tests/expr.rs b/src/unit_tests/expr.rs new file mode 100644 index 00000000..a0abf46a --- /dev/null +++ b/src/unit_tests/expr.rs @@ -0,0 +1,420 @@ +use super::*; +use crate::types::ProblemSize; + +// === Task 1: AST and Evaluator tests === + +#[test] +fn test_eval_num() { + let expr = Expr::Num(42.0); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 42.0); +} + +#[test] +fn test_eval_var() { + let expr = Expr::Var("n".into()); + let size = ProblemSize::new(vec![("n", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 10.0); +} + +#[test] +fn test_eval_unknown_var() { + let expr = Expr::Var("missing".into()); + let size = ProblemSize::new(vec![]); + assert!(matches!( + expr.evaluate(&size), + Err(EvalError::UnknownVar(_)) + )); +} + +#[test] +fn test_eval_add() { + let expr = Expr::binop(BinOp::Add, Expr::Num(3.0), Expr::Num(4.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_sub() { + let expr = Expr::binop(BinOp::Sub, Expr::Num(10.0), Expr::Num(3.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_mul() { + let expr = Expr::binop(BinOp::Mul, Expr::Num(3.0), Expr::Var("n".into())); + let size = ProblemSize::new(vec![("n", 5)]); + assert_eq!(expr.evaluate(&size).unwrap(), 15.0); +} + +#[test] +fn test_eval_div() { + let expr = Expr::binop(BinOp::Div, Expr::Num(10.0), Expr::Num(4.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 2.5); +} + +#[test] +fn test_eval_div_by_zero() { + let expr = Expr::binop(BinOp::Div, Expr::Num(1.0), Expr::Num(0.0)); + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::DivideByZero))); +} + +#[test] +fn test_eval_pow() { + let expr = Expr::binop(BinOp::Pow, Expr::Num(2.0), Expr::Num(10.0)); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 1024.0); +} + +#[test] +fn test_eval_pow_fractional_base_negative() { + let expr = Expr::binop(BinOp::Pow, Expr::Num(-2.0), Expr::Num(0.5)); + let size = ProblemSize::new(vec![]); + assert!(matches!( + expr.evaluate(&size), + Err(EvalError::Domain { .. }) + )); +} + +#[test] +fn test_eval_neg() { + let expr = Expr::Neg(Box::new(Expr::Num(5.0))); + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), -5.0); +} + +#[test] +fn test_eval_log2() { + let expr = Expr::Call { + func: Func::Log2, + args: vec![Expr::Num(8.0)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_log2_negative() { + let expr = Expr::Call { + func: Func::Log2, + args: vec![Expr::Num(-1.0)], + }; + let size = ProblemSize::new(vec![]); + assert!(matches!( + expr.evaluate(&size), + Err(EvalError::Domain { .. }) + )); +} + +#[test] +fn test_eval_sqrt() { + let expr = Expr::Call { + func: Func::Sqrt, + args: vec![Expr::Num(25.0)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 5.0); +} + +#[test] +fn test_eval_min() { + let expr = Expr::Call { + func: Func::Min, + args: vec![Expr::Num(3.0), Expr::Num(7.0)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_max() { + let expr = Expr::Call { + func: Func::Max, + args: vec![Expr::Num(3.0), Expr::Num(7.0)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 7.0); +} + +#[test] +fn test_eval_floor() { + let expr = Expr::Call { + func: Func::Floor, + args: vec![Expr::Num(3.7)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 3.0); +} + +#[test] +fn test_eval_ceil() { + let expr = Expr::Call { + func: Func::Ceil, + args: vec![Expr::Num(3.2)], + }; + let size = ProblemSize::new(vec![]); + assert_eq!(expr.evaluate(&size).unwrap(), 4.0); +} + +#[test] +fn test_eval_arity_error() { + let expr = Expr::Call { + func: Func::Log2, + args: vec![Expr::Num(1.0), Expr::Num(2.0)], + }; + let size = ProblemSize::new(vec![]); + assert!(matches!(expr.evaluate(&size), Err(EvalError::Arity { .. }))); +} + +#[test] +fn test_eval_complex() { + // 3 * n ^ 2 + 1.44 ^ m + let expr = Expr::binop( + BinOp::Add, + Expr::binop( + BinOp::Mul, + Expr::Num(3.0), + Expr::binop(BinOp::Pow, Expr::Var("n".into()), Expr::Num(2.0)), + ), + Expr::binop(BinOp::Pow, Expr::Num(1.44), Expr::Var("m".into())), + ); + let size = ProblemSize::new(vec![("n", 4), ("m", 3)]); + let result = expr.evaluate(&size).unwrap(); + let expected = 3.0 * 16.0 + 1.44_f64.powi(3); + assert!((result - expected).abs() < 1e-10); +} + +// === Task 2: Parser tests === + +#[test] +fn test_parse_num() { + let expr = Expr::parse("42").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 42.0); +} + +#[test] +fn test_parse_float() { + let expr = Expr::parse("1.44").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 1.44); +} + +#[test] +fn test_parse_var() { + let expr = Expr::parse("num_vertices").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 10.0); +} + +#[test] +fn test_parse_add() { + let expr = Expr::parse("3 + 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 7.0); +} + +#[test] +fn test_parse_precedence_mul_add() { + // 2 + 3 * 4 = 14 (not 20) + let expr = Expr::parse("2 + 3 * 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 14.0); +} + +#[test] +fn test_parse_precedence_pow() { + // 2 ^ 3 ^ 2 = 2 ^ 9 = 512 (right-associative) + let expr = Expr::parse("2 ^ 3 ^ 2").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 512.0); +} + +#[test] +fn test_parse_unary_neg() { + let expr = Expr::parse("-5").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), -5.0); +} + +#[test] +fn test_parse_neg_pow() { + // -2 ^ 2 = -(2^2) = -4 + let expr = Expr::parse("-2 ^ 2").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), -4.0); +} + +#[test] +fn test_parse_parens() { + let expr = Expr::parse("(2 + 3) * 4").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 20.0); +} + +#[test] +fn test_parse_function_log2() { + let expr = Expr::parse("log2(8)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 3.0); +} + +#[test] +fn test_parse_function_max() { + let expr = Expr::parse("max(3, 7)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 7.0); +} + +#[test] +fn test_parse_function_case_insensitive() { + let expr = Expr::parse("Log2(8)").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 3.0); +} + +#[test] +fn test_parse_complex_expression() { + let expr = Expr::parse("3 * num_vertices ^ 2 + 1.44 ^ num_edges").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 4), ("num_edges", 3)]); + let expected = 3.0 * 16.0 + 1.44_f64.powi(3); + assert!((expr.evaluate(&size).unwrap() - expected).abs() < 1e-10); +} + +#[test] +fn test_parse_nested_functions() { + let expr = Expr::parse("floor(log2(16))").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 4.0); +} + +#[test] +fn test_parse_unknown_function() { + let result = Expr::parse("foo(3)"); + assert!(matches!(result, Err(ParseError::UnknownFunction { .. }))); +} + +#[test] +fn test_parse_unexpected_eof() { + let result = Expr::parse("3 +"); + assert!(result.is_err()); +} + +#[test] +fn test_parse_empty() { + let result = Expr::parse(""); + assert!(result.is_err()); +} + +#[test] +fn test_parse_leading_dot() { + let expr = Expr::parse(".5").unwrap(); + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 0.5); +} + +#[test] +fn test_parse_subtraction() { + let expr = Expr::parse("10 - 3 - 2").unwrap(); + // left-associative: (10 - 3) - 2 = 5 + assert_eq!(expr.evaluate(&ProblemSize::new(vec![])).unwrap(), 5.0); +} + +#[test] +fn test_parse_division() { + let expr = Expr::parse("num_vertices / 2").unwrap(); + let size = ProblemSize::new(vec![("num_vertices", 10)]); + assert_eq!(expr.evaluate(&size).unwrap(), 5.0); +} + +// === Task 3: Display and Serde tests === + +#[test] +fn test_display_num_integer() { + let expr = Expr::Num(3.0); + assert_eq!(expr.to_string(), "3"); +} + +#[test] +fn test_display_num_float() { + let expr = Expr::Num(1.44); + assert_eq!(expr.to_string(), "1.44"); +} + +#[test] +fn test_display_var() { + let expr = Expr::Var("num_vertices".into()); + assert_eq!(expr.to_string(), "num_vertices"); +} + +#[test] +fn test_display_add() { + let expr = Expr::parse("a + b").unwrap(); + assert_eq!(expr.to_string(), "a + b"); +} + +#[test] +fn test_display_precedence() { + let expr = Expr::parse("a + b * c").unwrap(); + assert_eq!(expr.to_string(), "a + b * c"); +} + +#[test] +fn test_display_parens_needed() { + let expr = Expr::parse("(a + b) * c").unwrap(); + assert_eq!(expr.to_string(), "(a + b) * c"); +} + +#[test] +fn test_display_pow() { + let expr = Expr::parse("1.44 ^ n").unwrap(); + assert_eq!(expr.to_string(), "1.44 ^ n"); +} + +#[test] +fn test_display_neg() { + let expr = Expr::parse("-x").unwrap(); + assert_eq!(expr.to_string(), "-x"); +} + +#[test] +fn test_display_neg_compound() { + let expr = Expr::parse("-(a + b)").unwrap(); + assert_eq!(expr.to_string(), "-(a + b)"); +} + +#[test] +fn test_display_func() { + let expr = Expr::parse("log2(n)").unwrap(); + assert_eq!(expr.to_string(), "log2(n)"); +} + +#[test] +fn test_display_func_two_args() { + let expr = Expr::parse("max(a, b)").unwrap(); + assert_eq!(expr.to_string(), "max(a, b)"); +} + +#[test] +fn test_roundtrip_complex() { + let cases = vec![ + "3 * n ^ 2 + 1.44 ^ m", + "log2(n) * m", + "max(n, m) + 1", + "floor(n / 2)", + "-(a + b) * c", + "a ^ b ^ c", + "a - b - c", + ]; + let size = ProblemSize::new(vec![("n", 4), ("m", 3), ("a", 2), ("b", 3), ("c", 5)]); + for case in cases { + let expr1 = Expr::parse(case).unwrap(); + let displayed = expr1.to_string(); + let expr2 = Expr::parse(&displayed).unwrap(); + let v1 = expr1.evaluate(&size).unwrap(); + let v2 = expr2.evaluate(&size).unwrap(); + assert!( + (v1 - v2).abs() < 1e-10, + "Round-trip failed for {case}: displayed as {displayed}" + ); + } +} + +#[test] +fn test_serde_roundtrip() { + let expr = Expr::parse("3 * n ^ 2 + 1").unwrap(); + let json = serde_json::to_string(&expr).unwrap(); + let back: Expr = serde_json::from_str(&json).unwrap(); + let size = ProblemSize::new(vec![("n", 5)]); + assert_eq!(expr.evaluate(&size).unwrap(), back.evaluate(&size).unwrap()); +} From 727b3ad6178c62fddcdd1833f6b813716d1123c9 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 15 Feb 2026 01:42:15 +0800 Subject: [PATCH 4/8] refactor: migrate all reduction overhead from poly! macro to Expr string expressions --- mydocs/DEVELOPER_API.md | 2483 +++++++++++++++++ mydocs/RUST_FEATURES.md | 1266 +++++++++ src/export.rs | 26 +- src/rules/circuit_spinglass.rs | 5 +- src/rules/coloring_ilp.rs | 5 +- src/rules/coloring_qubo.rs | 3 +- src/rules/cost.rs | 54 +- src/rules/factoring_circuit.rs | 5 +- src/rules/factoring_ilp.rs | 28 +- src/rules/graph.rs | 6 +- src/rules/ilp_qubo.rs | 3 +- src/rules/ksatisfiability_qubo.rs | 5 +- src/rules/maximumclique_ilp.rs | 5 +- src/rules/maximumindependentset_gridgraph.rs | 9 +- src/rules/maximumindependentset_ilp.rs | 5 +- ...maximumindependentset_maximumsetpacking.rs | 9 +- src/rules/maximumindependentset_qubo.rs | 3 +- src/rules/maximumindependentset_triangular.rs | 5 +- src/rules/maximummatching_ilp.rs | 5 +- .../maximummatching_maximumsetpacking.rs | 5 +- src/rules/maximumsetpacking_ilp.rs | 5 +- src/rules/maximumsetpacking_qubo.rs | 3 +- src/rules/minimumdominatingset_ilp.rs | 5 +- src/rules/minimumsetcovering_ilp.rs | 5 +- src/rules/minimumvertexcover_ilp.rs | 5 +- ...inimumvertexcover_maximumindependentset.rs | 9 +- .../minimumvertexcover_minimumsetcovering.rs | 5 +- src/rules/minimumvertexcover_qubo.rs | 3 +- src/rules/registry.rs | 46 +- src/rules/sat_coloring.rs | 5 +- src/rules/sat_ksat.rs | 17 +- src/rules/sat_maximumindependentset.rs | 5 +- src/rules/sat_minimumdominatingset.rs | 5 +- src/rules/spinglass_maxcut.rs | 9 +- src/rules/spinglass_qubo.rs | 5 +- src/rules/travelingsalesman_ilp.rs | 27 +- src/unit_tests/export.rs | 54 +- src/unit_tests/rules/cost.rs | 10 +- src/unit_tests/rules/registry.rs | 9 +- 39 files changed, 3930 insertions(+), 237 deletions(-) create mode 100644 mydocs/DEVELOPER_API.md create mode 100644 mydocs/RUST_FEATURES.md diff --git a/mydocs/DEVELOPER_API.md b/mydocs/DEVELOPER_API.md new file mode 100644 index 00000000..aac17295 --- /dev/null +++ b/mydocs/DEVELOPER_API.md @@ -0,0 +1,2483 @@ +# Developer API Reference + +This document provides comprehensive API documentation for developers extending the problem-reductions library. + +## Table of Contents + +**Fundamentals** +0. [Conceptual Overview](#0-conceptual-overview) +0a. [Design Philosophy](#0a-design-philosophy) +0b. [How Everything Fits Together](#0b-how-everything-fits-together) + +**API Documentation** +1. [Core Traits](#1-core-traits) +2. [Problem Parametrization](#2-problem-parametrization) +3. [Graph Topologies](#3-graph-topologies) +4. [Problem Models](#4-problem-models) +5. [Reductions System](#5-reductions-system) +6. [Solution Representation](#6-solution-representation) +7. [Error Handling](#7-error-handling) + +**Developer Guide** +8. [Common Patterns & Workflows](#8-common-patterns--workflows) +9. [Extension Guide](#9-extension-guide) +10. [Internal Modules](#10-internal-modules) +11. [Testing Utilities](#11-testing-utilities) + +**Advanced Topics** +12. [Performance Considerations](#12-performance-considerations) +13. [Known Limitations](#13-known-limitations) +14. [FAQ & Troubleshooting](#14-faq--troubleshooting) +15. [Complete End-to-End Example](#15-complete-end-to-end-example) + +**CLI Tool** +16. [CLI Tool (`pred`)](#16-cli-tool-pred) + +--- + +## 0. Conceptual Overview + +### What is problem-reductions? + +This library solves one fundamental problem in computational complexity: **establishing relationships between different NP-hard problems**. + +The key insight is that NP-hard problems are equivalent in a specific sense: +- **Polynomial reduction**: If problem A can be reduced to problem B in polynomial time, then solving B gives us a solution to A +- **Hardness**: If any NP-hard problem is solvable in polynomial time, all NP-hard problems are +- **Practical value**: Understanding reductions helps us transfer algorithms and insights between problems + +### The Two-Pillar Architecture + +The library has two main parts working together: + +**1. Problem Definitions** (what to solve) +``` +MaximumIndependentSet + ↓ describes +A problem instance with variables, constraints, objective + ↓ can be +Solved by BruteForce, or reduced to another problem +``` + +**2. Reductions** (how to transform) +``` +MaximumIndependentSet --reduce--> MinimumVertexCover + ↓ via +ReduceTo trait + ↓ provides +Variable mapping: IS_solution --extract--> VC_solution +``` + +### Mental Model: Three Levels + +``` +┌─────────────────────────────────────────────────┐ +│ ABSTRACTION (What developer thinks about) │ +│ "I have an Independent Set problem" │ +└──────────────┬──────────────────────────────────┘ + │ +┌──────────────▼──────────────────────────────────┐ +│ TRAIT LEVEL (Rust contract) │ +│ Problem trait: dims(), evaluate() │ +│ ReduceTo trait: reduce_to(), extract_solution() │ +└──────────────┬──────────────────────────────────┘ + │ +┌──────────────▼──────────────────────────────────┐ +│ CONCRETE (Actual data) │ +│ MaximumIndependentSet │ +│ - graph: SimpleGraph │ +│ - weights: Vec │ +└─────────────────────────────────────────────────┘ +``` + +### Key Concepts You'll Encounter + +| Concept | Meaning | Example | +|---------|---------|---------| +| **Problem** | A decision/optimization task with variables and constraints | "Find the maximum independent set" | +| **Configuration** | An assignment of values to all variables | `[1, 0, 1, 0, 1]` for 5 boolean variables | +| **Metric** | The evaluation result of a configuration | `SolutionSize::Valid(42)` or `bool` | +| **Variant** | Dimensions describing a problem type | `[("graph", "SimpleGraph"), ("weight", "i32")]` | +| **Reduction** | Polynomial-time transformation from problem A to problem B | SAT → MaximumIndependentSet | +| **Overhead** | How problem size grows during reduction | n' = n + 10m (n vars, m clauses) | + +--- + +## 0a. Design Philosophy + +### Why Generic Over Graph Topologies? + +**Problem**: Many graph problems exist on different graph structures: +- Simple graphs (general case) +- Grid graphs (VLSI, image processing) +- Unit disk graphs (wireless networks) +- Hypergraphs (set systems) + +**Solution**: Don't rewrite the same problem logic for each topology. Instead: + +```rust +// One problem definition works for ALL topologies +pub struct MaximumIndependentSet { + graph: G, // Can be SimpleGraph, GridGraph, UnitDiskGraph, ... + weights: Vec, // Can be i32, f64, One, ... +} +``` + +**Benefits**: +- ✅ Code reuse +- ✅ Type safety (compiler checks graph compatibility) +- ✅ Monomorphization (no runtime overhead) + +### Why Separate Types for Weight? + +**Problem**: Problems exist in both weighted and unweighted forms: +- Unweighted: all vertices have weight 1 (simpler, sometimes restricted variant) +- Weighted: vertices have different weights (general case) + +**Solution**: Use the weight type as a parameter: + +```rust +MaximumIndependentSet::::new(5, edges) // Unweighted variant +MaximumIndependentSet::::new(5, edges) // Weighted variant (all weights = 1 initially) +MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]) // Custom weights +``` + +The `One` type is a unit weight marker where `One::to_sum()` always returns `1i32`. The type alias `Unweighted = One` is also available. + +**Benefits**: +- ✅ Semantic clarity: type system enforces which variant you're using +- ✅ Metadata: variant() method can report exact variant for reduction graph +- ✅ Specialization: can optimize for `One` if needed + +### Why Traits Instead of Inheritance? + +Rust doesn't have traditional OOP inheritance, so we use traits to define contracts: + +```rust +pub trait Problem: Clone { + type Metric: Clone; + fn dims(&self) -> Vec; + fn evaluate(&self, config: &[usize]) -> Self::Metric; + // ... +} + +// Now we can write generic code that works with ANY problem type! +fn solve_any_problem(p: &P) -> Option> { + // ... +} +``` + +**Why this matters**: +- ✅ Extensible: add new problems without modifying library code +- ✅ Generic: write reduction code once, works for all graph types +- ✅ Type-safe: compiler ensures problems implement required methods + +### Variant System Explained + +The `variant()` method returns metadata about a problem instance: + +```rust +impl Problem for MaximumIndependentSet { + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), // What graph topology? + ("weight", "i32"), // What weight type? + ] + } +} +``` + +**Why this exists**: +1. **Reduction graph**: The library builds a graph of all possible problem variants and reductions +2. **Runtime metadata**: Know what variant you're working with without exposing Rust types +3. **Documentation**: Automatically discover which problems are available and how they relate + +**Think of it like**: +``` +Problem name: "MaximumIndependentSet" +├── Variant 1: graph=SimpleGraph, weight=One +├── Variant 2: graph=SimpleGraph, weight=i32 +├── Variant 3: graph=GridGraph, weight=i32 +└── Variant 4: graph=UnitDiskGraph, weight=f64 +``` + +--- + +## 0b. How Everything Fits Together + +This diagram shows how all the pieces interconnect: + +``` + ┌─────────────────────────────────────┐ + │ USER CODE │ + │ (Your application) │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────▼──────────────────────┐ + │ PROBLEM INSTANCES │ + │ MaximumIndependentSet │ + │ MinimumVertexCover │ + │ Satisfiability │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────────┼──────────────────────┐ + │ │ │ + ┌───────▼────────┐ ┌──────▼──────┐ ┌───────────▼───────┐ + │ SOLVERS │ │ REDUCTIONS │ │ INTROSPECTION │ + │ │ │ │ │ │ + │ - BruteForce │ │ ReduceTo │ │ - variant() │ + │ - ILPSolver │ │ │ │ - dims() │ + │ - Custom │ │ │ │ - NAME constant │ + └───────┬────────┘ └──────┬──────┘ └───────────┬───────┘ + │ │ │ + │ ┌───────────▼──────────────┐ │ + └─────▶│ TRAIT SYSTEM │◀──────┘ + │ (Core abstraction) │ + │ - Problem │ + │ - OptimizationProblem │ + │ - SatisfactionProblem │ + │ - ReduceTo │ + │ - Solver │ + └───────────┬──────────────┘ + │ + ┌───────────▼──────────────┐ + │ REDUCTION GRAPH │ + │ (Built at startup) │ + │ │ + │ Nodes: │ + │ MIS[SimpleGraph, i32] │ + │ MVC[SimpleGraph, i32] │ + │ SAT │ + │ │ + │ Edges: │ + │ MIS ◀─complement─▶ MVC │ + │ SAT ─reduce─▶ MIS │ + │ ... │ + └──────────────────────────┘ +``` + +### Lifecycle of a Reduction Query + +When you call `.reduce_to()`, here's what happens internally: + +``` +User code: + let reduction = source.reduce_to(); + // type inferred: ReduceTo + +⬇️ Rust compiler searches for impl ReduceTo for SourceProblem + +⬇️ If found, the impl is called: + fn reduce_to(&self) -> Self::Result { + // Construct target problem from source + // Set up solution extraction mapping + // Return result type + } + +⬇️ Result type provides: + - target_problem(): reference to constructed problem + - extract_solution(target_sol): map back to source + +⬇️ Your code can now: + // Solve target + let target_solution = solver.find_best(target_problem); + + // Extract source solution + let source_sol = reduction.extract_solution(&target_solution); + // source_sol is now valid for original problem! +``` + +### Where Variant Information Flows + +``` +Problem Type Definition: + impl Problem for MaximumIndependentSet { + fn variant() -> Vec<(&str, &str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } + } + +⬇️ Inventory System (at compile time): + Collects all Problem implementations + Extracts their variant() info + Builds reduction metadata + +⬇️ Reduction Graph (at runtime): + Nodes represent [Problem + Variant] pairs: + - MIS[SimpleGraph, i32] + - MIS[SimpleGraph, f64] + - MIS[GridGraph, i32] + - MVC[SimpleGraph, i32] + - ... + + Edges represent available reductions: + - MIS[SimpleGraph, i32] --complement--> MVC[SimpleGraph, i32] + - MIS[SimpleGraph, i32] --to-set-packing--> SetPacking[...] + - ... + +⬇️ Your Application: + Can query: "What problems can I reduce this to?" + Answer: "MVC[SimpleGraph, i32], SetPacking[...], ..." + + Can query: "What's the overhead of reducing A to B?" + Answer: "num_vars: 1n + 0m, num_constraints: 5m" +``` + +--- + +## 1. Core Traits + +This section explains the fundamental traits that define the contract for problem types. + +### Understanding Trait Hierarchy + +``` +┌─────────────────────────────────────┐ +│ Problem (base trait) │ +│ - const NAME │ +│ - type Metric │ +│ - dims(), evaluate() │ +│ - num_variables() (derived) │ +│ - variant() │ +└──────────────┬──────────────────────┘ + │ + ┌──────────┴──────────────────────────────────┐ + │ │ +┌───▼───────────────────────────┐ ┌──────────────▼───────────────┐ +│ OptimizationProblem │ │ SatisfactionProblem │ +│ (Metric = SolutionSize) │ │ (Metric = bool) │ +│ + type Value │ │ (marker trait, no methods) │ +│ + direction() │ │ │ +└───────────────────────────────┘ └──────────────────────────────┘ + +Solver + - find_best() for OptimizationProblem + - find_satisfying() for SatisfactionProblem + +ReduceTo + - reduce_to() → ReductionResult +``` + +**Key relationship**: +- All problems implement `Problem` +- Optimization problems additionally implement `OptimizationProblem` +- Decision problems additionally implement `SatisfactionProblem` +- Solvers dispatch on the problem type +- Reductions connect problem types + +### 1.1 Problem Trait + +**Location**: `src/traits.rs` + +The foundational trait that all problems must implement. + +```rust +pub trait Problem: Clone { + /// Base name of this problem type (e.g., "MaximumIndependentSet"). + const NAME: &'static str; + + /// The evaluation metric type. + type Metric: Clone; + + /// Configuration space dimensions. Each entry is the cardinality of that variable. + fn dims(&self) -> Vec; + + /// Evaluate the problem on a configuration. + fn evaluate(&self, config: &[usize]) -> Self::Metric; + + /// Number of variables (derived from dims). + fn num_variables(&self) -> usize { + self.dims().len() + } + + /// Returns variant attributes derived from type parameters. + fn variant() -> Vec<(&'static str, &'static str)>; +} +``` + +#### Associated Types + +| Type | Bounds | Purpose | +|------|--------|---------| +| `Metric` | `Clone` | Evaluation result type — `SolutionSize` for optimization, `bool` for satisfaction | + +#### Required Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `dims()` | `Vec` | Configuration space dimensions (e.g., `[2, 2, 2]` for 3 binary vars) | +| `evaluate(config)` | `Self::Metric` | Evaluate a configuration | +| `variant()` | `Vec<(&str, &str)>` | Describe problem variant (graph type, weight type) | + +#### Provided Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `num_variables()` | `usize` | Number of decision variables (default: `dims().len()`) | + +#### Contract + +- Configuration length must equal `num_variables()` (i.e., `dims().len()`) +- Each configuration value at index `i` must be in `0..dims()[i]` +- `evaluate()` must never panic on valid configs +- Invalid configs: behavior is implementation-defined (may return `SolutionSize::Invalid`, `false`, etc.) + +#### Variant Method Deep Dive + +The `variant()` method is **static** (uses `Self::` not `&self`) and returns metadata about problem variants: + +```rust +impl Problem for MaximumIndependentSet { + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } +} +``` + +**Why static?** Because we need variant info *before* creating a problem instance. This lets us: +1. Build a reduction graph at startup (knowing all available problems) +2. Match reductions without instantiating problems +3. Provide introspection without overhead + +**Common variant keys**: +- `"graph"` - Graph topology type (e.g., "SimpleGraph", "GridGraph", "UnitDiskGraph") +- `"weight"` - Weight type (e.g., "i32", "f64", "One") + +**How variants are used internally**: +```rust +// The library builds this mapping: +"MaximumIndependentSet" has variants: + - [("graph", "SimpleGraph"), ("weight", "One")] + - [("graph", "SimpleGraph"), ("weight", "i32")] + - [("graph", "SimpleGraph"), ("weight", "f64")] + - [("graph", "GridGraph"), ("weight", "i32")] + - ... + +Then it finds which reductions are available: + - MIS[SimpleGraph, i32] --complement--> MVC[SimpleGraph, i32] + - MIS[SimpleGraph, i32] --subset--> SetPacking[...] + - ... +``` + +#### Creating Configurations + +Understanding how configurations are created and what they represent: + +```rust +// Binary problem: dims() = [2, 2, 2, 2, 2] +// Variables represent yes/no decisions +let config = vec![1, 0, 1, 0, 1]; // Five binary decisions +// config[0] = 1: select vertex 0 +// config[1] = 0: don't select vertex 1 +// config[2] = 1: select vertex 2 +// etc. + +// Multi-flavor problem: dims() = [3, 3, 3, 3, 3] (e.g., k-coloring with k=3) +// Variables represent k-way choices +let config = vec![0, 1, 2, 1, 0]; // Five vertices, up to 3 colors +// config[0] = 0: color vertex 0 with color 0 +// config[1] = 1: color vertex 1 with color 1 +// etc. +``` + +### 1.2 OptimizationProblem Trait + +**Location**: `src/traits.rs` + +Extension for problems with a numeric objective to optimize. + +```rust +pub trait OptimizationProblem: Problem> { + /// The inner objective value type (e.g., `i32`, `f64`). + type Value: PartialOrd + Clone; + + /// Whether to maximize or minimize the metric. + fn direction(&self) -> Direction; +} +``` + +The supertrait bound guarantees `Metric = SolutionSize`, so the solver can call `metric.is_valid()` and `metric.is_better()` directly — no per-problem customization needed. + +#### Associated Types + +| Type | Bounds | Purpose | +|------|--------|---------| +| `Value` | `PartialOrd + Clone` | Inner objective value (e.g., `i32`, `f64`) | + +#### Required Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `direction()` | `Direction` | `Direction::Maximize` or `Direction::Minimize` | + +#### When to Use OptimizationProblem + +**Use OptimizationProblem when**: +- Your problem has a numeric objective to optimize +- Configurations can be feasible or infeasible + +**Examples that use it**: +- `MaximumIndependentSet` (maximize weight, constraint: no adjacent vertices) +- `MinimumVertexCover` (minimize weight, constraint: all edges covered) +- `MaxCut` (maximize cut weight) +- `QUBO` (minimize quadratic objective) + +### 1.3 SatisfactionProblem Trait + +**Location**: `src/traits.rs` + +Marker trait for satisfaction (decision) problems. + +```rust +pub trait SatisfactionProblem: Problem {} +``` + +Satisfaction problems evaluate configurations to `bool`: `true` if the configuration satisfies all constraints, `false` otherwise. + +**Examples that use it**: +- `Satisfiability` (SAT) +- `KSatisfiability` (k-SAT) + +### 1.4 Solver Trait + +**Location**: `src/solvers/mod.rs` + +Interface for all solvers. + +```rust +pub trait Solver { + /// Find one optimal solution for an optimization problem. + fn find_best(&self, problem: &P) -> Option>; + + /// Find any satisfying solution for a satisfaction problem (Metric = bool). + fn find_satisfying>(&self, problem: &P) -> Option>; +} +``` + +#### Contract + +- `find_best()` returns one optimal configuration, or `None` if no feasible solution exists +- `find_satisfying()` returns one satisfying configuration, or `None` if unsatisfiable +- Use `BruteForce::find_all_best()` / `find_all_satisfying()` for all solutions + +### 1.5 ReduceTo Trait + +**Location**: `src/rules/traits.rs` + +Interface for problem reductions. + +```rust +pub trait ReduceTo: Problem { + /// The reduction result type. + type Result: ReductionResult; + + /// Reduce this problem to the target problem type. + fn reduce_to(&self) -> Self::Result; +} + +pub trait ReductionResult: Clone { + /// The source problem type. + type Source: Problem; + /// The target problem type. + type Target: Problem; + + /// Get a reference to the target problem. + fn target_problem(&self) -> &Self::Target; + + /// Extract a solution from target problem space to source problem space. + fn extract_solution(&self, target_solution: &[usize]) -> Vec; +} +``` + +#### Usage + +```rust +// Reduce source problem to target problem +let source = MaximumIndependentSet::::new(5, edges); +let reduction = source.reduce_to(); // type inferred from context +let target = reduction.target_problem(); + +// Solve target and extract source solution +let solver = BruteForce::new(); +if let Some(target_sol) = solver.find_best(target) { + let source_sol = reduction.extract_solution(&target_sol); + // source_sol is now a valid solution to the original problem +} +``` + +--- + +## 2. Problem Parametrization + +### 2.1 Generic Type Parameters + +All graph-based problems are parametrized by: + +```rust +pub struct ProblemName { + graph: G, + weights: Vec, +} +``` + +- **G**: Graph topology (e.g., `SimpleGraph`, `GridGraph`, `UnitDiskGraph`) +- **W**: Weight type (e.g., `i32`, `f64`, `One`) + +#### Weight Type System + +Weights use the `WeightElement` trait: + +```rust +pub trait WeightElement: Clone + Default + 'static { + /// The numeric type used for sums and comparisons. + type Sum: NumericSize; + /// Convert this weight element to the sum type. + fn to_sum(&self) -> Self::Sum; +} +``` + +This decouples the per-element weight type from the accumulation type: +- For `i32`: `Sum = i32`, `to_sum()` returns the value +- For `f64`: `Sum = f64`, `to_sum()` returns the value +- For `One`: `Sum = i32`, `to_sum()` always returns `1` + +The `NumericSize` supertrait bundles common numeric bounds: +```rust +NumericSize: Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static +``` + +### 2.2 Unweighted Problems + +For unweighted variants, use the `One` marker type (or its alias `Unweighted`): + +```rust +use problemreductions::types::One; + +// Unweighted variant — all vertices have weight 1 +let problem = MaximumIndependentSet::::new(5, edges); +``` + +--- + +## 3. Graph Topologies + +**Location**: `src/topology/` + +### 3.1 Graph Trait + +```rust +pub trait Graph: Clone + Send + Sync + 'static { + /// The name of the graph type (e.g., "SimpleGraph", "GridGraph"). + const NAME: &'static str; + + fn num_vertices(&self) -> usize; + fn num_edges(&self) -> usize; + fn neighbors(&self, vertex: usize) -> Vec; + fn has_edge(&self, u: usize, v: usize) -> bool; + fn edges(&self) -> Vec<(usize, usize)>; + // ... other methods +} +``` + +### 3.2 Built-in Topologies + +#### SimpleGraph + +**Location**: `src/topology/graph.rs` + +Standard undirected simple graph (no self-loops, no multi-edges). + +```rust +let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3)]); +``` + +#### GridGraph + +**Location**: `src/topology/grid_graph.rs` + +Vertices arranged in a 2D grid. Vertices are neighbors if adjacent in grid. + +```rust +let graph = GridGraph::new(4, 4); // 4x4 grid, 16 vertices total +``` + +#### UnitDiskGraph + +**Location**: `src/topology/unit_disk_graph.rs` + +Vertices in 2D Euclidean space with edges between vertices within distance 1. + +```rust +let vertices = vec![(0.0, 0.0), (0.5, 0.5), (1.5, 1.5)]; +let graph = UnitDiskGraph::new(vertices); +``` + +#### Hypergraph + +**Location**: `src/topology/hypergraph.rs` + +Vertices with hyperedges (edges can contain more than 2 vertices). + +--- + +## 4. Problem Models + +**Location**: `src/models/` + +Problems are organized by category: + +### 4.1 Graph Problems + +**Location**: `src/models/graph/` + +- **MaximumIndependentSet** - Maximum weight independent set (no adjacent vertices) +- **MinimumVertexCover** - Minimum weight vertex cover (cover all edges) +- **MinimumDominatingSet** - Minimum dominating set (every vertex is dominated) +- **MaxCut** - Maximum cut (maximize edges between two partitions) +- **MaximumClique** - Maximum weight clique (all vertices pairwise adjacent) +- **MaximumMatching** - Maximum weight matching (no two edges share vertices) +- **KColoring** - K-vertex coloring (adjacent vertices have different colors) +- **MaximalIS** - Maximal (not maximum) independent set +- **TravelingSalesman** - Minimum weight Hamiltonian cycle + +#### Example: MaximumIndependentSet + +```rust +use problemreductions::models::graph::MaximumIndependentSet; +use problemreductions::topology::SimpleGraph; + +// Unit-weighted (all vertices have weight 1) +let problem = MaximumIndependentSet::::new(5, vec![(0,1), (1,2)]); + +// Custom weights +let problem = MaximumIndependentSet::::with_weights( + 5, + vec![(0,1), (1,2)], + vec![1, 2, 3, 4, 5] +); + +// From existing graph +let graph = SimpleGraph::new(5, edges); +let problem = MaximumIndependentSet::from_graph(graph, weights); + +// From graph with unit weights +let problem = MaximumIndependentSet::::from_graph_unit_weights(graph); +``` + +### 4.2 Satisfiability Problems + +**Location**: `src/models/satisfiability/` + +- **Satisfiability** - Boolean satisfiability (CNF clauses) +- **KSatisfiability** - SAT restricted to k-literal clauses + +#### Example: Satisfiability + +```rust +use problemreductions::models::satisfiability::{Satisfiability, CNFClause}; + +// Clauses in CNF: (x1 ∨ x2) ∧ (¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3) +// Literals are 1-indexed signed integers (positive = true, negative = negated) +let problem = Satisfiability::new(3, vec![ + CNFClause::new(vec![1, 2]), // x1 ∨ x2 + CNFClause::new(vec![-2, 3]), // ¬x2 ∨ x3 + CNFClause::new(vec![-1, -3]), // ¬x1 ∨ ¬x3 +]); +``` + +### 4.3 Set Problems + +**Location**: `src/models/set/` + +- **MinimumSetCovering** - Minimum weight set cover +- **MaximumSetPacking** - Maximum weight set packing + +#### Example: MinimumSetCovering + +```rust +use problemreductions::models::set::MinimumSetCovering; + +// Covering problem: select sets to cover all elements +let problem = MinimumSetCovering::::new( + 3, // universe_size (elements 0..3) + vec![ + vec![0, 1], // Set 0 covers elements 0, 1 + vec![1, 2], // Set 1 covers elements 1, 2 + vec![0, 2], // Set 2 covers elements 0, 2 + ], +); +``` + +### 4.4 Optimization Problems + +**Location**: `src/models/optimization/` + +- **SpinGlass** - Ising model Hamiltonian minimization +- **QUBO** - Quadratic unconstrained binary optimization +- **ILP** - Integer linear programming (requires `ilp` feature) + +### 4.5 Specialized Problems + +**Location**: `src/models/specialized/` + +- **CircuitSAT** - Boolean circuit satisfiability +- **Factoring** - Integer factorization +- **PaintShop** - Minimize color switches +- **BicliqueCover** - Biclique cover on bipartite graphs +- **BMF** - Boolean matrix factorization + +--- + +## 5. Reductions System + +**Location**: `src/rules/` + +### 5.1 Reduction Structure + +```rust +// Define the reduction result type +pub struct ReductionSourceToTarget { + target: TargetProblem, + // ... solution extraction data +} + +impl ReductionResult for ReductionSourceToTarget { + type Source = SourceProblem; + type Target = TargetProblem; + + fn target_problem(&self) -> &TargetProblem { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + // Map target solution back to source variables + // ... + } +} + +// Register the reduction +impl ReduceTo for SourceProblem { + type Result = ReductionSourceToTarget; + + fn reduce_to(&self) -> ReductionSourceToTarget { + // Construct target problem from source problem + // Create solution mapping + // ... + } +} + +// Register metadata for graph visualization +inventory::submit! { + ReductionEntry { + source_name: "SourceProblem", + target_name: "TargetProblem", + source_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vars", Polynomial { ... }), + ("num_constraints", Polynomial { ... }), + ]), + } +} +``` + +### 5.2 Built-in Reductions + +The library includes reductions between these problem pairs: + +- **MaximumIndependentSet ↔ MinimumVertexCover** - Complement on same graph +- **MaximumIndependentSet → MaximumSetPacking** - Element sets as independent sets +- **MaximumIndependentSet → QUBO** - Penalty encoding +- **MinimumVertexCover → MinimumSetCovering** - Elements from edges +- **MinimumVertexCover → QUBO** - Penalty encoding +- **MaximumMatching → MaximumSetPacking** - Edge representation +- **MaximumSetPacking → QUBO** - Penalty encoding +- **SAT → MaximumIndependentSet** - Variable gadgets +- **SAT → KColoring** - Clause coloring +- **SAT → MinimumDominatingSet** - Domination gadgets +- **SAT ↔ K-SAT** - Clause conversion +- **K-SAT → QUBO** - Direct QUBO encoding +- **KColoring → QUBO** - Color penalty encoding +- **SpinGlass ↔ MaxCut** - Weight transformation +- **SpinGlass ↔ QUBO** - Problem transformation +- **CircuitSAT → SpinGlass** - Logic gadgets +- **Factoring → CircuitSAT** - Multiplication circuit + +Natural-edge reductions (graph subtype relaxation): +- **MIS\ → MIS\** - Identity mapping +- **MIS\ → MIS\** - Graph cast +- **MIS\ → MIS\** - Graph cast + +Feature-gated reductions (require `ilp` feature): +- Various problems → **ILP** (MaximumIndependentSet, MinimumVertexCover, MaximumClique, MaximumMatching, MinimumDominatingSet, MinimumSetCovering, MaximumSetPacking, KColoring, Factoring, TravelingSalesman) +- **ILP → QUBO** - Linearization + +### 5.3 Reduction Overhead + +Every reduction in the library carries **overhead metadata** that describes how the target problem size relates to the source problem size. This is essential for: + +1. **Cost-aware path finding**: When multiple reduction chains exist (e.g., SAT → MIS → QUBO vs SAT → 3-SAT → QUBO), the overhead lets the system pick the chain that produces the smallest target problem for a given input +2. **Documentation**: The paper and reduction graph visualization automatically display overhead formulas +3. **Planning**: Users can estimate target problem size before actually performing the reduction + +#### Core Types + +**Location**: `src/rules/registry.rs` and `src/polynomial.rs` + +```rust +/// Overhead specification for a reduction. +pub struct ReductionOverhead { + /// Output size as polynomials of input size variables. + /// Each entry is (output_field_name, polynomial_formula). + pub output_size: Vec<(&'static str, Polynomial)>, +} +``` + +Each entry maps an **output field name** (a dimension of the target problem) to a **polynomial** of input field names (dimensions of the source problem). For example, when reducing MIS to ILP, the output might specify that ILP's `num_vars` equals the graph's `num_vertices`, and ILP's `num_constraints` equals the graph's `num_edges`. + +Polynomials are built from monomials: + +```rust +/// A monomial: coefficient × Π(variable^exponent) +pub struct Monomial { + pub coefficient: f64, + pub variables: Vec<(&'static str, u8)>, // (variable_name, exponent) +} + +/// A polynomial: Σ monomials +pub struct Polynomial { + pub terms: Vec, +} +``` + +#### The `poly!` Macro + +Instead of constructing `Polynomial` and `Monomial` values manually, use the `poly!` convenience macro: + +```rust +use problemreductions::poly; + +// Single variable: p(x) = num_vertices +poly!(num_vertices) + +// Variable with exponent: p(x) = num_literals² +poly!(num_literals ^ 2) + +// Constant: p(x) = 5 +poly!(5) + +// Scaled variable: p(x) = 3 × num_vertices +poly!(3 * num_vertices) + +// Scaled variable with exponent: p(x) = 9 × n² +poly!(9 * n ^ 2) + +// Product of two variables: p(x) = num_vertices × num_colors +poly!(num_vertices * num_colors) + +// Scaled product: p(x) = 3 × a × b +poly!(3 * a * b) + +// Addition (combine with + operator): +poly!(num_vars) + poly!(num_clauses) // p(x) = num_vars + num_clauses +``` + +#### Specifying Overhead in Reductions + +Overhead is attached to reductions via the `#[reduction]` proc macro attribute: + +```rust +#[reduction( + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]) + } +)] +impl ReduceTo> for MaximumIndependentSet { + // ... +} +``` + +The field names (e.g., `"num_vertices"`, `"num_edges"`) in the polynomial variables refer to the **source problem's** `ProblemSize` components. The field names as keys (the first element of each tuple) name the **target problem's** size dimensions. + +#### Real-World Examples + +**Identity overhead** (MIS ↔ MVC complement): +```rust +// Same graph, same size — no blowup +ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), // target has same vertices + ("num_edges", poly!(num_edges)), // target has same edges +]) +``` + +**Linear overhead** (MIS → ILP): +```rust +// One ILP variable per vertex, one constraint per edge +ReductionOverhead::new(vec![ + ("num_vars", poly!(num_vertices)), + ("num_constraints", poly!(num_edges)), +]) +``` + +**Quadratic overhead** (SAT → MIS): +```rust +// Vertices = number of literals, edges up to literals² +ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_literals)), + ("num_edges", poly!(num_literals ^ 2)), +]) +``` + +**Product overhead** (KColoring → QUBO): +```rust +// One QUBO variable per (vertex, color) pair +ReductionOverhead::new(vec![ + ("num_vars", poly!(num_vertices * num_colors)), +]) +``` + +**Additive overhead** (SAT → K-SAT clause splitting): +```rust +// Splitting long clauses adds extra variables and clauses +ReductionOverhead::new(vec![ + ("num_clauses", poly!(num_clauses) + poly!(num_literals)), + ("num_vars", poly!(num_vars) + poly!(num_literals)), +]) +``` + +#### Evaluating Overhead at Runtime + +Given a concrete input problem size, you can compute the expected target size: + +```rust +let overhead = ReductionOverhead::new(vec![ + ("num_vars", poly!(num_vertices)), + ("num_constraints", poly!(num_edges)), +]); + +let input_size = ProblemSize::new(vec![("num_vertices", 100), ("num_edges", 500)]); +let output_size = overhead.evaluate_output_size(&input_size); + +assert_eq!(output_size.get("num_vars"), Some(100)); +assert_eq!(output_size.get("num_constraints"), Some(500)); +``` + +### 5.4 Cost-Aware Path Finding + +**Location**: `src/rules/cost.rs` and `src/rules/graph.rs` + +The reduction graph supports Dijkstra-based shortest-path search with customizable cost functions. This lets you find the cheapest multi-step reduction chain between any two problem types, where "cheapest" is defined by the overhead formulas evaluated on your actual input size. + +#### PathCostFn Trait + +```rust +pub trait PathCostFn { + /// Compute cost of taking an edge given current problem size. + fn edge_cost(&self, overhead: &ReductionOverhead, current_size: &ProblemSize) -> f64; +} +``` + +#### Built-in Cost Functions + +| Cost Function | Description | Use Case | +|--------------|-------------|----------| +| `Minimize("field")` | Minimize a single output field | "I want the fewest QUBO variables" | +| `MinimizeWeighted(vec)` | Minimize weighted sum of fields | "Balance variables and constraints" | +| `MinimizeMax(vec)` | Minimize the maximum of fields | "No single dimension should blow up" | +| `MinimizeLexicographic(vec)` | Lexicographic minimization | "Minimize vars first, break ties by constraints" | +| `MinimizeSteps` | Minimize number of reduction hops | "Shortest chain regardless of size" | +| `CustomCost(closure)` | User-defined cost from closure | Any custom objective | + +#### Usage Example + +```rust +use problemreductions::rules::{ReductionGraph, Minimize}; +use problemreductions::types::ProblemSize; + +let graph = ReductionGraph::build(); + +// Find cheapest path from SAT to QUBO, minimizing QUBO variables +let input_size = ProblemSize::new(vec![ + ("num_vars", 10), + ("num_clauses", 20), + ("num_literals", 60), // 20 clauses × 3 literals +]); + +let path = graph.find_cheapest_path( + ("Satisfiability", ""), + ("QUBO", ""), + &input_size, + &Minimize("num_vars"), +); + +if let Some(path) = path { + println!("Best chain has {} steps", path.len()); + for step in &path.edges { + println!(" {} → {}", step.source_name, step.target_name); + } +} +``` + +#### How It Works + +The path finder uses Dijkstra's algorithm on the reduction graph: + +``` +1. Start at source node with input_size +2. For each outgoing edge, compute: + - edge_cost = cost_fn.edge_cost(&edge.overhead, ¤t_size) + - new_size = edge.overhead.evaluate_output_size(¤t_size) +3. Propagate new_size as current_size for the next hop +4. Continue until reaching the target node +5. Return the minimum-cost path +``` + +This means the cost function sees the **accumulated** problem size at each step, not just the original input. A chain A → B → C correctly accounts for B's intermediate size when computing the cost of B → C. + +--- + +## 6. Solution Representation + +### 6.1 Configuration Format + +A configuration is `Vec` where: +- Length equals `num_variables()` (i.e., `dims().len()`) +- Each value at index `i` is in `[0, dims()[i])` + +```rust +// Binary problems (dims = [2, 2, 2, 2, 2]) +config[i] == 0 // Variable i NOT selected +config[i] == 1 // Variable i selected + +// Multi-flavor problems (e.g., k-coloring, dims = [k, k, k, ...]) +config[i] ∈ [0, k) // Color assigned to vertex i +``` + +### 6.2 SolutionSize Enum + +**Location**: `src/types.rs` + +```rust +pub enum SolutionSize { + /// A valid (feasible) solution with the given objective value. + Valid(T), + /// An invalid (infeasible) solution that violates constraints. + Invalid, +} + +impl SolutionSize { + pub fn is_valid(&self) -> bool; + pub fn size(&self) -> Option<&T>; + pub fn unwrap(self) -> T; // panics if Invalid + pub fn map U>(self, f: F) -> SolutionSize; +} + +impl SolutionSize { + /// Returns true if self is a better solution than other for the given direction. + pub fn is_better(&self, other: &Self, direction: Direction) -> bool; +} +``` + +**Key differences from a struct-based approach**: +- `SolutionSize::Invalid` carries no size — invalid configs simply have no meaningful objective +- Pattern matching: `if let SolutionSize::Valid(size) = result { ... }` +- `is_better()` considers: Valid always beats Invalid; two Invalids are equally bad + +### 6.3 Direction Enum + +**Location**: `src/types.rs` + +```rust +pub enum Direction { + Maximize, + Minimize, +} +``` + +Used by `OptimizationProblem::direction()` and `SolutionSize::is_better()`. + +--- + +## 7. Error Handling + +### 7.1 Error Types + +**Location**: `src/error.rs` + +```rust +#[derive(Error, Debug, Clone, PartialEq)] +pub enum ProblemError { + #[error("invalid configuration size: expected {expected}, got {got}")] + InvalidConfigSize { expected: usize, got: usize }, + + #[error("invalid flavor value {value} at index {index}: expected 0..{num_flavors}")] + InvalidFlavor { index: usize, value: usize, num_flavors: usize }, + + #[error("invalid problem: {0}")] + InvalidProblem(String), + + #[error("invalid weights length: expected {expected}, got {got}")] + InvalidWeightsLength { expected: usize, got: usize }, + + #[error("empty problem: {0}")] + EmptyProblem(String), + + #[error("index out of bounds: {index} >= {bound}")] + IndexOutOfBounds { index: usize, bound: usize }, + + #[error("I/O error: {0}")] + IoError(String), + + #[error("serialization error: {0}")] + SerializationError(String), +} +``` + +### 7.2 Panic vs Result Strategy + +The library uses **panics for programming errors**: + +| Situation | Handling | Rationale | +|-----------|----------|-----------| +| Invalid vertex indices | Panic | Programming error | +| Weight length mismatch | Panic | Programming error | +| Invalid config length | Return `SolutionSize::Invalid` or `false` | Runtime validation | +| Constraint violation | Return `SolutionSize::Invalid` | Normal operation | + +**Example**: +```rust +pub fn with_weights(..., weights: Vec) -> Self { + assert_eq!(weights.len(), num_vertices, + "weights length must match num_vertices"); + // ... +} +``` + +--- + +## 8. Common Patterns & Workflows + +### Pattern 1: Solving a Problem Directly + +The simplest workflow: define a problem, then solve it. + +```rust +use problemreductions::prelude::*; +use problemreductions::models::graph::MaximumIndependentSet; +use problemreductions::topology::SimpleGraph; + +// Step 1: Create the problem +let problem = MaximumIndependentSet::::new( + 5, // 5 vertices + vec![(0, 1), (1, 2), (2, 3)] // Edges +); + +// Step 2: Verify problem properties +println!("Variables: {}", problem.num_variables()); // Output: 5 +println!("Direction: {:?}", problem.direction()); // Maximize +println!("Variant: {:?}", MaximumIndependentSet::::variant()); + +// Step 3: Solve it +let solver = BruteForce::new(); +let optimal_solutions = solver.find_all_best(&problem); + +// Step 4: Evaluate and interpret +for solution in &optimal_solutions { + let result = problem.evaluate(solution); + if let SolutionSize::Valid(size) = result { + println!("Solution: {:?}, Size: {}", solution, size); + } +} +``` + +### Pattern 2: Using Reductions + +Transform a problem you're interested in to another problem, solve it, extract the solution. + +```rust +use problemreductions::rules::ReduceTo; + +// Step 1: Create source problem +let source = MaximumIndependentSet::::new(5, edges); + +// Step 2: Reduce to target problem (must implement ReduceTo) +let reduction: ReductionISToVC<_, _> = source.reduce_to(); +let target = reduction.target_problem(); + +// Step 3: Solve the target problem +let solver = BruteForce::new(); +let target_solutions = solver.find_all_best(target); + +// Step 4: Extract source solutions from target solutions +for target_sol in &target_solutions { + let source_sol = reduction.extract_solution(target_sol); + let result = source.evaluate(&source_sol); + if let SolutionSize::Valid(size) = result { + println!("Original problem solution: {:?}, size: {}", source_sol, size); + } +} +``` + +**Why do this?** +- Different solvers might work better for the target problem +- Reductions let you leverage algorithms designed for other problems +- Understanding reductions helps verify solution correctness + +### Pattern 3: Implementing a Problem + +Create a new NP-hard problem type. + +```rust +use problemreductions::topology::{Graph, SimpleGraph}; +use problemreductions::traits::{Problem, OptimizationProblem}; +use problemreductions::types::{Direction, SolutionSize, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MyProblem { + graph: G, + weights: Vec, +} + +impl MyProblem { + pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self + where + W: From, + { + let graph = SimpleGraph::new(num_vertices, edges); + let weights = vec![W::from(1); num_vertices]; + Self { graph, weights } + } +} + +impl Problem for MyProblem +where + G: Graph, + W: WeightElement, +{ + const NAME: &'static str = "MyProblem"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", G::NAME), + ("weight", crate::variant::short_type_name::()), + ] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + // Check constraints + // ... your constraint logic here ... + + // Compute objective + let mut total = W::Sum::zero(); + for (i, &selected) in config.iter().enumerate() { + if selected == 1 { + total += self.weights[i].to_sum(); + } + } + SolutionSize::Valid(total) + } +} + +impl OptimizationProblem for MyProblem +where + G: Graph, + W: WeightElement, +{ + type Value = W::Sum; + + fn direction(&self) -> Direction { + Direction::Maximize + } +} +``` + +### Pattern 4: Implementing a Reduction + +Transform problem A into problem B, with solution extraction. + +```rust +use problemreductions::rules::{ReduceTo, ReductionResult}; + +// Result type holds both the transformed problem and mapping data +pub struct ReductionISToVC { + target: MinimumVertexCover, +} + +// Implement the result trait for extraction +impl ReductionResult for ReductionISToVC +where + G: Graph, + W: WeightElement, +{ + type Source = MaximumIndependentSet; + type Target = MinimumVertexCover; + + fn target_problem(&self) -> &MinimumVertexCover { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + // For complement: if target selects v, source selects NOT v + target_solution.iter().map(|&x| 1 - x).collect() + } +} + +// Implement the reduction +impl ReduceTo> for MaximumIndependentSet +where + G: Graph, + W: WeightElement, +{ + type Result = ReductionISToVC; + + fn reduce_to(&self) -> ReductionISToVC { + let target = MinimumVertexCover::from_graph( + self.graph().clone(), + self.weights(), + ); + ReductionISToVC { target } + } +} +``` + +### Pattern 5: Solving Satisfaction Problems + +```rust +use problemreductions::prelude::*; + +let sat = Satisfiability::new(3, vec![ + CNFClause::new(vec![1, 2]), + CNFClause::new(vec![-1, 3]), +]); + +let solver = BruteForce::new(); + +// Find one satisfying assignment +if let Some(solution) = solver.find_satisfying(&sat) { + println!("Satisfying: {:?}", solution); +} + +// Find all satisfying assignments +let all = solver.find_all_satisfying(&sat); +println!("Found {} satisfying assignments", all.len()); +``` + +--- + +## 9. Extension Guide + +### 9.1 Adding a New Graph Problem + +See [Pattern 3](#pattern-3-implementing-a-problem) above for the full implementation pattern. + +Key steps: +1. Create `src/models/graph/my_problem.rs` +2. Implement `Problem` trait with `type Metric = SolutionSize` +3. Implement `OptimizationProblem` with `direction()` +4. Register schema with `inventory::submit! { ProblemSchemaEntry { ... } }` +5. Register module in `src/models/graph/mod.rs` + +### 9.2 Adding a Reduction + +**Location**: `src/rules/_.rs` + +See [Pattern 4](#pattern-4-implementing-a-reduction) above for the implementation pattern. + +Key steps: +1. Create `src/rules/_.rs` +2. Define result struct implementing `ReductionResult` +3. Implement `ReduceTo` on the source problem +4. Register metadata with `inventory::submit! { ReductionEntry { ... } }` +5. Register module in `src/rules/mod.rs` + +Use the `#[reduction]` proc macro attribute for automatic inventory registration: +```rust +#[reduction( + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]) + } +)] +impl ReduceTo for SourceProblem { + // ... +} +``` + +### 9.3 Adding Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::solvers::BruteForce; + + #[test] + fn test_source_to_target_closed_loop() { + // 1. Create small instance + let problem = SourceProblem::::new(5, vec![(0,1), (1,2)]); + + // 2. Reduce + let reduction = problem.reduce_to(); + let target = reduction.target_problem(); + + // 3. Solve both directly + let solver = BruteForce::new(); + let source_solutions = solver.find_all_best(&problem); + let target_solutions = solver.find_all_best(target); + + // 4. Extract and verify + for sol in &target_solutions { + let extracted = reduction.extract_solution(sol); + let result = problem.evaluate(&extracted); + assert!(result.is_valid()); + } + } +} +``` + +--- + +## 10. Internal Modules + +### 10.1 Configuration Utilities + +**Location**: `src/config.rs` + +```rust +/// Iterator over all configs for uniform flavor count +pub struct ConfigIterator { /* ... */ } + +impl ConfigIterator { + pub fn new(num_variables: usize, num_flavors: usize) -> Self; + pub fn total(&self) -> usize; +} + +impl Iterator for ConfigIterator { + type Item = Vec; +} + +/// Iterator over all configs for per-variable dimensions +pub struct DimsIterator { /* ... */ } + +impl DimsIterator { + pub fn new(dims: Vec) -> Self; + pub fn total(&self) -> usize; +} + +impl Iterator for DimsIterator { + type Item = Vec; +} + +/// Convert between index and configuration +pub fn index_to_config(index: usize, num_variables: usize, num_flavors: usize) -> Vec; +pub fn config_to_index(config: &[usize], num_flavors: usize) -> usize; + +/// Convert between config and bits +pub fn config_to_bits(config: &[usize]) -> Vec; +pub fn bits_to_config(bits: &[bool]) -> Vec; +``` + +`DimsIterator` is used by `BruteForce` internally to enumerate all configurations based on `problem.dims()`. + +### 10.2 ProblemSize Metadata + +**Location**: `src/types.rs` + +```rust +pub struct ProblemSize { + pub components: Vec<(String, usize)>, +} + +impl ProblemSize { + pub fn new(components: Vec<(&str, usize)>) -> Self; + pub fn get(&self, name: &str) -> Option; +} +``` + +### 10.3 BruteForce Solver + +**Location**: `src/solvers/brute_force.rs` + +```rust +#[derive(Debug, Clone, Default)] +pub struct BruteForce; + +impl BruteForce { + pub fn new() -> Self; + + /// Find all optimal solutions for an optimization problem. + pub fn find_all_best(&self, problem: &P) -> Vec>; + + /// Find all satisfying solutions for a satisfaction problem. + pub fn find_all_satisfying>(&self, problem: &P) -> Vec>; +} + +impl Solver for BruteForce { + /// Returns one optimal solution (or None). + fn find_best(&self, problem: &P) -> Option>; + + /// Returns one satisfying solution (or None). + fn find_satisfying>(&self, problem: &P) -> Option>; +} +``` + +### 10.4 Variant and Type Name Utilities + +**Location**: `src/variant.rs` + +```rust +/// Extract short type name from full path (e.g., "my_crate::types::One" → "One") +pub fn short_type_name() -> &'static str; + +/// Convert const generic usize to static str (for values 1-10) +pub const fn const_usize_str() -> &'static str; +``` + +Used internally by `Problem::variant()` implementations to extract clean type names. + +### 10.5 Polynomial Representation + +**Location**: `src/polynomial.rs` + +Used for representing reduction overhead formulas. + +```rust +pub struct Polynomial { + pub terms: Vec, +} + +pub struct PolynomialTerm { + pub coefficient: f64, + pub variables: Vec, + pub exponent: f64, +} +``` + +### 10.6 Truth Table + +**Location**: `src/truth_table.rs` + +Utilities for working with boolean truth tables (used by CircuitSAT). + +--- + +## 11. Testing Utilities + +**Location**: `src/testing/` + +### 11.1 Test Case Structs + +```rust +/// A test case for graph problems +pub struct GraphTestCase { + pub num_vertices: usize, + pub edges: Vec<(usize, usize)>, + pub weights: Option>, + pub valid_solution: Vec, + pub expected_size: W, + pub optimal_size: Option, +} + +impl GraphTestCase { + pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>, + valid_solution: Vec, expected_size: W) -> Self; + pub fn with_weights(num_vertices: usize, edges: Vec<(usize, usize)>, + weights: Vec, valid_solution: Vec, + expected_size: W) -> Self; + pub fn with_optimal(self, optimal: W) -> Self; +} + +/// A test case for SAT problems +pub struct SatTestCase { + pub num_vars: usize, + pub clauses: Vec>, + pub satisfying_assignment: Option>, + pub is_satisfiable: bool, +} + +impl SatTestCase { + pub fn satisfiable(num_vars: usize, clauses: Vec>, + satisfying_assignment: Vec) -> Self; + pub fn unsatisfiable(num_vars: usize, clauses: Vec>) -> Self; +} +``` + +### 11.2 Test Macros + +```rust +// Generate comprehensive test suite for graph problems +graph_problem_tests! { + problem_type: MaximumIndependentSet, + test_cases: [ + (triangle, 3, [(0, 1), (1, 2), (0, 2)], [1, 0, 0], 1, true), + (path, 3, [(0, 1), (1, 2)], [1, 0, 1], 2, true), + ] +} + +// Test that two problems are complements (e.g., IS + VC = n) +complement_test! { + name: test_is_vc_complement, + problem_a: MaximumIndependentSet, + problem_b: MinimumVertexCover, + test_graphs: [ + (3, [(0, 1), (1, 2)]), + (4, [(0, 1), (1, 2), (2, 3)]), + ] +} + +// Quick single-instance validation +quick_problem_test!( + MaximumIndependentSet, + new(3, vec![(0, 1)]), + solution: [0, 0, 1], + expected_value: 1, + is_max: true +); +``` + +--- + +## 12. Performance Considerations + +### 12.1 Brute Force Complexity + +- **Time**: O(∏ dims[i] × cost_per_evaluation) +- **Space**: O(num_variables) per configuration + result storage +- **Practical limit**: ~20 binary variables + +### 12.2 Graph Representation + +Uses adjacency list internally (petgraph): + +- **Neighbor lookup**: O(degree) +- **Memory**: O(V + E) +- **Best for**: Sparse graphs + +### 12.3 Monomorphization + +Use concrete types to avoid dynamic dispatch: + +```rust +// Good: Monomorphized, fast +MaximumIndependentSet::::new(...) +MaximumIndependentSet::::new(...) + +// Also good: Generic function, monomorphized per call site +fn solve(p: &P) -> Option> { + BruteForce::new().find_best(p) +} +``` + +--- + +## 13. Known Limitations + +| Limitation | Implication | Workaround | +| --- | --- | --- | +| Vertex indices must be 0..n | No automatic remapping | Preprocess input | +| BruteForce solver is O(2^n) | Impractical for large instances | Use ILP solver (enabled by default in CLI, feature-gated in library) | +| No parallel evaluation | Single-threaded | Future: parallel config iteration | + +--- + +## Quick Reference + +### Creating Problems + +```rust +// Unit-weighted graph problem +let is = MaximumIndependentSet::::new(5, vec![(0,1), (1,2)]); + +// Weighted graph problem +let is = MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]); + +// SAT problem +let sat = Satisfiability::new(3, vec![ + CNFClause::new(vec![1, 2]), + CNFClause::new(vec![-1, 3]), + CNFClause::new(vec![-2, -3]), +]); +``` + +### Evaluating Solutions + +```rust +let config = vec![1, 0, 1, 0, 1]; +let result = problem.evaluate(&config); +match result { + SolutionSize::Valid(size) => println!("Size: {}", size), + SolutionSize::Invalid => println!("Infeasible"), +} +``` + +### Solving + +```rust +let solver = BruteForce::new(); + +// One optimal solution +if let Some(sol) = solver.find_best(&problem) { + println!("{:?}", sol); +} + +// All optimal solutions +let all = solver.find_all_best(&problem); +``` + +### Using Reductions + +```rust +let source = MaximumIndependentSet::::new(5, edges); +let reduction = source.reduce_to(); +let target = reduction.target_problem(); + +let solver = BruteForce::new(); +if let Some(target_sol) = solver.find_best(target) { + let source_sol = reduction.extract_solution(&target_sol); + println!("Source solution: {:?}", source_sol); +} +``` + +### Getting Problem Information + +```rust +let problem = MaximumIndependentSet::::new(3, vec![(0,1), (1,2)]); +println!("Name: {}", MaximumIndependentSet::::NAME); +println!("Variant: {:?}", MaximumIndependentSet::::variant()); +println!("Variables: {}", problem.num_variables()); +println!("Dims: {:?}", problem.dims()); +println!("Direction: {:?}", problem.direction()); +``` + +### CLI Quick Reference + +```bash +# Explore +pred list # all problem types +pred show MIS # problem details +pred path MIS QUBO # find reduction path + +# Create → Solve (one-liner) +pred create MIS --edges 0-1,1-2,2-3 | pred solve - + +# Create → Reduce → Solve +pred create MIS --edges 0-1,1-2,2-3 -o p.json +pred reduce p.json --to QUBO -o bundle.json +pred solve bundle.json + +# Evaluate a configuration +pred evaluate p.json --config 1,0,1,0 +``` + +## 14. FAQ & Troubleshooting + +### Common Questions + +#### Q: How do I choose between MaximumIndependentSet, MinimumVertexCover, and other graph problems? + +**A:** They're often equivalent via reductions: + +```text +Independent Set: Select vertices with no edges between them + - Goal: MAXIMIZE weight + - Constraint: No two selected vertices are adjacent + +Vertex Cover: Select vertices that touch all edges + - Goal: MINIMIZE weight + - Constraint: Every edge has at least one endpoint selected + +Clique: Select vertices that are ALL adjacent to each other + - Goal: MAXIMIZE weight + - Constraint: Every pair of selected vertices is adjacent + +Key insight: Their solutions are complements! + IS_solution = [1, 0, 1, 0] (vertices 0, 2 selected) + VC_solution = [0, 1, 0, 1] (vertices 1, 3 selected) + IS + VC = [1, 1, 1, 1] (all vertices) +``` + +**Choose based on your application**: + +- Independent Set: "find non-interfering items" (assignments, scheduling) +- Vertex Cover: "find minimum monitors" (surveillance, redundancy) +- Clique: "find communities" (social networks, dense subgraphs) + +#### Q: What's the difference between One and i32 weight type? + +**A:** + +```rust +// Both create problems with all weights = 1 +MaximumIndependentSet::::new(5, edges) +MaximumIndependentSet::::new(5, edges) // defaults to weight 1 + +// Difference is semantic and in the variant: +One: "This problem is inherently unweighted, all vertices equal" + variant() reports ("weight", "One") +i32: "This problem can have any integer weights" + variant() reports ("weight", "i32") + +// You can change weights in i32 version: +MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]) +``` + +**Use One when**: +- Problem is theoretically unweighted (pure complexity question) + +**Use i32 when**: +- Problem naturally has weights (generalization) +- You need flexibility + +#### Q: Why does reducing MaximumIndependentSet to MinimumVertexCover give a larger problem? + +**A:** In this case, it doesn't! It's a complement: + +```rust +// Same graph, same vertices +let is = MaximumIndependentSet::::new(5, edges); +let reduction: ReductionISToVC<_, _> = is.reduce_to(); + +// The reduced problem has the SAME structure +// We just flip 0→1 and 1→0 in the solution extraction +``` + +But in other reductions, overhead is real: + +```text +SAT (3 variables, 5 clauses) → MaximumIndependentSet + +Source: 3 variables, 5 clauses +Target: ~30 variables! (3 + 5*5) + Why? Each clause needs gadget vertices + +This is why some problems are "harder to solve": +- Direct brute force on SAT: 2^3 = 8 configurations +- After reduction: 2^30 = 1 billion configurations +``` + +#### Q: How do I know if a reduction is correct? + +**A:** Every reduction must pass the **closed-loop test**: + +```rust +#[test] +fn test_reduction_closed_loop() { + // 1. Create source problem + let source = SourceProblem::new(...); + + // 2. Get optimal solutions to source (direct) + let solver = BruteForce::new(); + let source_solutions = solver.find_all_best(&source); + + // 3. Reduce and solve target + let reduction = source.reduce_to(); + let target = reduction.target_problem(); + let target_solutions = solver.find_all_best(target); + + // 4. Extract and verify: target solutions map to valid source solutions + for target_sol in &target_solutions { + let extracted = reduction.extract_solution(target_sol); + let source_result = source.evaluate(&extracted); + assert!(source_result.is_valid()); + } + + // 5. Both should have same optimal value + // This proves the reduction preserves optimality +} +``` + +#### Q: Can I implement my own solver? + +**A:** Yes! Implement the `Solver` trait: + +```rust +use problemreductions::solvers::Solver; +use problemreductions::traits::{OptimizationProblem, Problem}; + +pub struct MyHeuristic { + max_iterations: usize, +} + +impl Solver for MyHeuristic { + fn find_best(&self, problem: &P) -> Option> { + // Your optimization algorithm here + None // Placeholder + } + + fn find_satisfying>(&self, problem: &P) -> Option> { + // Your satisfaction algorithm here + None // Placeholder + } +} +``` + +#### Q: What if I only want to work with certain graph types? + +**A:** You can constrain the generic parameter: + +```rust +// This function works with ANY graph topology +fn count_variables( + problem: &MaximumIndependentSet +) -> usize { + problem.num_variables() +} + +// This function works ONLY with SimpleGraph +fn my_special_solver( + problem: &MaximumIndependentSet +) -> Option> { + // Can access SimpleGraph-specific features here + let graph = problem.graph(); + // ... + None +} +``` + +#### Q: How do I serialize/deserialize problems? + +**A:** All problems implement `Serialize` and `Deserialize`: + +```rust +use serde_json; + +let problem = MaximumIndependentSet::::new(5, edges); + +// To JSON +let json = serde_json::to_string(&problem).unwrap(); + +// From JSON (if you know the type) +let loaded: MaximumIndependentSet = + serde_json::from_str(&json).unwrap(); +``` + +**Limitation**: Type information is lost. You must know the problem type when deserializing. + +--- + +### Debugging Tips + +#### Problem: "Variable out of bounds" panic + +**Cause**: You're creating edges or weights with invalid vertex indices. + +```rust +// WRONG: Vertex 5 doesn't exist (only 0-4) +let problem = MaximumIndependentSet::::new(5, vec![(0, 5)]); + +// CORRECT: Valid vertex indices +let problem = MaximumIndependentSet::::new(5, vec![(0, 4)]); +``` + +#### Problem: "Weights length must match" panic + +**Cause**: Weights vector has wrong length. + +```rust +let vertices = 5; +let edges = vec![(0, 1)]; + +// WRONG: 3 weights for 5 vertices +let p = MaximumIndependentSet::with_weights(vertices, edges, vec![1, 2, 3]); + +// CORRECT: 5 weights for 5 vertices +let p = MaximumIndependentSet::with_weights(vertices, edges, vec![1, 2, 3, 4, 5]); +``` + +#### Problem: Configuration returns Invalid even though it looks correct + +**Cause**: Likely constraint violation, not configuration validity. + +```rust +let problem = MaximumIndependentSet::::new( + 3, + vec![(0, 1), (1, 2)] // Path: 0-1-2 +); + +// INVALID: Both 0 and 1 selected, but edge (0,1) forbids this +let config = vec![1, 1, 0]; +let result = problem.evaluate(&config); +assert!(!result.is_valid()); // SolutionSize::Invalid + +// VALID: Vertices 0 and 2 are not adjacent +let config = vec![1, 0, 1]; +let result = problem.evaluate(&config); +assert!(result.is_valid()); // SolutionSize::Valid(2) +``` + +#### Problem: Reduction seems to lose solutions + +**Cause**: Check if your solution extraction is correct. + +```rust +// In your ReductionResult implementation: +impl ReductionResult for MyReduction { + type Source = SourceProblem; + type Target = TargetProblem; + + fn extract_solution(&self, target_sol: &[usize]) -> Vec { + // WRONG: Just return target solution unchanged + target_sol.to_vec() // Wrong for complement! + + // CORRECT: Apply proper transformation + target_sol.iter().map(|&x| 1 - x).collect() // Flip bits for complement + } +} +``` + +--- + +## 15. Complete End-to-End Example + +Here's a comprehensive example showing how all the pieces work together: + +```rust +use problemreductions::prelude::*; +use problemreductions::models::graph::MaximumIndependentSet; +use problemreductions::topology::SimpleGraph; +use problemreductions::solvers::BruteForce; + +fn main() { + // ============ STEP 1: Create the problem ============ + // We have a simple graph with 4 vertices and 3 edges + let problem = MaximumIndependentSet::::new( + 4, + vec![(0, 1), (1, 2), (2, 3)] // Linear chain: 0-1-2-3 + ); + + println!("=== Problem Definition ==="); + println!("Name: {}", MaximumIndependentSet::::NAME); + println!("Variant: {:?}", MaximumIndependentSet::::variant()); + println!("Variables: {}", problem.num_variables()); + println!("Dims: {:?}", problem.dims()); + println!("Direction: {:?}", problem.direction()); + + // ============ STEP 2: Manually test some configurations ============ + println!("\n=== Manual Configuration Tests ==="); + + // Configuration 1: Select only vertex 0 + let config1 = vec![1, 0, 0, 0]; + let result1 = problem.evaluate(&config1); + println!("Config [1,0,0,0]: {:?}", result1); // Valid(1) + + // Configuration 2: Select vertices 0 and 2 (non-adjacent) + let config2 = vec![1, 0, 1, 0]; + let result2 = problem.evaluate(&config2); + println!("Config [1,0,1,0]: {:?}", result2); // Valid(2) + + // Configuration 3: Select vertices 0 and 1 (adjacent - invalid!) + let config3 = vec![1, 1, 0, 0]; + let result3 = problem.evaluate(&config3); + println!("Config [1,1,0,0]: {:?}", result3); // Invalid + + // ============ STEP 3: Solve optimally using BruteForce ============ + println!("\n=== Solving with BruteForce ==="); + let solver = BruteForce::new(); + let optimal_solutions = solver.find_all_best(&problem); + + println!("Found {} optimal solutions:", optimal_solutions.len()); + for (i, solution) in optimal_solutions.iter().enumerate() { + let result = problem.evaluate(solution); + println!(" Solution {}: {:?} ({:?})", i, solution, result); + } + + // ============ STEP 4: Use reduction to solve via another problem ============ + println!("\n=== Solving via Reduction ==="); + let reduction: ReductionISToVC<_, _> = problem.reduce_to(); + let target_problem = reduction.target_problem(); + + println!("Reduced to MinimumVertexCover"); + println!("MVC variables: {}", target_problem.num_variables()); + println!("MVC direction: {:?}", target_problem.direction()); + + // Solve the target problem + let target_solutions = solver.find_all_best(target_problem); + println!("Found {} MVC solutions", target_solutions.len()); + + // Extract back to original problem + for (i, target_sol) in target_solutions.iter().enumerate() { + let extracted = reduction.extract_solution(target_sol); + let result = problem.evaluate(&extracted); + + println!(" MVC solution {}: {:?} → IS: {:?} ({:?})", + i, target_sol, extracted, result); + } + + // ============ STEP 5: Verify consistency ============ + println!("\n=== Verification ==="); + let direct_size = optimal_solutions.iter() + .filter_map(|sol| problem.evaluate(sol).size().copied()) + .max() + .unwrap_or(0); + + let via_reduction_size = target_solutions.iter() + .filter_map(|target_sol| { + let extracted = reduction.extract_solution(target_sol); + problem.evaluate(&extracted).size().copied() + }) + .max() + .unwrap_or(0); + + println!("Direct IS solve: {} vertices selected", direct_size); + println!("Via MVC reduction: {} vertices selected", via_reduction_size); + println!("Consistent: {}", direct_size == via_reduction_size); +} +``` + +### Understanding the Example + +1. **Problem Creation**: We create a MaximumIndependentSet problem on a 4-vertex path graph +2. **Manual Testing**: We evaluate three configurations to understand constraints +3. **Direct Solve**: Use BruteForce to find all optimal solutions +4. **Reduction**: Transform to MinimumVertexCover and solve via reduction +5. **Verification**: Check that both approaches agree + +**Key insights**: + +- MIS and MVC are **complement problems** on the same graph +- MIS **maximizes** (Direction::Maximize) selection +- MVC **minimizes** (Direction::Minimize) selection +- Solutions are related: IS_sol = NOT(VC_sol) +- Both methods find the same optimal value! + +--- + +## 16. CLI Tool (`pred`) + +The `pred` CLI tool provides a command-line interface for exploring NP-hard problem reductions without writing Rust code. It's published as a separate crate (`problemreductions-cli`, binary name `pred`). + +### 16.1 Installation + +```bash +# From crates.io (default: HiGHS ILP backend) +cargo install problemreductions-cli + +# With alternative ILP backends +cargo install problemreductions-cli --features coin-cbc +cargo install problemreductions-cli --features scip +cargo install problemreductions-cli --no-default-features --features clarabel + +# From source +make cli # builds target/release/pred +``` + +### 16.2 Global Flags + +All commands support these flags: + +| Flag | Description | +| --- | --- | +| `-o, --output ` | Save output as JSON to file (implies JSON mode) | +| `--json` | Output JSON to stdout instead of human-readable text | +| `-q, --quiet` | Suppress informational messages on stderr | + +### 16.3 Problem Aliases + +Short aliases are supported everywhere a problem name is expected: + +| Alias | Full Name | +| --- | --- | +| `MIS` | MaximumIndependentSet | +| `MVC` | MinimumVertexCover | +| `SAT` | Satisfiability | +| `3SAT` | KSatisfiability (K=3) | +| `KSAT` | KSatisfiability | +| `TSP` | TravelingSalesman | + +Unknown names trigger fuzzy-match suggestions. + +### 16.4 Graph Exploration + +#### `pred list` — List all problem types + +```bash +pred list # human-readable table +pred list --json | jq '.' # JSON output +``` + +#### `pred show ` — Problem details + +```bash +pred show MIS # variants, fields, reductions +pred show MIS/UnitDiskGraph # specific graph variant +``` + +#### `pred to` / `pred from` — Explore reduction neighbors + +```bash +pred to MIS # 1-hop: what MIS reduces to +pred to MIS --hops 2 # 2-hop reachable targets +pred from QUBO # what reduces to QUBO +pred from QUBO --hops 3 +``` + +Output is an ASCII tree visualization. + +#### `pred path ` — Find reduction paths + +```bash +pred path MIS QUBO # cheapest path +pred path MIS QUBO --all # all paths +pred path MIS QUBO -o path.json # save for use with `pred reduce --via` +pred path MIS QUBO --cost minimize:num_variables # custom cost function +``` + +Cost functions: +- `minimize-steps` (default) — fewest reduction hops +- `minimize:` — minimize a specific target size field (e.g., `num_variables`) + +#### `pred export-graph` — Export full reduction graph + +```bash +pred export-graph -o reduction_graph.json +``` + +### 16.5 Creating Problem Instances + +#### `pred create [OPTIONS]` + +**Graph problems** (MIS, MVC, MaxCut, MaxClique, MaximumMatching, MinimumDominatingSet, SpinGlass, TSP): + +```bash +pred create MIS --edges 0-1,1-2,2-3 -o problem.json +pred create MIS --edges 0-1,1-2 --weights 2,1,3 -o weighted.json +``` + +**SAT problems**: + +```bash +pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json +pred create 3SAT --num-vars 4 --clauses "1,2,3;-1,2,-3" -o 3sat.json +``` + +**QUBO**: + +```bash +pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json +``` + +**KColoring**: + +```bash +pred create KColoring --k 3 --edges 0-1,1-2,2-0 -o kcol.json +``` + +**Factoring**: + +```bash +pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json +``` + +**Random graph generation**: + +```bash +pred create MIS --random --num-vertices 10 --edge-prob 0.3 +pred create MIS --random --num-vertices 10 --seed 42 -o big.json +``` + +### 16.6 Evaluating and Inspecting + +#### `pred evaluate --config ` + +```bash +pred evaluate problem.json --config 1,0,1,0 +pred evaluate problem.json --config 1,0,1,0 -o result.json +``` + +#### `pred inspect ` + +Shows problem type, size metrics, available solvers, and reduction targets. + +```bash +pred inspect problem.json +pred inspect bundle.json +``` + +### 16.7 Solving + +#### `pred solve [OPTIONS]` + +```bash +pred solve problem.json # ILP solver (default, auto-reduces) +pred solve problem.json --solver brute-force # exhaustive search +pred solve problem.json --timeout 10 # abort after 10 seconds +pred solve bundle.json # solve a reduction bundle +``` + +The ILP solver auto-reduces non-ILP problems to ILP before solving. When given a reduction bundle (from `pred reduce`), it solves the target and maps the solution back. + +### 16.8 Reducing + +#### `pred reduce --to [OPTIONS]` + +```bash +pred reduce problem.json --to QUBO -o reduced.json +pred reduce problem.json --to ILP -o reduced.json +pred reduce problem.json --via path.json -o reduced.json # explicit route +``` + +Output is a reduction bundle containing source, target, and path metadata. Feed it to `pred solve` to solve and extract the original solution. + +### 16.9 Piping and Stdin + +All file-accepting commands support `-` for stdin: + +```bash +pred create MIS --edges 0-1,1-2 | pred solve - +pred create MIS --edges 0-1,1-2 | pred evaluate - --config 1,0,1 +pred create MIS --edges 0-1,1-2 | pred reduce - --to QUBO | pred solve - +pred create MIS --edges 0-1,1-2 | pred inspect - +``` + +### 16.10 Shell Completions + +```bash +# Auto-detect shell +eval "$(pred completions)" + +# Or specify: bash, zsh, fish +eval "$(pred completions zsh)" +``` + +### 16.11 End-to-End CLI Workflow + +```bash +# 1. Explore the reduction graph +pred show MIS +pred path MIS QUBO + +# 2. Create a problem instance +pred create MIS --edges 0-1,1-2,2-3,3-4,4-0 -o problem.json + +# 3. Solve directly (auto-reduces to ILP) +pred solve problem.json -o solution.json + +# 4. Or: explicit reduction + solve +pred reduce problem.json --to QUBO -o bundle.json +pred solve bundle.json --solver brute-force + +# 5. Verify a known configuration +pred evaluate problem.json --config 1,0,1,0,0 +``` + +### 16.12 JSON Output Formats + +All commands support `--json` or `-o` for structured output: + +- **Problem JSON**: `{"type": "...", "variant": {...}, "data": {...}}` +- **Reduction bundle**: `{"source": {...}, "target": {...}, "path": [...]}` +- **Solution JSON**: `{"problem": "...", "solver": "...", "solution": [...], "evaluation": "..."}` + +--- diff --git a/mydocs/RUST_FEATURES.md b/mydocs/RUST_FEATURES.md new file mode 100644 index 00000000..7d4bbb3a --- /dev/null +++ b/mydocs/RUST_FEATURES.md @@ -0,0 +1,1266 @@ +# Rust Features Guide + +This guide explains all key Rust language features used in the problem-reductions library. Perfect for Rust newcomers to understand the codebase. + +## Table of Contents + +1. [Traits](#1-traits) +2. [Generics](#2-generics) +3. [Associated Types](#3-associated-types) +4. [Trait Bounds](#4-trait-bounds) +5. [PhantomData](#5-phantomdata) +6. [Type Aliases](#6-type-aliases) +7. [Enums](#7-enums) +8. [Pattern Matching](#8-pattern-matching) +9. [Derive Macros](#9-derive-macros) +10. [Declarative Macros](#10-declarative-macros) +11. [Modules and Visibility](#11-modules-and-visibility) +12. [Iterators](#12-iterators) +13. [Closures](#13-closures) +14. [Error Handling](#14-error-handling) +15. [Lifetimes](#15-lifetimes) +16. [Const and Static](#16-const-and-static) +17. [Marker Traits](#17-marker-traits) +18. [Builder Pattern](#18-builder-pattern) +19. [Serde Serialization](#19-serde-serialization) +20. [Common Standard Library Types](#20-common-standard-library-types) + +--- + +## 1. Traits + +Traits define shared behavior. They're similar to interfaces in other languages. + +### Basic Trait Definition + +```rust +// Define a trait with required methods +pub trait Problem { + fn num_variables(&self) -> usize; + fn num_flavors(&self) -> usize; +} + +// Implement the trait for a type +impl Problem for MyProblem { + fn num_variables(&self) -> usize { + self.variables.len() + } + + fn num_flavors(&self) -> usize { + 2 // Binary problem + } +} +``` + +### Provided Methods (Default Implementations) + +```rust +pub trait Problem { + fn num_variables(&self) -> usize; // Required + + // Provided method with default implementation + fn variables(&self) -> std::ops::Range { + 0..self.num_variables() + } +} +``` + +Implementors get `variables()` for free but can override it. + +### Trait Inheritance (Supertraits) + +```rust +// ConstraintSatisfactionProblem requires Problem to be implemented first +pub trait ConstraintSatisfactionProblem: Problem { + fn constraints(&self) -> Vec; +} +``` + +**Used in this library**: +- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` +- `src/solvers/mod.rs` - `Solver` +- `src/models/graph/template.rs` - `GraphConstraint` + +--- + +## 2. Generics + +Generics allow writing code that works with multiple types. + +### Generic Structs + +```rust +// W is a type parameter (defaults to i32) +pub struct GraphProblem { + weights: Vec, + // ... +} + +// Usage +let problem: GraphProblem = ...; +let problem: GraphProblem = ...; // Different weight type +let problem: GraphProblem = ...; // Uses default W = i32 +``` + +### Generic Functions + +```rust +// T is a type parameter +fn find_best(items: &[T]) -> Option<&T> +where + T: PartialOrd, // T must be comparable +{ + items.iter().max() +} +``` + +### Generic Impl Blocks + +```rust +// Implement for all W types that meet the bounds +impl GraphProblem { + pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { + // ... + } +} +``` + +**Used in this library**: +- `GraphProblem` - generic over constraint type and weight type +- `SolutionSize` - generic over size type +- `Solver::find_best` - generic over problem type + +--- + +## 3. Associated Types + +Associated types are type placeholders in traits, defined by implementors. + +```rust +pub trait Problem { + // Associated type - implementor chooses the concrete type + type Size: Clone + PartialOrd; + + fn solution_size(&self, config: &[usize]) -> SolutionSize; +} + +// Implementation specifies the concrete type +impl Problem for IndependentSet { + type Size = i32; // This problem uses i32 for sizes + + fn solution_size(&self, config: &[usize]) -> SolutionSize { + // ... + } +} +``` + +### Why Associated Types vs Generics? + +```rust +// With associated type (cleaner) +fn solve(problem: &P) -> P::Size { ... } + +// With generics (more verbose) +fn solve, S>(problem: &P) -> S { ... } +``` + +Associated types are used when there's exactly one type that makes sense per implementation. + +**Used in this library**: +- `Problem::Size` - the objective value type + +--- + +## 4. Trait Bounds + +Trait bounds constrain generic types to those implementing certain traits. + +### Where Clauses + +```rust +impl Problem for GraphProblem +where + W: Clone + Default + PartialOrd + Num + Zero + AddAssign, +{ + // W must implement all these traits +} +``` + +### Multiple Bounds + +```rust +// Using + to require multiple traits +fn process(item: T) { ... } + +// Equivalent where clause (more readable for many bounds) +fn process(item: T) +where + T: Clone + Debug + Send, +{ ... } +``` + +### Common Trait Bounds in This Library + +| Bound | Meaning | +|-------|---------| +| `Clone` | Can be duplicated with `.clone()` | +| `Copy` | Can be copied implicitly (bit-by-bit) | +| `Default` | Has a default value via `Default::default()` | +| `PartialOrd` | Can be compared with `<`, `>`, etc. | +| `Send` | Safe to send between threads | +| `Sync` | Safe to share references between threads | +| `'static` | Contains no borrowed references (or they're `'static`) | + +**Used in this library**: +- `W: Clone + Default + PartialOrd + Num + Zero + AddAssign` +- `C: GraphConstraint` (which requires `Clone + Send + Sync + 'static`) + +--- + +## 5. PhantomData + +`PhantomData` is a zero-size type that tells the compiler "this struct logically owns a T". + +### The Problem + +```rust +// This won't compile - C is unused! +pub struct GraphProblem { + weights: Vec, + // C is not used anywhere in fields +} +``` + +### The Solution + +```rust +use std::marker::PhantomData; + +pub struct GraphProblem { + weights: Vec, + _constraint: PhantomData, // Zero-size, just marks that C is "used" +} + +// Creating PhantomData +let _constraint = PhantomData::; +// or +let _constraint: PhantomData = PhantomData; +``` + +### Why Use It? + +1. **Type safety**: Different `C` types create different `GraphProblem` types +2. **Zero runtime cost**: `PhantomData` has size 0 +3. **Compiler satisfaction**: Makes unused type parameters valid + +**Used in this library**: +- `GraphProblem` uses `PhantomData` to carry the constraint type + +--- + +## 6. Type Aliases + +Type aliases create new names for existing types. + +### Basic Alias + +```rust +// Create a shorter name +pub type IndependentSetT = GraphProblem; + +// Usage - these are equivalent: +let p1: GraphProblem = ...; +let p2: IndependentSetT = ...; +let p3: IndependentSetT = ...; // Uses default W = i32 +``` + +### Result Type Alias (Common Pattern) + +```rust +// In error.rs +pub type Result = std::result::Result; + +// Usage - cleaner than writing the full type +fn load_problem() -> Result { ... } +``` + +**Used in this library**: +- `IndependentSetT`, `VertexCoverT`, etc. +- `Result` in error handling + +--- + +## 7. Enums + +Enums define types that can be one of several variants. + +### Basic Enum + +```rust +pub enum EnergyMode { + LargerSizeIsBetter, // Maximization + SmallerSizeIsBetter, // Minimization +} + +// Usage +let mode = EnergyMode::LargerSizeIsBetter; +``` + +### Enum with Data + +```rust +pub enum ProblemCategory { + Graph(GraphSubcategory), // Contains a GraphSubcategory + Satisfiability(SatSubcategory), // Contains a SatSubcategory + Set(SetSubcategory), +} + +// Usage +let cat = ProblemCategory::Graph(GraphSubcategory::Independent); +``` + +### Enum Methods + +```rust +impl EnergyMode { + pub fn is_maximization(&self) -> bool { + matches!(self, EnergyMode::LargerSizeIsBetter) + } + + pub fn is_better(&self, a: &T, b: &T) -> bool { + match self { + EnergyMode::LargerSizeIsBetter => a > b, + EnergyMode::SmallerSizeIsBetter => a < b, + } + } +} +``` + +**Used in this library**: +- `EnergyMode` - optimization direction +- `ProblemCategory` - problem classification +- `ComplexityClass` - P, NP-complete, etc. +- `ProblemError` - error types + +--- + +## 8. Pattern Matching + +Pattern matching extracts data from enums and other types. + +### Match Expression + +```rust +match self.energy_mode() { + EnergyMode::LargerSizeIsBetter => { + // Maximization logic + } + EnergyMode::SmallerSizeIsBetter => { + // Minimization logic + } +} +``` + +### If Let (Single Pattern) + +```rust +// Only handle one variant +if let Some(weight) = self.weights.get(i) { + println!("Weight: {}", weight); +} + +// Equivalent to: +match self.weights.get(i) { + Some(weight) => println!("Weight: {}", weight), + None => {} +} +``` + +### Matches! Macro + +```rust +// Returns bool - cleaner than match for simple checks +pub fn is_hard(&self) -> bool { + matches!( + self, + ComplexityClass::NpComplete | ComplexityClass::NpHard + ) +} +``` + +### Destructuring + +```rust +// Extract data from enum variants +match category { + ProblemCategory::Graph(sub) => { + println!("Graph subcategory: {:?}", sub); + } + _ => {} // Wildcard - matches anything else +} + +// Destructure structs +let SolutionSize { size, is_valid } = problem.solution_size(&config); +``` + +**Used throughout the library** for handling enums and Option/Result types. + +--- + +## 9. Derive Macros + +Derive macros automatically implement traits for your types. + +### Common Derives + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ComplexityClass { + P, + NpComplete, + NpHard, +} +``` + +| Derive | What It Does | +|--------|--------------| +| `Debug` | Enables `{:?}` formatting for printing | +| `Clone` | Adds `.clone()` method for deep copy | +| `Copy` | Enables implicit copying (for small types) | +| `PartialEq` | Enables `==` and `!=` comparison | +| `Eq` | Marks that equality is reflexive (a == a) | +| `Hash` | Enables use as HashMap key | +| `Default` | Adds `Default::default()` constructor | + +### Serde Derives + +```rust +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct GraphProblem { + weights: Vec, + #[serde(skip)] // Don't serialize this field + _constraint: PhantomData, +} +``` + +**Used in this library**: +- Most types derive `Debug`, `Clone` +- Enums often derive `Copy`, `PartialEq`, `Eq`, `Hash` +- Problem types derive `Serialize`, `Deserialize` + +--- + +## 10. Declarative Macros + +Declarative macros (`macro_rules!`) generate code at compile time. + +### Basic Macro + +```rust +#[macro_export] // Makes macro available to users of the crate +macro_rules! quick_problem_test { + // Pattern to match + ( + $problem_type:ty, // Type + $constructor:ident($($args:expr),*), // Identifier and expressions + solution: [$($sol:expr),*], // Expressions + expected_size: $size:expr, + is_valid: $valid:expr + ) => { + // Code to generate + { + let problem = <$problem_type>::$constructor($($args),*); + let solution = vec![$($sol),*]; + let result = problem.solution_size(&solution); + assert_eq!(result.size, $size); + assert_eq!(result.is_valid, $valid); + } + }; +} +``` + +### Macro Fragment Types + +| Fragment | Matches | +|----------|---------| +| `$name:ident` | Identifier (variable/function name) | +| `$e:expr` | Expression | +| `$t:ty` | Type | +| `$p:pat` | Pattern | +| `$b:block` | Block `{ ... }` | +| `$s:stmt` | Statement | +| `$l:lifetime` | Lifetime `'a` | +| `$m:meta` | Attribute content | +| `$tt:tt` | Single token tree | + +### Repetition + +```rust +// $(...),* means "zero or more, comma-separated" +// $(...),+ means "one or more, comma-separated" +// $(...);* means "zero or more, semicolon-separated" + +macro_rules! create_vec { + ($($element:expr),*) => { + vec![$($element),*] + }; +} + +let v = create_vec![1, 2, 3]; // Expands to vec![1, 2, 3] +``` + +### Complex Macro Example + +```rust +#[macro_export] +macro_rules! graph_problem_tests { + ( + problem_type: $problem:ty, + constraint_type: $constraint:ty, + test_cases: [ + $( + ($name:ident, $n:expr, [$($edge:expr),*], [$($sol:expr),*], $size:expr, $is_max:expr) + ),* $(,)? // Optional trailing comma + ] + ) => { + mod generated_tests { + use super::*; + + $( // Repeat for each test case + mod $name { + use super::*; + + #[test] + fn test_creation() { + let problem = <$problem>::new($n, vec![$($edge),*]); + assert_eq!(problem.num_variables(), $n); + } + + #[test] + fn test_solution() { + let problem = <$problem>::new($n, vec![$($edge),*]); + let solution = vec![$($sol),*]; + let result = problem.solution_size(&solution); + assert_eq!(result.size, $size); + } + } + )* + } + }; +} +``` + +**Used in this library**: +- `src/testing/macros.rs` - `graph_problem_tests!`, `complement_test!`, `quick_problem_test!` + +--- + +## 11. Modules and Visibility + +Rust organizes code into modules with explicit visibility. + +### Module Declaration + +```rust +// In lib.rs or main.rs +pub mod models; // Load from models/mod.rs or models.rs +pub mod solvers; +mod internal; // Private module (no pub) + +// In models/mod.rs +pub mod graph; // Load from models/graph/mod.rs +pub mod sat; +``` + +### Visibility Modifiers + +| Modifier | Meaning | +|----------|---------| +| (none) | Private to current module | +| `pub` | Public to everyone | +| `pub(crate)` | Public within this crate only | +| `pub(super)` | Public to parent module | +| `pub(in path)` | Public to specific path | + +```rust +pub struct MyStruct { + pub public_field: i32, + private_field: i32, // Private + pub(crate) crate_field: i32, // Crate-public +} +``` + +### Re-exports + +```rust +// In lib.rs - make nested items available at crate root +pub use models::graph::IndependentSetT; +pub use solvers::BruteForce; + +// Users can now write: +use problemreductions::IndependentSetT; +// Instead of: +use problemreductions::models::graph::IndependentSetT; +``` + +### Prelude Pattern + +```rust +// In prelude.rs - common imports bundled together +pub use crate::traits::{Problem, ConstraintSatisfactionProblem}; +pub use crate::types::{EnergyMode, SolutionSize}; +pub use crate::solvers::BruteForce; + +// Users import everything at once: +use problemreductions::prelude::*; +``` + +**Used in this library**: +- `src/lib.rs` - module declarations and re-exports +- `src/prelude.rs` - common imports +- Each subdirectory has `mod.rs` for organization + +--- + +## 12. Iterators + +Iterators provide a way to process sequences of elements. + +### Iterator Trait + +```rust +pub trait Iterator { + type Item; + fn next(&mut self) -> Option; + // Many provided methods... +} +``` + +### Common Iterator Methods + +```rust +let numbers = vec![1, 2, 3, 4, 5]; + +// map - transform each element +let doubled: Vec = numbers.iter().map(|x| x * 2).collect(); + +// filter - keep elements matching predicate +let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect(); + +// fold - accumulate into single value +let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); + +// enumerate - add indices +for (i, x) in numbers.iter().enumerate() { + println!("Index {}: {}", i, x); +} + +// any/all - boolean checks +let has_five = numbers.iter().any(|&x| x == 5); +let all_positive = numbers.iter().all(|&x| x > 0); + +// find - get first matching element +let first_even = numbers.iter().find(|&&x| x % 2 == 0); + +// chain - combine iterators +let combined: Vec = vec![1, 2].into_iter() + .chain(vec![3, 4].into_iter()) + .collect(); +``` + +### iter() vs into_iter() vs iter_mut() + +```rust +let mut v = vec![1, 2, 3]; + +// iter() - borrows, yields &T +for x in v.iter() { /* x is &i32 */ } + +// iter_mut() - mutable borrow, yields &mut T +for x in v.iter_mut() { *x += 1; } + +// into_iter() - takes ownership, yields T +for x in v.into_iter() { /* x is i32, v is consumed */ } +``` + +### Custom Iterator + +```rust +pub struct ConfigIterator { + current: usize, + total: usize, + num_variables: usize, + num_flavors: usize, +} + +impl Iterator for ConfigIterator { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.current >= self.total { + return None; + } + let config = index_to_config(self.current, self.num_variables, self.num_flavors); + self.current += 1; + Some(config) + } +} +``` + +**Used in this library**: +- `ConfigIterator` for enumerating all configurations +- Extensive use of iterator combinators in solvers and constraint evaluation + +--- + +## 13. Closures + +Closures are anonymous functions that can capture their environment. + +### Basic Closure + +```rust +// Closure with inferred types +let add = |a, b| a + b; +let result = add(2, 3); // 5 + +// Closure with explicit types +let add: fn(i32, i32) -> i32 = |a, b| a + b; + +// Multi-line closure +let complex = |x| { + let y = x * 2; + y + 1 +}; +``` + +### Capturing Variables + +```rust +let factor = 2; + +// Borrow by reference (Fn trait) +let multiply = |x| x * factor; + +// Borrow mutably (FnMut trait) +let mut count = 0; +let mut increment = || { count += 1; }; + +// Take ownership (FnOnce trait) +let data = vec![1, 2, 3]; +let consume = move || { + println!("{:?}", data); + // data is now owned by the closure +}; +``` + +### Closures as Arguments + +```rust +// Using iterator methods +let doubled: Vec = numbers.iter() + .map(|x| x * 2) // Closure as argument + .filter(|x| x > &5) // Another closure + .collect(); + +// Custom function taking closure +fn apply_twice(f: F, x: i32) -> i32 +where + F: Fn(i32) -> i32, // F is a closure that takes and returns i32 +{ + f(f(x)) +} +``` + +**Used in this library**: +- Iterator chains in constraint evaluation +- Map/filter operations on solutions + +--- + +## 14. Error Handling + +Rust uses `Result` and `Option` for error handling instead of exceptions. + +### Option Type + +```rust +enum Option { + Some(T), + None, +} + +// Usage +fn find_weight(&self, index: usize) -> Option<&W> { + self.weights.get(index) // Returns None if out of bounds +} + +// Handling Option +match result { + Some(value) => println!("Found: {}", value), + None => println!("Not found"), +} + +// Shortcuts +let value = result.unwrap(); // Panics if None +let value = result.unwrap_or(default); // Use default if None +let value = result.expect("message"); // Panics with message if None +let value = result?; // Return None early if None +``` + +### Result Type + +```rust +enum Result { + Ok(T), + Err(E), +} + +// Usage +fn parse_config(s: &str) -> Result { + if s.is_empty() { + return Err(ParseError::Empty); + } + Ok(Config::new(s)) +} + +// Handling Result +match result { + Ok(value) => println!("Success: {:?}", value), + Err(e) => println!("Error: {:?}", e), +} + +// The ? operator - propagate errors +fn load_and_process() -> Result { + let config = parse_config(input)?; // Returns Err early if error + let data = load_data(&config)?; + Ok(process(data)) +} +``` + +### thiserror Crate + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ProblemError { + #[error("invalid configuration size: expected {expected}, got {got}")] + InvalidConfigSize { expected: usize, got: usize }, + + #[error("invalid problem: {0}")] + InvalidProblem(String), +} +``` + +### Panic vs Result + +```rust +// Use panic for programming errors (bugs) +assert_eq!(weights.len(), num_vertices); // Panics if false + +// Use Result for recoverable errors +fn load_file(path: &str) -> Result { ... } +``` + +**Used in this library**: +- `ProblemError` enum with `thiserror` +- `assert!` and `assert_eq!` for invariant checking +- `Option` for optional weights and metadata + +--- + +## 15. Lifetimes + +Lifetimes ensure references are valid for as long as they're used. + +### Basic Lifetime Annotation + +```rust +// 'a is a lifetime parameter +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { x } else { y } +} +// Return value lives as long as both inputs +``` + +### Lifetime in Structs + +```rust +// Struct containing a reference needs lifetime +struct ProblemRef<'a> { + data: &'a [usize], +} + +impl<'a> ProblemRef<'a> { + fn new(data: &'a [usize]) -> Self { + Self { data } + } +} +``` + +### Static Lifetime + +```rust +// 'static means the reference lives for the entire program +const NAME: &'static str = "Independent Set"; + +// Often written without 'static (it's inferred for string literals) +const NAME: &str = "Independent Set"; +``` + +### Lifetime Elision + +Rust infers lifetimes in common cases: + +```rust +// These are equivalent: +fn first(s: &str) -> &str { ... } +fn first<'a>(s: &'a str) -> &'a str { ... } + +// Rules: +// 1. Each input reference gets its own lifetime +// 2. If one input reference, output gets same lifetime +// 3. If &self or &mut self, output gets self's lifetime +``` + +**Used in this library**: +- `&'static str` for constant strings in `ProblemInfo` +- `&'static [&'static str]` for aliases arrays +- Mostly elided (automatic) in method signatures + +--- + +## 16. Const and Static + +### Const + +Compile-time constants, inlined everywhere they're used. + +```rust +impl GraphConstraint for IndependentSetConstraint { + const NAME: &'static str = "Independent Set"; + const ENERGY_MODE: EnergyMode = EnergyMode::LargerSizeIsBetter; +} + +// In traits - associated constants +pub trait GraphConstraint { + const NAME: &'static str; // Must be provided by implementor + const ALIASES: &'static [&'static str] = &[]; // Default value +} +``` + +### Const Fn + +Functions that can be evaluated at compile time. + +```rust +impl ProblemInfo { + // Can be called in const context + pub const fn new(name: &'static str, description: &'static str) -> Self { + Self { + name, + description, + complexity_class: ComplexityClass::NpComplete, + // ... + } + } + + pub const fn with_complexity(mut self, class: ComplexityClass) -> Self { + self.complexity_class = class; + self + } +} + +// Can create ProblemInfo at compile time +const MY_INFO: ProblemInfo = ProblemInfo::new("My Problem", "Description") + .with_complexity(ComplexityClass::NpComplete); +``` + +### Static + +Global variables with a fixed memory address. + +```rust +// Mutable static requires unsafe +static mut COUNTER: usize = 0; + +// Prefer const or lazy_static for most cases +``` + +**Used in this library**: +- Associated `const` in `GraphConstraint` trait +- `const fn` builder methods in `ProblemInfo` +- `&'static str` for string constants + +--- + +## 17. Marker Traits + +Marker traits indicate properties without providing methods. + +### Send and Sync + +```rust +// Send: Safe to transfer ownership between threads +// Sync: Safe to share references between threads (&T is Send) + +pub trait GraphConstraint: Clone + Send + Sync + 'static { + // Implementations must be thread-safe +} +``` + +### Sized + +```rust +// Most types are Sized (known size at compile time) +// ?Sized allows dynamically-sized types (like trait objects) +fn process(value: &T) { ... } +``` + +### Copy + +```rust +#[derive(Copy, Clone)] +pub struct Point { + x: i32, + y: i32, +} + +// Copy types are implicitly copied (not moved) +let p1 = Point { x: 1, y: 2 }; +let p2 = p1; // p1 is copied, both are valid +``` + +**Used in this library**: +- `GraphConstraint: Send + Sync + 'static` for thread safety +- Small enums derive `Copy` (e.g., `ComplexityClass`, `EnergyMode`) + +--- + +## 18. Builder Pattern + +A pattern for constructing complex objects step by step. + +### Basic Builder + +```rust +pub struct ProblemInfo { + pub name: &'static str, + pub description: &'static str, + pub complexity_class: ComplexityClass, + pub aliases: &'static [&'static str], +} + +impl ProblemInfo { + // Start with required fields + pub const fn new(name: &'static str, description: &'static str) -> Self { + Self { + name, + description, + complexity_class: ComplexityClass::Unknown, + aliases: &[], + } + } + + // Builder methods return Self for chaining + pub const fn with_complexity(mut self, class: ComplexityClass) -> Self { + self.complexity_class = class; + self + } + + pub const fn with_aliases(mut self, aliases: &'static [&'static str]) -> Self { + self.aliases = aliases; + self + } +} + +// Usage - method chaining +let info = ProblemInfo::new("My Problem", "Description") + .with_complexity(ComplexityClass::NpComplete) + .with_aliases(&["MP", "MyProb"]); +``` + +**Used in this library**: +- `ProblemInfo` builder for metadata +- `GraphTestCase` builder for test cases + +--- + +## 19. Serde Serialization + +Serde provides automatic serialization/deserialization. + +### Basic Usage + +```rust +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Problem { + name: String, + size: usize, +} + +// Serialize to JSON +let json = serde_json::to_string(&problem)?; + +// Deserialize from JSON +let problem: Problem = serde_json::from_str(&json)?; +``` + +### Field Attributes + +```rust +#[derive(Serialize, Deserialize)] +pub struct GraphProblem { + weights: Vec, + + #[serde(skip)] // Don't serialize this field + _constraint: PhantomData, + + #[serde(default)] // Use Default if missing + optional_field: Option, + + #[serde(rename = "numVertices")] // Different name in JSON + num_vertices: usize, +} +``` + +### Custom Serialization + +```rust +use serde::{Serializer, Deserializer}; + +#[derive(Serialize, Deserialize)] +pub struct MyType { + #[serde(serialize_with = "serialize_special")] + special_field: SpecialType, +} + +fn serialize_special(value: &SpecialType, serializer: S) -> Result +where + S: Serializer, +{ + // Custom serialization logic + serializer.serialize_str(&value.to_string()) +} +``` + +**Used in this library**: +- Problem types derive `Serialize, Deserialize` +- `#[serde(skip)]` for `PhantomData` fields +- Category enums are serializable + +--- + +## 20. Common Standard Library Types + +### Vec + +Dynamic array. + +```rust +let mut v: Vec = Vec::new(); +v.push(1); +v.push(2); + +// Macro shorthand +let v = vec![1, 2, 3]; + +// Access +let first = v[0]; +let maybe = v.get(0); // Returns Option<&T> + +// Iterate +for x in &v { ... } +for x in v.iter() { ... } +for x in v.into_iter() { ... } // Consumes v +``` + +### HashMap + +Key-value store. + +```rust +use std::collections::HashMap; + +let mut map = HashMap::new(); +map.insert("key", 42); + +let value = map.get("key"); // Option<&V> +let value = map["key"]; // Panics if missing + +for (key, value) in &map { ... } +``` + +### Range + +```rust +// Range types +0..5 // 0, 1, 2, 3, 4 (exclusive end) +0..=5 // 0, 1, 2, 3, 4, 5 (inclusive end) +..5 // RangeTo +5.. // RangeFrom +.. // RangeFull + +// Usage +for i in 0..5 { ... } +let slice = &array[1..3]; +``` + +### String vs &str + +```rust +// &str - string slice, borrowed, immutable +let s: &str = "hello"; + +// String - owned, growable +let s: String = String::from("hello"); +let s: String = "hello".to_string(); + +// Convert +let slice: &str = &owned_string; +let owned: String = slice.to_string(); +``` + +### Box + +Heap allocation for single values. + +```rust +// Allocate on heap +let boxed: Box = Box::new(5); + +// Useful for recursive types +enum List { + Cons(i32, Box), + Nil, +} +``` + +--- + +## Summary: Most Important Features + +For extending this library, focus on these features: + +1. **Traits** - Define shared behavior (`Problem`, `GraphConstraint`) +2. **Generics** - Write flexible code (`GraphProblem`) +3. **Associated Types** - Type placeholders in traits (`Problem::Size`) +4. **Trait Bounds** - Constrain generic types (`W: Clone + Default`) +5. **PhantomData** - Carry unused type parameters +6. **Type Aliases** - Convenient names (`IndependentSetT`) +7. **Enums** - Multiple variants (`EnergyMode`, `ProblemCategory`) +8. **Derive Macros** - Auto-implement traits (`#[derive(Debug, Clone)]`) +9. **Declarative Macros** - Code generation (`graph_problem_tests!`) +10. **Iterators** - Process collections functionally + +## Further Reading + +- [The Rust Book](https://doc.rust-lang.org/book/) +- [Rust By Example](https://doc.rust-lang.org/rust-by-example/) +- [Rustlings](https://github.com/rust-lang/rustlings) - Small exercises +- [std documentation](https://doc.rust-lang.org/std/) diff --git a/src/export.rs b/src/export.rs index 32639855..b6920134 100644 --- a/src/export.rs +++ b/src/export.rs @@ -26,18 +26,11 @@ pub struct ProblemSide { pub instance: serde_json::Value, } -/// A monomial in JSON: coefficient × Π(variable^exponent). -#[derive(Serialize, Clone, Debug)] -pub struct MonomialJson { - pub coefficient: f64, - pub variables: Vec<(String, u8)>, -} - -/// One output field mapped to a polynomial. +/// One output field mapped to an expression string. #[derive(Serialize, Clone, Debug)] pub struct OverheadEntry { pub field: String, - pub polynomial: Vec, + pub expression: String, } /// Top-level reduction structure (written to `.json`). @@ -66,20 +59,9 @@ pub fn overhead_to_json(overhead: &ReductionOverhead) -> Vec { overhead .output_size .iter() - .map(|(field, poly)| OverheadEntry { + .map(|(field, expr)| OverheadEntry { field: field.to_string(), - polynomial: poly - .terms - .iter() - .map(|m| MonomialJson { - coefficient: m.coefficient, - variables: m - .variables - .iter() - .map(|(name, exp)| (name.to_string(), *exp)) - .collect(), - }) - .collect(), + expression: expr.to_string(), }) .collect() } diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 30a019bf..1fbf7778 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -8,7 +8,6 @@ use crate::models::optimization::SpinGlass; use crate::models::specialized::{Assignment, BooleanExpr, BooleanOp, CircuitSAT}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -416,8 +415,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_spins", poly!(num_assignments)), - ("num_interactions", poly!(num_assignments)), + ("num_spins", "num_assignments"), + ("num_interactions", "num_assignments"), ]) } )] diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index f9e982da..b250bc5a 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -9,7 +9,6 @@ use crate::models::graph::KColoring; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -125,8 +124,8 @@ fn reduce_kcoloring_to_ilp( #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices ^ 2)), - ("num_constraints", poly!(num_vertices) + poly!(num_vertices * num_edges)), + ("num_vars", "num_vertices * num_colors"), + ("num_constraints", "num_vertices + num_edges * num_colors"), ]) } )] diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index 4a2f954d..4d6f20f1 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -10,7 +10,6 @@ use crate::models::graph::KColoring; use crate::models::optimization::QUBO; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -107,7 +106,7 @@ fn reduce_kcoloring_to_qubo( // Register only the KN variant in the reduction graph #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_vertices ^ 2))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vertices * num_colors")]) } )] impl ReduceTo> for KColoring { type Result = ReductionKColoringToQUBO; diff --git a/src/rules/cost.rs b/src/rules/cost.rs index 0cbf1bf1..33463312 100644 --- a/src/rules/cost.rs +++ b/src/rules/cost.rs @@ -14,7 +14,59 @@ pub struct Minimize(pub &'static str); impl PathCostFn for Minimize { fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { - overhead.evaluate_output_size(size).get(self.0).unwrap_or(0) as f64 + overhead + .evaluate_output_size(size) + .expect("overhead evaluation failed") + .get(self.0) + .unwrap_or(0) as f64 + } +} + +/// Minimize weighted sum of output fields. +pub struct MinimizeWeighted(pub Vec<(&'static str, f64)>); + +impl PathCostFn for MinimizeWeighted { + fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { + let output = overhead + .evaluate_output_size(size) + .expect("overhead evaluation failed"); + self.0 + .iter() + .map(|(field, weight)| weight * output.get(field).unwrap_or(0) as f64) + .sum() + } +} + +/// Minimize the maximum of specified fields. +pub struct MinimizeMax(pub Vec<&'static str>); + +impl PathCostFn for MinimizeMax { + fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { + let output = overhead + .evaluate_output_size(size) + .expect("overhead evaluation failed"); + self.0 + .iter() + .map(|field| output.get(field).unwrap_or(0) as f64) + .fold(0.0, f64::max) + } +} + +/// Lexicographic: minimize first field, break ties with subsequent. +pub struct MinimizeLexicographic(pub Vec<&'static str>); + +impl PathCostFn for MinimizeLexicographic { + fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { + let output = overhead + .evaluate_output_size(size) + .expect("overhead evaluation failed"); + let mut cost = 0.0; + let mut scale = 1.0; + for field in &self.0 { + cost += scale * output.get(field).unwrap_or(0) as f64; + scale *= 1e-10; + } + cost } } diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index e43a8853..cddba9bd 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -8,7 +8,6 @@ //! carry propagation, building up partial products row by row. use crate::models::specialized::{Assignment, BooleanExpr, Circuit, CircuitSAT, Factoring}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -178,8 +177,8 @@ fn build_multiplier_cell( #[reduction(overhead = { ReductionOverhead::new(vec![ - ("num_variables", poly!(num_bits_first * num_bits_second)), - ("num_assignments", poly!(num_bits_first * num_bits_second)), + ("num_variables", "num_bits_first * num_bits_second"), + ("num_assignments", "num_bits_first * num_bits_second"), ]) })] impl ReduceTo for Factoring { diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index 491c0627..8ed4dcaf 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -19,7 +19,6 @@ use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; use crate::models::specialized::Factoring; -use crate::polynomial::{Monomial, Polynomial}; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -95,31 +94,8 @@ impl ReductionResult for ReductionFactoringToILP { #[reduction(overhead = { ReductionOverhead::new(vec![ - // num_vars = m + n + m*n + num_carries where num_carries = max(m+n, target_bits) - // For feasible instances, target_bits <= m+n, so this is 2(m+n) + m*n - ("num_vars", Polynomial { - terms: vec![ - Monomial::var("num_bits_first").scale(2.0), - Monomial::var("num_bits_second").scale(2.0), - Monomial { - coefficient: 1.0, - variables: vec![("num_bits_first", 1), ("num_bits_second", 1)], - }, - ] - }), - // num_constraints = 3*m*n + num_bit_positions + 1 - // For feasible instances (target_bits <= m+n), this is 3*m*n + (m+n) + 1 - ("num_constraints", Polynomial { - terms: vec![ - Monomial { - coefficient: 3.0, - variables: vec![("num_bits_first", 1), ("num_bits_second", 1)], - }, - Monomial::var("num_bits_first"), - Monomial::var("num_bits_second"), - Monomial::constant(1.0), - ] - }), + ("num_vars", "2 * num_bits_first + 2 * num_bits_second + num_bits_first * num_bits_second"), + ("num_constraints", "3 * num_bits_first * num_bits_second + num_bits_first + num_bits_second + 1"), ]) })] impl ReduceTo for Factoring { diff --git a/src/rules/graph.rs b/src/rules/graph.rs index a1c5c746..9a0af5ef 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -856,7 +856,11 @@ impl ReductionGraph { ) -> NeighborTree { let children = node_children .get(&idx) - .map(|cs| cs.iter().map(|&c| build(c, node_children, nodes, graph)).collect()) + .map(|cs| { + cs.iter() + .map(|&c| build(c, node_children, nodes, graph)) + .collect() + }) .unwrap_or_default(); let node = &nodes[graph[idx]]; NeighborTree { diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index dbbb5c00..2e86b0d2 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -10,7 +10,6 @@ //! Slack variables: ceil(log2(slack_range)) bits per inequality constraint. use crate::models::optimization::{Comparison, ObjectiveSense, ILP, QUBO}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -37,7 +36,7 @@ impl ReductionResult for ReductionILPToQUBO { } #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_vars))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vars")]) } )] impl ReduceTo> for ILP { type Result = ReductionILPToQUBO; diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 2cfb4f98..61f86d73 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -14,7 +14,6 @@ use crate::models::optimization::QUBO; use crate::models::satisfiability::KSatisfiability; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -293,7 +292,7 @@ fn build_qubo_matrix( } #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_vars))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vars")]) } )] impl ReduceTo> for KSatisfiability { type Result = ReductionKSatToQUBO; @@ -311,7 +310,7 @@ impl ReduceTo> for KSatisfiability { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vars) + poly!(num_clauses)), + ("num_vars", "num_vars + num_clauses"), ]) } )] impl ReduceTo> for KSatisfiability { diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 08f5026e..e30862ea 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -8,7 +8,6 @@ use crate::models::graph::MaximumClique; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -45,8 +44,8 @@ impl ReductionResult for ReductionCliqueToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_vertices ^ 2)), + ("num_vars", "num_vertices"), + ("num_constraints", "num_vertices ^ 2"), ]) } )] diff --git a/src/rules/maximumindependentset_gridgraph.rs b/src/rules/maximumindependentset_gridgraph.rs index 8d0ee475..d2ff47ab 100644 --- a/src/rules/maximumindependentset_gridgraph.rs +++ b/src/rules/maximumindependentset_gridgraph.rs @@ -4,7 +4,6 @@ //! Maps an arbitrary graph's MIS problem to an equivalent weighted MIS on a grid graph. use crate::models::graph::MaximumIndependentSet; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -34,8 +33,8 @@ impl ReductionResult for ReductionISSimpleToGrid { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices * num_vertices)), - ("num_edges", poly!(num_vertices * num_vertices)), + ("num_vertices", "num_vertices * num_vertices"), + ("num_edges", "num_vertices * num_vertices"), ]) } )] @@ -81,8 +80,8 @@ impl ReductionResult for ReductionISUnitDiskToGrid { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices * num_vertices)), - ("num_edges", poly!(num_vertices * num_vertices)), + ("num_vertices", "num_vertices * num_vertices"), + ("num_edges", "num_vertices * num_vertices"), ]) } )] diff --git a/src/rules/maximumindependentset_ilp.rs b/src/rules/maximumindependentset_ilp.rs index 220cd7e7..136b59b6 100644 --- a/src/rules/maximumindependentset_ilp.rs +++ b/src/rules/maximumindependentset_ilp.rs @@ -7,7 +7,6 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -44,8 +43,8 @@ impl ReductionResult for ReductionISToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_edges)), + ("num_vars", "num_vertices"), + ("num_constraints", "num_edges"), ]) } )] diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 0cdbbb02..0dc97e52 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -5,7 +5,6 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::set::MaximumSetPacking; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -39,8 +38,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_sets", poly!(num_vertices)), - ("universe_size", poly!(num_vertices)), + ("num_sets", "num_vertices"), + ("universe_size", "num_vertices"), ]) } )] @@ -90,8 +89,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_sets)), - ("num_edges", poly!(num_sets)), + ("num_vertices", "num_sets"), + ("num_edges", "num_sets"), ]) } )] diff --git a/src/rules/maximumindependentset_qubo.rs b/src/rules/maximumindependentset_qubo.rs index ea1e5d08..0c159c83 100644 --- a/src/rules/maximumindependentset_qubo.rs +++ b/src/rules/maximumindependentset_qubo.rs @@ -7,7 +7,6 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::optimization::QUBO; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -32,7 +31,7 @@ impl ReductionResult for ReductionISToQUBO { } #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_vertices))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vertices")]) } )] impl ReduceTo> for MaximumIndependentSet { type Result = ReductionISToQUBO; diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 09d6a85e..091a994c 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -5,7 +5,6 @@ //! triangular lattice grid graph. use crate::models::graph::MaximumIndependentSet; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -36,8 +35,8 @@ impl ReductionResult for ReductionISSimpleToTriangular { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices * num_vertices)), - ("num_edges", poly!(num_vertices * num_vertices)), + ("num_vertices", "num_vertices * num_vertices"), + ("num_edges", "num_vertices * num_vertices"), ]) } )] diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index dc016861..d07069cb 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -8,7 +8,6 @@ use crate::models::graph::MaximumMatching; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -45,8 +44,8 @@ impl ReductionResult for ReductionMatchingToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_edges)), - ("num_constraints", poly!(num_vertices)), + ("num_vars", "num_edges"), + ("num_constraints", "num_vertices"), ]) } )] diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index c477fc66..fa15a9f5 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -5,7 +5,6 @@ use crate::models::graph::MaximumMatching; use crate::models::set::MaximumSetPacking; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,8 +39,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_sets", poly!(num_edges)), - ("universe_size", poly!(num_vertices)), + ("num_sets", "num_edges"), + ("universe_size", "num_vertices"), ]) } )] diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index b5f22d74..b3dfbedb 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -7,7 +7,6 @@ use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; use crate::models::set::MaximumSetPacking; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -43,8 +42,8 @@ impl ReductionResult for ReductionSPToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_sets)), - ("num_constraints", poly!(num_sets ^ 2)), + ("num_vars", "num_sets"), + ("num_constraints", "num_sets ^ 2"), ]) } )] diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 2e5a48c0..1da95d93 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -8,7 +8,6 @@ use crate::models::optimization::QUBO; use crate::models::set::MaximumSetPacking; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -33,7 +32,7 @@ impl ReductionResult for ReductionSPToQUBO { } #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_sets))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_sets")]) } )] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToQUBO; diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index b6c526b8..9c5a1b37 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -8,7 +8,6 @@ use crate::models::graph::MinimumDominatingSet; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -46,8 +45,8 @@ impl ReductionResult for ReductionDSToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_vertices)), + ("num_vars", "num_vertices"), + ("num_constraints", "num_vertices"), ]) } )] diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 1a3889b7..1f9c3b01 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -7,7 +7,6 @@ use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; use crate::models::set::MinimumSetCovering; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -43,8 +42,8 @@ impl ReductionResult for ReductionSCToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_sets)), - ("num_constraints", poly!(universe_size)), + ("num_vars", "num_sets"), + ("num_constraints", "universe_size"), ]) } )] diff --git a/src/rules/minimumvertexcover_ilp.rs b/src/rules/minimumvertexcover_ilp.rs index 18780fd5..f4459971 100644 --- a/src/rules/minimumvertexcover_ilp.rs +++ b/src/rules/minimumvertexcover_ilp.rs @@ -7,7 +7,6 @@ use crate::models::graph::MinimumVertexCover; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -44,8 +43,8 @@ impl ReductionResult for ReductionVCToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_edges)), + ("num_vars", "num_vertices"), + ("num_constraints", "num_edges"), ]) } )] diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index 0715c474..a3d62380 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -3,7 +3,6 @@ //! These problems are complements: a set S is an independent set iff V\S is a vertex cover. use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -37,8 +36,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), + ("num_vertices", "num_vertices"), + ("num_edges", "num_edges"), ]) } )] @@ -80,8 +79,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), + ("num_vertices", "num_vertices"), + ("num_edges", "num_edges"), ]) } )] diff --git a/src/rules/minimumvertexcover_minimumsetcovering.rs b/src/rules/minimumvertexcover_minimumsetcovering.rs index e2f130f1..46ceb105 100644 --- a/src/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/rules/minimumvertexcover_minimumsetcovering.rs @@ -5,7 +5,6 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::MinimumSetCovering; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -39,8 +38,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_sets", poly!(num_vertices)), - ("universe_size", poly!(num_edges)), + ("num_sets", "num_vertices"), + ("universe_size", "num_edges"), ]) } )] diff --git a/src/rules/minimumvertexcover_qubo.rs b/src/rules/minimumvertexcover_qubo.rs index b0422e57..45d1b498 100644 --- a/src/rules/minimumvertexcover_qubo.rs +++ b/src/rules/minimumvertexcover_qubo.rs @@ -8,7 +8,6 @@ use crate::models::graph::MinimumVertexCover; use crate::models::optimization::QUBO; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -34,7 +33,7 @@ impl ReductionResult for ReductionVCToQUBO { } #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", poly!(num_vertices))]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vertices")]) } )] impl ReduceTo> for MinimumVertexCover { type Result = ReductionVCToQUBO; diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 95790e95..d16484db 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -1,5 +1,6 @@ //! Automatic reduction registration via inventory. +use crate::expr::{EvalError, Expr, Func}; use crate::polynomial::Polynomial; use crate::rules::traits::DynReductionResult; use crate::types::ProblemSize; @@ -9,14 +10,24 @@ use std::collections::HashSet; /// Overhead specification for a reduction. #[derive(Clone, Debug, Default, serde::Serialize)] pub struct ReductionOverhead { - /// Output size as polynomials of input size variables. - /// Each entry is (output_field_name, polynomial). - pub output_size: Vec<(&'static str, Polynomial)>, + /// Output size as symbolic expressions of input size variables. + /// Each entry is (output_field_name, expression). + pub output_size: Vec<(&'static str, Expr)>, } impl ReductionOverhead { - pub fn new(output_size: Vec<(&'static str, Polynomial)>) -> Self { - Self { output_size } + pub fn new(specs: Vec<(&'static str, &'static str)>) -> Self { + Self { + output_size: specs + .into_iter() + .map(|(field, expr_str)| { + let expr = Expr::parse(expr_str).unwrap_or_else(|e| { + panic!("invalid overhead expression for '{field}': {e}") + }); + (field, expr) + }) + .collect(), + } } /// Identity overhead: each output field equals the same-named input field. @@ -29,17 +40,24 @@ impl ReductionOverhead { /// Evaluate output size given input size. /// - /// Uses `round()` for the f64 to usize conversion because polynomial coefficients + /// Uses `round()` for the f64 to usize conversion because expression coefficients /// are typically integers (1, 2, 3, 7, 21, etc.) and any fractional results come /// from floating-point arithmetic imprecision, not intentional fractions. - /// For problem sizes, rounding to nearest integer is the most intuitive behavior. - pub fn evaluate_output_size(&self, input: &ProblemSize) -> ProblemSize { - let fields: Vec<_> = self - .output_size - .iter() - .map(|(name, poly)| (*name, poly.evaluate(input).round() as usize)) - .collect(); - ProblemSize::new(fields) + pub fn evaluate_output_size(&self, input: &ProblemSize) -> Result { + let mut fields = Vec::new(); + for (name, expr) in &self.output_size { + let val = expr.evaluate(input)?; + let rounded = val.round(); + if !rounded.is_finite() || rounded < 0.0 || rounded > usize::MAX as f64 { + return Err(EvalError::Domain { + func: Func::Floor, + detail: format!("overhead for '{name}' produced out-of-range value: {val}") + .into(), + }); + } + fields.push((*name, rounded as usize)); + } + Ok(ProblemSize::new(fields)) } /// Collect all input variable names referenced by the overhead polynomials. diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index 8337c5b9..e7000672 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -10,7 +10,6 @@ use crate::models::graph::KColoring; use crate::models::satisfiability::Satisfiability; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::sat_maximumindependentset::BoolVar; @@ -300,9 +299,9 @@ impl ReductionSATToColoring { overhead = { ReductionOverhead::new(vec![ // 2*num_vars + 3 (base) + 5*(num_literals - num_clauses) (OR gadgets) - ("num_vertices", poly!(2 * num_vars) + poly!(5 * num_literals) + poly!(num_clauses).scale(-5.0) + poly!(3)), + ("num_vertices", "2 * num_vars + 5 * num_literals - 5 * num_clauses + 3"), // 3 (triangle) + 3*num_vars + 11*(num_literals - num_clauses) (OR gadgets) + 2*num_clauses (set_true) - ("num_edges", poly!(3 * num_vars) + poly!(11 * num_literals) + poly!(num_clauses).scale(-9.0) + poly!(3)), + ("num_edges", "3 * num_vars + 11 * num_literals - 9 * num_clauses + 3"), ]) } )] diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 5321fde6..a75045f5 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -7,7 +7,6 @@ //! K-SAT -> SAT: Trivial embedding (K-SAT is a special case of SAT) use crate::models::satisfiability::{CNFClause, KSatisfiability, Satisfiability}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -113,11 +112,11 @@ fn add_clause_to_ksat( macro_rules! impl_sat_to_ksat { ($ktype:ty, $k:expr) => { #[reduction(overhead = { - ReductionOverhead::new(vec![ - ("num_clauses", poly!(num_clauses) + poly!(num_literals)), - ("num_vars", poly!(num_vars) + poly!(num_literals)), - ]) - })] + ReductionOverhead::new(vec![ + ("num_clauses", "num_clauses + num_literals"), + ("num_vars", "num_vars + num_literals"), + ]) + })] impl ReduceTo> for Satisfiability { type Result = ReductionSATToKSAT<$ktype>; @@ -188,9 +187,9 @@ macro_rules! impl_ksat_to_sat { ($ktype:ty) => { #[reduction(overhead = { ReductionOverhead::new(vec![ - ("num_clauses", poly!(num_clauses)), - ("num_vars", poly!(num_vars)), - ("num_literals", poly!(num_literals)), + ("num_clauses", "num_clauses"), + ("num_vars", "num_vars"), + ("num_literals", "num_literals"), ]) })] impl ReduceTo for KSatisfiability<$ktype> { diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index f678bf79..8ec7c83d 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -10,7 +10,6 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::satisfiability::Satisfiability; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -112,8 +111,8 @@ impl ReductionSATToIS { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_literals)), - ("num_edges", poly!(num_literals ^ 2)), + ("num_vertices", "num_literals"), + ("num_edges", "num_literals ^ 2"), ]) } )] diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index 07646902..aa693c63 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -16,7 +16,6 @@ use crate::models::graph::MinimumDominatingSet; use crate::models::satisfiability::Satisfiability; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::sat_maximumindependentset::BoolVar; @@ -116,8 +115,8 @@ impl ReductionSATToDS { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(3 * num_vars) + poly!(num_clauses)), - ("num_edges", poly!(3 * num_vars) + poly!(num_literals)), + ("num_vertices", "3 * num_vars + num_clauses"), + ("num_edges", "3 * num_vars + num_literals"), ]) } )] diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index ef6ae91b..748fd595 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -5,7 +5,6 @@ use crate::models::graph::MaxCut; use crate::models::optimization::SpinGlass; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -46,8 +45,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_spins", poly!(num_vertices)), - ("num_interactions", poly!(num_edges)), + ("num_spins", "num_vertices"), + ("num_interactions", "num_edges"), ]) } )] @@ -137,8 +136,8 @@ where #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_spins)), - ("num_edges", poly!(num_interactions)), + ("num_vertices", "num_spins"), + ("num_edges", "num_interactions"), ]) } )] diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 2d39981f..5c408401 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -6,7 +6,6 @@ //! Transformation: s = 2x - 1 (so x=0 -> s=-1, x=1 -> s=+1) use crate::models::optimization::{SpinGlass, QUBO}; -use crate::poly; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -35,7 +34,7 @@ impl ReductionResult for ReductionQUBOToSG { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_spins", poly!(num_vars)), + ("num_spins", "num_vars"), ]) } )] @@ -112,7 +111,7 @@ impl ReductionResult for ReductionSGToQUBO { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_spins)), + ("num_vars", "num_spins"), ]) } )] diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index af2b37e2..bb12dc96 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -7,7 +7,6 @@ use crate::models::graph::TravelingSalesman; use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; -use crate::polynomial::{Monomial, Polynomial}; use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -75,30 +74,8 @@ impl ReductionResult for ReductionTSPToILP { #[reduction( overhead = { ReductionOverhead::new(vec![ - // num_vars = n^2 + 2*m*n - ("num_vars", Polynomial::var_pow("num_vertices", 2) + Polynomial { - terms: vec![Monomial { - coefficient: 2.0, - variables: vec![("num_vertices", 1), ("num_edges", 1)], - }] - }), - // num_constraints = 2n + n(n(n-1) - 2m) + 6mn = n^3 - n^2 + 2n + 4mn - ("num_constraints", Polynomial::var_pow("num_vertices", 3) + Polynomial { - terms: vec![ - Monomial { - coefficient: -1.0, - variables: vec![("num_vertices", 2)], - }, - Monomial { - coefficient: 2.0, - variables: vec![("num_vertices", 1)], - }, - Monomial { - coefficient: 4.0, - variables: vec![("num_vertices", 1), ("num_edges", 1)], - }, - ] - }), + ("num_vars", "num_vertices ^ 2 + 2 * num_vertices * num_edges"), + ("num_constraints", "num_vertices ^ 3 - num_vertices ^ 2 + 2 * num_vertices + 4 * num_vertices * num_edges"), ]) } )] diff --git a/src/unit_tests/export.rs b/src/unit_tests/export.rs index 25c43c63..23cb4aab 100644 --- a/src/unit_tests/export.rs +++ b/src/unit_tests/export.rs @@ -1,5 +1,4 @@ use super::*; -use crate::polynomial::Polynomial; use crate::rules::registry::ReductionOverhead; #[test] @@ -11,61 +10,33 @@ fn test_overhead_to_json_empty() { #[test] fn test_overhead_to_json_single_field() { - let overhead = ReductionOverhead::new(vec![( - "num_vertices", - Polynomial::var("n") + Polynomial::var("m"), - )]); + let overhead = ReductionOverhead::new(vec![("num_vertices", "n + m")]); let entries = overhead_to_json(&overhead); assert_eq!(entries.len(), 1); assert_eq!(entries[0].field, "num_vertices"); - assert_eq!(entries[0].polynomial.len(), 2); - - // Check first monomial: 1*n - assert_eq!(entries[0].polynomial[0].coefficient, 1.0); - assert_eq!( - entries[0].polynomial[0].variables, - vec![("n".to_string(), 1)] - ); - - // Check second monomial: 1*m - assert_eq!(entries[0].polynomial[1].coefficient, 1.0); - assert_eq!( - entries[0].polynomial[1].variables, - vec![("m".to_string(), 1)] - ); + assert_eq!(entries[0].expression, "n + m"); } #[test] -fn test_overhead_to_json_constant_monomial() { - let overhead = ReductionOverhead::new(vec![("num_vars", Polynomial::constant(42.0))]); +fn test_overhead_to_json_constant() { + let overhead = ReductionOverhead::new(vec![("num_vars", "42")]); let entries = overhead_to_json(&overhead); assert_eq!(entries.len(), 1); assert_eq!(entries[0].field, "num_vars"); - assert_eq!(entries[0].polynomial.len(), 1); - assert_eq!(entries[0].polynomial[0].coefficient, 42.0); - assert!(entries[0].polynomial[0].variables.is_empty()); + assert_eq!(entries[0].expression, "42"); } #[test] fn test_overhead_to_json_scaled_power() { - let overhead = - ReductionOverhead::new(vec![("num_edges", Polynomial::var_pow("n", 2).scale(3.0))]); + let overhead = ReductionOverhead::new(vec![("num_edges", "3 * n ^ 2")]); let entries = overhead_to_json(&overhead); assert_eq!(entries.len(), 1); - assert_eq!(entries[0].polynomial.len(), 1); - assert_eq!(entries[0].polynomial[0].coefficient, 3.0); - assert_eq!( - entries[0].polynomial[0].variables, - vec![("n".to_string(), 2)] - ); + assert_eq!(entries[0].expression, "3 * n ^ 2"); } #[test] fn test_overhead_to_json_multiple_fields() { - let overhead = ReductionOverhead::new(vec![ - ("num_vertices", Polynomial::var("n")), - ("num_edges", Polynomial::var_pow("n", 2)), - ]); + let overhead = ReductionOverhead::new(vec![("num_vertices", "n"), ("num_edges", "n ^ 2")]); let entries = overhead_to_json(&overhead); assert_eq!(entries.len(), 2); assert_eq!(entries[0].field, "num_vertices"); @@ -141,7 +112,6 @@ fn test_write_example_creates_files() { write_example("_test_export", &data, &results); - // Verify files exist and contain valid JSON let reduction_path = "docs/paper/examples/_test_export.json"; let results_path = "docs/paper/examples/_test_export.result.json"; @@ -157,7 +127,6 @@ fn test_write_example_creates_files() { serde_json::json!([1, 0, 1]) ); - // Clean up test files let _ = fs::remove_file(reduction_path); let _ = fs::remove_file(results_path); } @@ -190,15 +159,12 @@ fn test_reduction_data_serialization() { }, overhead: vec![OverheadEntry { field: "num_vertices".to_string(), - polynomial: vec![MonomialJson { - coefficient: 1.0, - variables: vec![("n".to_string(), 1)], - }], + expression: "n".to_string(), }], }; let json = serde_json::to_value(&data).unwrap(); assert_eq!(json["overhead"][0]["field"], "num_vertices"); - assert_eq!(json["overhead"][0]["polynomial"][0]["coefficient"], 1.0); + assert_eq!(json["overhead"][0]["expression"], "n"); } #[test] diff --git a/src/unit_tests/rules/cost.rs b/src/unit_tests/rules/cost.rs index 1be5f44b..a6e915b5 100644 --- a/src/unit_tests/rules/cost.rs +++ b/src/unit_tests/rules/cost.rs @@ -1,11 +1,7 @@ use super::*; -use crate::polynomial::Polynomial; fn test_overhead() -> ReductionOverhead { - ReductionOverhead::new(vec![ - ("n", Polynomial::var("n").scale(2.0)), - ("m", Polynomial::var("m")), - ]) + ReductionOverhead::new(vec![("n", "2 * n"), ("m", "m")]) } #[test] @@ -29,7 +25,9 @@ fn test_minimize_steps() { #[test] fn test_custom_cost() { let cost_fn = CustomCost(|overhead: &ReductionOverhead, size: &ProblemSize| { - let output = overhead.evaluate_output_size(size); + let output = overhead + .evaluate_output_size(size) + .expect("overhead evaluation failed"); (output.get("n").unwrap_or(0) + output.get("m").unwrap_or(0)) as f64 }); let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index 6a33f912..a2685abf 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -1,5 +1,4 @@ use super::*; -use crate::poly; /// Dummy reduce_fn for unit tests that don't exercise runtime reduction. fn dummy_reduce_fn(_: &dyn std::any::Any) -> Box { @@ -8,10 +7,10 @@ fn dummy_reduce_fn(_: &dyn std::any::Any) -> Box Date: Sun, 15 Feb 2026 01:42:43 +0800 Subject: [PATCH 5/8] refactor: remove Polynomial/Monomial/poly! (replaced by Expr) --- src/export.rs | 2 +- src/expr.rs | 57 ++++++ src/lib.rs | 2 +- src/polynomial.rs | 340 ----------------------------------- src/rules/graph.rs | 2 +- src/rules/registry.rs | 30 ++-- src/unit_tests/polynomial.rs | 180 ------------------- 7 files changed, 78 insertions(+), 535 deletions(-) delete mode 100644 src/polynomial.rs delete mode 100644 src/unit_tests/polynomial.rs diff --git a/src/export.rs b/src/export.rs index b6920134..3a8237a9 100644 --- a/src/export.rs +++ b/src/export.rs @@ -5,7 +5,7 @@ //! - `.json` — reduction structure (source, target, overhead) //! - `.result.json` — runtime solutions //! -//! The schema mirrors the internal types: `ReductionOverhead` for polynomials, +//! The schema mirrors the internal types: `ReductionOverhead` for overhead expressions, //! `Problem::variant()` for problem variants, and `Problem::NAME` for problem names. use crate::rules::registry::ReductionOverhead; diff --git a/src/expr.rs b/src/expr.rs index 2610d212..4f3acdc9 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -4,6 +4,7 @@ //! Supports arithmetic, exponentiation, and built-in math functions. use crate::types::ProblemSize; +use std::collections::HashSet; use std::fmt; /// A symbolic expression over named variables. @@ -96,6 +97,62 @@ impl Expr { } } +// ── Analysis & Transformation ── + +impl Expr { + /// Collect all variable names referenced in this expression. + pub fn variable_names(&self) -> HashSet<&str> { + let mut names = HashSet::new(); + self.collect_variable_names(&mut names); + names + } + + fn collect_variable_names<'a>(&'a self, names: &mut HashSet<&'a str>) { + match self { + Expr::Num(_) => {} + Expr::Var(name) => { + names.insert(name); + } + Expr::Neg(inner) => inner.collect_variable_names(names), + Expr::BinOp { lhs, rhs, .. } => { + lhs.collect_variable_names(names); + rhs.collect_variable_names(names); + } + Expr::Call { args, .. } => { + for arg in args { + arg.collect_variable_names(names); + } + } + } + } + + /// Substitute variables in this expression using a mapping. + /// Variables found in the mapping are replaced by clones of the mapped expression; + /// variables not in the mapping are left unchanged. + pub fn substitute(&self, mapping: &std::collections::HashMap<&str, &Expr>) -> Expr { + match self { + Expr::Num(v) => Expr::Num(*v), + Expr::Var(name) => { + if let Some(replacement) = mapping.get(name.as_ref()) { + (*replacement).clone() + } else { + Expr::Var(name.clone()) + } + } + Expr::Neg(inner) => Expr::Neg(Box::new(inner.substitute(mapping))), + Expr::BinOp { op, lhs, rhs } => Expr::BinOp { + op: *op, + lhs: Box::new(lhs.substitute(mapping)), + rhs: Box::new(rhs.substitute(mapping)), + }, + Expr::Call { func, args } => Expr::Call { + func: *func, + args: args.iter().map(|a| a.substitute(mapping)).collect(), + }, + } + } +} + // ── Evaluator ── impl Expr { diff --git a/src/lib.rs b/src/lib.rs index 86d070aa..75079f4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub mod expr; pub mod graph_types; pub mod io; pub mod models; -pub(crate) mod polynomial; + pub mod registry; pub mod rules; pub mod solvers; diff --git a/src/polynomial.rs b/src/polynomial.rs deleted file mode 100644 index f17e3087..00000000 --- a/src/polynomial.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! Polynomial representation for reduction overhead. - -use crate::types::ProblemSize; -use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::ops::Add; - -/// A monomial: coefficient × Π(variable^exponent) -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -pub struct Monomial { - pub coefficient: f64, - pub variables: Vec<(&'static str, u8)>, -} - -impl Monomial { - pub fn constant(c: f64) -> Self { - Self { - coefficient: c, - variables: vec![], - } - } - - pub fn var(name: &'static str) -> Self { - Self { - coefficient: 1.0, - variables: vec![(name, 1)], - } - } - - pub fn var_pow(name: &'static str, exp: u8) -> Self { - Self { - coefficient: 1.0, - variables: vec![(name, exp)], - } - } - - pub fn scale(mut self, c: f64) -> Self { - self.coefficient *= c; - self - } - - pub fn evaluate(&self, size: &ProblemSize) -> f64 { - let var_product: f64 = self - .variables - .iter() - .map(|(name, exp)| { - let val = size.get(name).unwrap_or(0) as f64; - val.powi(*exp as i32) - }) - .product(); - self.coefficient * var_product - } - - /// Multiply two monomials. - pub fn mul(&self, other: &Monomial) -> Monomial { - let mut variables = self.variables.clone(); - variables.extend_from_slice(&other.variables); - Monomial { - coefficient: self.coefficient * other.coefficient, - variables, - } - } - - /// Normalize: sort variables by name, merge duplicate entries. - pub fn normalize(&mut self) { - self.variables.sort_by_key(|(name, _)| *name); - let mut merged: Vec<(&'static str, u8)> = Vec::new(); - for &(name, exp) in &self.variables { - if let Some(last) = merged.last_mut() { - if last.0 == name { - last.1 += exp; - continue; - } - } - merged.push((name, exp)); - } - // Remove zero-exponent variables - merged.retain(|&(_, exp)| exp > 0); - self.variables = merged; - } - - /// Variable signature for like-term comparison (after normalization). - fn var_signature(&self) -> &[(&'static str, u8)] { - &self.variables - } -} - -/// A polynomial: Σ monomials -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -pub struct Polynomial { - pub terms: Vec, -} - -impl Polynomial { - pub fn zero() -> Self { - Self { terms: vec![] } - } - - pub fn constant(c: f64) -> Self { - Self { - terms: vec![Monomial::constant(c)], - } - } - - pub fn var(name: &'static str) -> Self { - Self { - terms: vec![Monomial::var(name)], - } - } - - pub fn var_pow(name: &'static str, exp: u8) -> Self { - Self { - terms: vec![Monomial::var_pow(name, exp)], - } - } - - /// Create a polynomial with a single monomial that is a product of two variables. - pub fn var_product(a: &'static str, b: &'static str) -> Self { - Self { - terms: vec![Monomial { - coefficient: 1.0, - variables: vec![(a, 1), (b, 1)], - }], - } - } - - pub fn scale(mut self, c: f64) -> Self { - for term in &mut self.terms { - term.coefficient *= c; - } - self - } - - pub fn evaluate(&self, size: &ProblemSize) -> f64 { - self.terms.iter().map(|m| m.evaluate(size)).sum() - } - - /// Collect all variable names referenced by this polynomial. - pub fn variable_names(&self) -> HashSet<&'static str> { - self.terms - .iter() - .flat_map(|m| m.variables.iter().map(|(name, _)| *name)) - .collect() - } - - /// Multiply two polynomials. - pub fn mul(&self, other: &Polynomial) -> Polynomial { - let mut terms = Vec::new(); - for a in &self.terms { - for b in &other.terms { - terms.push(a.mul(b)); - } - } - let mut result = Polynomial { terms }; - result.normalize(); - result - } - - /// Raise to a non-negative integer power. - pub fn pow(&self, n: u8) -> Polynomial { - match n { - 0 => Polynomial::constant(1.0), - 1 => self.clone(), - _ => { - let mut result = self.clone(); - for _ in 1..n { - result = result.mul(self); - } - result - } - } - } - - /// Substitute variables with polynomials. - /// - /// Each variable in the polynomial is replaced by the corresponding - /// polynomial from the mapping. Variables not in the mapping are left as-is. - pub fn substitute(&self, mapping: &HashMap<&str, &Polynomial>) -> Polynomial { - let mut result = Polynomial::zero(); - for mono in &self.terms { - // Start with the coefficient - let mut term_poly = Polynomial::constant(mono.coefficient); - // Multiply by each variable's substitution raised to its exponent - for &(name, exp) in &mono.variables { - let var_poly = if let Some(&replacement) = mapping.get(name) { - replacement.pow(exp) - } else { - Polynomial::var_pow(name, exp) - }; - term_poly = term_poly.mul(&var_poly); - } - result = result + term_poly; - } - result.normalize(); - result - } - - /// Normalize: normalize all monomials, then combine like terms. - pub fn normalize(&mut self) { - for term in &mut self.terms { - term.normalize(); - } - // Combine like terms - let mut combined: Vec = Vec::new(); - for term in &self.terms { - if let Some(existing) = combined - .iter_mut() - .find(|m| m.var_signature() == term.var_signature()) - { - existing.coefficient += term.coefficient; - } else { - combined.push(term.clone()); - } - } - // Remove zero-coefficient terms - combined.retain(|m| m.coefficient.abs() > 1e-15); - self.terms = combined; - } - - /// Return a normalized copy. - pub fn normalized(&self) -> Polynomial { - let mut p = self.clone(); - p.normalize(); - p - } -} - -impl fmt::Display for Monomial { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let coeff_i = self.coefficient.round() as i64; - let is_int = (self.coefficient - coeff_i as f64).abs() < 1e-10; - if self.variables.is_empty() { - if is_int { - write!(f, "{coeff_i}") - } else { - write!(f, "{}", self.coefficient) - } - } else { - let has_coeff = if is_int { - match coeff_i { - 1 => false, - -1 => { - write!(f, "-")?; - false - } - _ => { - write!(f, "{coeff_i}")?; - true - } - } - } else { - write!(f, "{}", self.coefficient)?; - true - }; - for (i, (name, exp)) in self.variables.iter().enumerate() { - if has_coeff || i > 0 { - write!(f, " * ")?; - } - write!(f, "{name}")?; - if *exp > 1 { - write!(f, "^{exp}")?; - } - } - Ok(()) - } - } -} - -impl fmt::Display for Polynomial { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.terms.is_empty() { - write!(f, "0") - } else { - for (i, term) in self.terms.iter().enumerate() { - if i > 0 { - if term.coefficient < 0.0 { - write!(f, " - ")?; - let negated = Monomial { - coefficient: -term.coefficient, - variables: term.variables.clone(), - }; - write!(f, "{negated}")?; - } else { - write!(f, " + ")?; - write!(f, "{term}")?; - } - } else { - write!(f, "{term}")?; - } - } - Ok(()) - } - } -} - -impl Add for Polynomial { - type Output = Self; - - fn add(mut self, other: Self) -> Self { - self.terms.extend(other.terms); - self - } -} - -/// Convenience macro for building polynomials. -#[macro_export] -macro_rules! poly { - // Single variable: poly!(n) - ($name:ident) => { - $crate::polynomial::Polynomial::var(stringify!($name)) - }; - // Variable with exponent: poly!(n^2) - ($name:ident ^ $exp:literal) => { - $crate::polynomial::Polynomial::var_pow(stringify!($name), $exp) - }; - // Constant: poly!(5) - ($c:literal) => { - $crate::polynomial::Polynomial::constant($c as f64) - }; - // Scaled variable: poly!(3 * n) - ($c:literal * $name:ident) => { - $crate::polynomial::Polynomial::var(stringify!($name)).scale($c as f64) - }; - // Scaled variable with exponent: poly!(9 * n^2) - ($c:literal * $name:ident ^ $exp:literal) => { - $crate::polynomial::Polynomial::var_pow(stringify!($name), $exp).scale($c as f64) - }; - // Product of two variables: poly!(a * b) - ($a:ident * $b:ident) => { - $crate::polynomial::Polynomial::var_product(stringify!($a), stringify!($b)) - }; - // Scaled product of two variables: poly!(3 * a * b) - ($c:literal * $a:ident * $b:ident) => { - $crate::polynomial::Polynomial::var_product(stringify!($a), stringify!($b)).scale($c as f64) - }; -} - -#[cfg(test)] -#[path = "unit_tests/polynomial.rs"] -mod tests; diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 9a0af5ef..60597778 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -97,7 +97,7 @@ pub(crate) struct EdgeJson { pub(crate) source: usize, /// Index into the `nodes` array for the target problem variant. pub(crate) target: usize, - /// Reduction overhead: output size as polynomials of input size. + /// Reduction overhead: output size as expressions of input size. pub(crate) overhead: Vec, /// Relative rustdoc path for the reduction module. pub(crate) doc_path: String, diff --git a/src/rules/registry.rs b/src/rules/registry.rs index d16484db..12f064e4 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -1,7 +1,6 @@ //! Automatic reduction registration via inventory. use crate::expr::{EvalError, Expr, Func}; -use crate::polynomial::Polynomial; use crate::rules::traits::DynReductionResult; use crate::types::ProblemSize; use std::any::Any; @@ -34,7 +33,14 @@ impl ReductionOverhead { /// Used by variant cast reductions where problem size doesn't change. pub fn identity(fields: &[&'static str]) -> Self { Self { - output_size: fields.iter().map(|&f| (f, Polynomial::var(f))).collect(), + output_size: fields + .iter() + .map(|&f| { + let expr = Expr::parse(f) + .unwrap_or_else(|e| panic!("invalid identity field name '{f}': {e}")); + (f, expr) + }) + .collect(), } } @@ -60,32 +66,32 @@ impl ReductionOverhead { Ok(ProblemSize::new(fields)) } - /// Collect all input variable names referenced by the overhead polynomials. + /// Collect all input variable names referenced by the overhead expressions. pub fn input_variable_names(&self) -> HashSet<&'static str> { self.output_size .iter() - .flat_map(|(_, poly)| poly.variable_names()) + .flat_map(|(_, expr)| expr.variable_names()) .collect() } /// Compose two overheads: substitute self's output into `next`'s input. /// - /// Returns a new overhead whose polynomials map from self's input variables + /// Returns a new overhead whose expressions map from self's input variables /// directly to `next`'s output variables. pub fn compose(&self, next: &ReductionOverhead) -> ReductionOverhead { use std::collections::HashMap; - // Build substitution map: output field name → output polynomial - let mapping: HashMap<&str, &Polynomial> = self + // Build substitution map: output field name → output expression + let mapping: HashMap<&str, &Expr> = self .output_size .iter() - .map(|(name, poly)| (*name, poly)) + .map(|(name, expr)| (*name, expr)) .collect(); let composed = next .output_size .iter() - .map(|(name, poly)| (*name, poly.substitute(&mapping))) + .map(|(name, expr)| (*name, expr.substitute(&mapping))) .collect(); ReductionOverhead { @@ -93,12 +99,12 @@ impl ReductionOverhead { } } - /// Get the polynomial for a named output field. - pub fn get(&self, name: &str) -> Option<&Polynomial> { + /// Get the expression for a named output field. + pub fn get(&self, name: &str) -> Option<&Expr> { self.output_size .iter() .find(|(n, _)| *n == name) - .map(|(_, p)| p) + .map(|(_, e)| e) } } diff --git a/src/unit_tests/polynomial.rs b/src/unit_tests/polynomial.rs deleted file mode 100644 index 04dfd7f5..00000000 --- a/src/unit_tests/polynomial.rs +++ /dev/null @@ -1,180 +0,0 @@ -use super::*; - -#[test] -fn test_monomial_constant() { - let m = Monomial::constant(5.0); - let size = ProblemSize::new(vec![("n", 10)]); - assert_eq!(m.evaluate(&size), 5.0); -} - -#[test] -fn test_monomial_variable() { - let m = Monomial::var("n"); - let size = ProblemSize::new(vec![("n", 10)]); - assert_eq!(m.evaluate(&size), 10.0); -} - -#[test] -fn test_monomial_var_pow() { - let m = Monomial::var_pow("n", 2); - let size = ProblemSize::new(vec![("n", 5)]); - assert_eq!(m.evaluate(&size), 25.0); -} - -#[test] -fn test_polynomial_add() { - // 3n + 2m - let p = Polynomial::var("n").scale(3.0) + Polynomial::var("m").scale(2.0); - - let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); - assert_eq!(p.evaluate(&size), 40.0); // 3*10 + 2*5 -} - -#[test] -fn test_polynomial_complex() { - // n^2 + 3m - let p = Polynomial::var_pow("n", 2) + Polynomial::var("m").scale(3.0); - - let size = ProblemSize::new(vec![("n", 4), ("m", 2)]); - assert_eq!(p.evaluate(&size), 22.0); // 16 + 6 -} - -#[test] -fn test_poly_macro() { - let size = ProblemSize::new(vec![("n", 5), ("m", 3)]); - - assert_eq!(poly!(n).evaluate(&size), 5.0); - assert_eq!(poly!(n ^ 2).evaluate(&size), 25.0); - assert_eq!(poly!(3 * n).evaluate(&size), 15.0); - assert_eq!(poly!(2 * m ^ 2).evaluate(&size), 18.0); -} - -#[test] -fn test_missing_variable() { - let p = Polynomial::var("missing"); - let size = ProblemSize::new(vec![("n", 10)]); - assert_eq!(p.evaluate(&size), 0.0); // missing var = 0 -} - -#[test] -fn test_polynomial_zero() { - let p = Polynomial::zero(); - let size = ProblemSize::new(vec![("n", 100)]); - assert_eq!(p.evaluate(&size), 0.0); -} - -#[test] -fn test_polynomial_constant() { - let p = Polynomial::constant(42.0); - let size = ProblemSize::new(vec![("n", 100)]); - assert_eq!(p.evaluate(&size), 42.0); -} - -#[test] -fn test_monomial_scale() { - let m = Monomial::var("n").scale(3.0); - let size = ProblemSize::new(vec![("n", 10)]); - assert_eq!(m.evaluate(&size), 30.0); -} - -#[test] -fn test_polynomial_scale() { - let p = Polynomial::var("n").scale(5.0); - let size = ProblemSize::new(vec![("n", 10)]); - assert_eq!(p.evaluate(&size), 50.0); -} - -#[test] -fn test_monomial_multi_variable() { - // n * m^2 - let m = Monomial { - coefficient: 1.0, - variables: vec![("n", 1), ("m", 2)], - }; - let size = ProblemSize::new(vec![("n", 2), ("m", 3)]); - assert_eq!(m.evaluate(&size), 18.0); // 2 * 9 -} - -#[test] -fn test_display_monomial_constant_int() { - assert_eq!(format!("{}", Monomial::constant(5.0)), "5"); -} - -#[test] -fn test_display_monomial_constant_float() { - assert_eq!(format!("{}", Monomial::constant(3.5)), "3.5"); -} - -#[test] -fn test_display_monomial_single_var() { - assert_eq!(format!("{}", Monomial::var("n")), "n"); -} - -#[test] -fn test_display_monomial_neg_one_coeff() { - assert_eq!(format!("{}", Monomial::var("n").scale(-1.0)), "-n"); -} - -#[test] -fn test_display_monomial_scaled_var() { - assert_eq!(format!("{}", Monomial::var("n").scale(3.0)), "3 * n"); -} - -#[test] -fn test_display_monomial_var_pow() { - assert_eq!(format!("{}", Monomial::var_pow("n", 2)), "n^2"); -} - -#[test] -fn test_display_monomial_multi_var() { - let m = Monomial { - coefficient: 2.0, - variables: vec![("n", 1), ("m", 2)], - }; - assert_eq!(format!("{m}"), "2 * n * m^2"); -} - -#[test] -fn test_display_monomial_float_coeff_var() { - let m = Monomial { - coefficient: 1.5, - variables: vec![("n", 1)], - }; - assert_eq!(format!("{m}"), "1.5 * n"); -} - -#[test] -fn test_display_polynomial_zero() { - assert_eq!(format!("{}", Polynomial::zero()), "0"); -} - -#[test] -fn test_display_polynomial_single_term() { - assert_eq!(format!("{}", Polynomial::var("n").scale(3.0)), "3 * n"); -} - -#[test] -fn test_display_polynomial_addition() { - let p = Polynomial::var("n").scale(3.0) + Polynomial::var("m").scale(2.0); - assert_eq!(format!("{p}"), "3 * n + 2 * m"); -} - -#[test] -fn test_display_polynomial_subtraction() { - let p = Polynomial::var("n").scale(3.0) + Polynomial::var("m").scale(-2.0); - assert_eq!(format!("{p}"), "3 * n - 2 * m"); -} - -#[test] -fn test_poly_macro_product() { - let size = ProblemSize::new(vec![("a", 3), ("b", 4)]); - assert_eq!(poly!(a * b).evaluate(&size), 12.0); - assert_eq!(format!("{}", poly!(a * b)), "a * b"); -} - -#[test] -fn test_poly_macro_scaled_product() { - let size = ProblemSize::new(vec![("a", 3), ("b", 4)]); - assert_eq!(poly!(5 * a * b).evaluate(&size), 60.0); - assert_eq!(format!("{}", poly!(5 * a * b)), "5 * a * b"); -} From f2c6d1c09e17ef7ee42fed388720f22220e015c4 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 15 Feb 2026 01:47:44 +0800 Subject: [PATCH 6/8] fix: unknown variables default to 0 (matching old Polynomial behavior) --- src/expr.rs | 5 +---- src/unit_tests/expr.rs | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/expr.rs b/src/expr.rs index 4f3acdc9..84e3833f 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -160,10 +160,7 @@ impl Expr { pub fn evaluate(&self, size: &ProblemSize) -> Result { match self { Expr::Num(v) => Ok(*v), - Expr::Var(name) => size - .get(name) - .map(|v| v as f64) - .ok_or_else(|| EvalError::UnknownVar(name.clone())), + Expr::Var(name) => Ok(size.get(name).unwrap_or(0) as f64), Expr::Neg(inner) => Ok(-inner.evaluate(size)?), Expr::BinOp { op, lhs, rhs } => { let l = lhs.evaluate(size)?; diff --git a/src/unit_tests/expr.rs b/src/unit_tests/expr.rs index a0abf46a..dbe67271 100644 --- a/src/unit_tests/expr.rs +++ b/src/unit_tests/expr.rs @@ -18,13 +18,10 @@ fn test_eval_var() { } #[test] -fn test_eval_unknown_var() { +fn test_eval_unknown_var_defaults_to_zero() { let expr = Expr::Var("missing".into()); let size = ProblemSize::new(vec![]); - assert!(matches!( - expr.evaluate(&size), - Err(EvalError::UnknownVar(_)) - )); + assert_eq!(expr.evaluate(&size).unwrap(), 0.0); } #[test] From cea3c9b671a1c241693d4744b3a871429ea3e156 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 16 Feb 2026 01:33:14 +0800 Subject: [PATCH 7/8] feat: ReductionPath carries per-edge overheads for staged evaluation --- src/rules/graph.rs | 20 +++++++++++++++++++- src/unit_tests/rules/graph.rs | 6 +++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 60597778..3d44ce42 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -108,6 +108,8 @@ pub(crate) struct EdgeJson { pub struct ReductionPath { /// Variant-level steps in the path. pub steps: Vec, + /// Overhead for each edge in the path (length = steps.len() - 1). + pub overheads: Vec, } impl ReductionPath { @@ -145,6 +147,15 @@ impl ReductionPath { } names } + + /// Evaluate the end-to-end overhead by chaining each step's overhead. + pub fn evaluate(&self, input: &ProblemSize) -> Result { + let mut current = input.clone(); + for overhead in &self.overheads { + current = overhead.evaluate_output_size(¤t)?; + } + Ok(current) + } } impl std::fmt::Display for ReductionPath { @@ -528,7 +539,14 @@ impl ReductionGraph { } }) .collect(); - ReductionPath { steps } + let overheads = node_path + .windows(2) + .map(|w| { + let edge_idx = self.graph.find_edge(w[0], w[1]).unwrap(); + self.graph[edge_idx].overhead.clone() + }) + .collect(); + ReductionPath { steps, overheads } } /// Find all simple paths between two specific problem variants. diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 41c1bf98..9134fc47 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -400,7 +400,10 @@ fn test_all_categories_present() { #[test] fn test_empty_path_source_target() { - let path = ReductionPath { steps: vec![] }; + let path = ReductionPath { + steps: vec![], + overheads: vec![], + }; assert!(path.is_empty()); assert_eq!(path.len(), 0); assert!(path.source().is_none()); @@ -415,6 +418,7 @@ fn test_single_node_path() { name: "MaximumIndependentSet".to_string(), variant: BTreeMap::new(), }], + overheads: vec![], }; assert!(!path.is_empty()); assert_eq!(path.len(), 0); // No reductions, just one type From b6681ba667403e51b1e3a67700d48f290cdff953 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sat, 21 Feb 2026 13:33:24 +0800 Subject: [PATCH 8/8] update --- .gitignore | 3 +- mydocs/DEVELOPER_API.md | 2483 ------------------ mydocs/RUST_FEATURES.md | 1266 --------- problemreductions-cli/src/commands/reduce.rs | 5 +- problemreductions-cli/src/commands/solve.rs | 11 +- src/lib.rs | 1 - src/rules/circuit_ilp.rs | 6 +- src/rules/coloring_ilp.rs | 4 +- src/rules/coloring_qubo.rs | 2 +- src/rules/graph.rs | 5 +- src/rules/qubo_ilp.rs | 6 +- src/rules/registry.rs | 2 +- src/rules/sat_circuitsat.rs | 6 +- src/unit_tests/reduction_graph.rs | 48 +- 14 files changed, 49 insertions(+), 3799 deletions(-) delete mode 100644 mydocs/DEVELOPER_API.md delete mode 100644 mydocs/RUST_FEATURES.md diff --git a/.gitignore b/.gitignore index 56a172d9..ace80227 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,5 @@ docs/paper/examples/ # Claude Code logs claude-output.log .worktrees/ -.worktree/ \ No newline at end of file +.worktree/ +mydocs/ diff --git a/mydocs/DEVELOPER_API.md b/mydocs/DEVELOPER_API.md deleted file mode 100644 index aac17295..00000000 --- a/mydocs/DEVELOPER_API.md +++ /dev/null @@ -1,2483 +0,0 @@ -# Developer API Reference - -This document provides comprehensive API documentation for developers extending the problem-reductions library. - -## Table of Contents - -**Fundamentals** -0. [Conceptual Overview](#0-conceptual-overview) -0a. [Design Philosophy](#0a-design-philosophy) -0b. [How Everything Fits Together](#0b-how-everything-fits-together) - -**API Documentation** -1. [Core Traits](#1-core-traits) -2. [Problem Parametrization](#2-problem-parametrization) -3. [Graph Topologies](#3-graph-topologies) -4. [Problem Models](#4-problem-models) -5. [Reductions System](#5-reductions-system) -6. [Solution Representation](#6-solution-representation) -7. [Error Handling](#7-error-handling) - -**Developer Guide** -8. [Common Patterns & Workflows](#8-common-patterns--workflows) -9. [Extension Guide](#9-extension-guide) -10. [Internal Modules](#10-internal-modules) -11. [Testing Utilities](#11-testing-utilities) - -**Advanced Topics** -12. [Performance Considerations](#12-performance-considerations) -13. [Known Limitations](#13-known-limitations) -14. [FAQ & Troubleshooting](#14-faq--troubleshooting) -15. [Complete End-to-End Example](#15-complete-end-to-end-example) - -**CLI Tool** -16. [CLI Tool (`pred`)](#16-cli-tool-pred) - ---- - -## 0. Conceptual Overview - -### What is problem-reductions? - -This library solves one fundamental problem in computational complexity: **establishing relationships between different NP-hard problems**. - -The key insight is that NP-hard problems are equivalent in a specific sense: -- **Polynomial reduction**: If problem A can be reduced to problem B in polynomial time, then solving B gives us a solution to A -- **Hardness**: If any NP-hard problem is solvable in polynomial time, all NP-hard problems are -- **Practical value**: Understanding reductions helps us transfer algorithms and insights between problems - -### The Two-Pillar Architecture - -The library has two main parts working together: - -**1. Problem Definitions** (what to solve) -``` -MaximumIndependentSet - ↓ describes -A problem instance with variables, constraints, objective - ↓ can be -Solved by BruteForce, or reduced to another problem -``` - -**2. Reductions** (how to transform) -``` -MaximumIndependentSet --reduce--> MinimumVertexCover - ↓ via -ReduceTo trait - ↓ provides -Variable mapping: IS_solution --extract--> VC_solution -``` - -### Mental Model: Three Levels - -``` -┌─────────────────────────────────────────────────┐ -│ ABSTRACTION (What developer thinks about) │ -│ "I have an Independent Set problem" │ -└──────────────┬──────────────────────────────────┘ - │ -┌──────────────▼──────────────────────────────────┐ -│ TRAIT LEVEL (Rust contract) │ -│ Problem trait: dims(), evaluate() │ -│ ReduceTo trait: reduce_to(), extract_solution() │ -└──────────────┬──────────────────────────────────┘ - │ -┌──────────────▼──────────────────────────────────┐ -│ CONCRETE (Actual data) │ -│ MaximumIndependentSet │ -│ - graph: SimpleGraph │ -│ - weights: Vec │ -└─────────────────────────────────────────────────┘ -``` - -### Key Concepts You'll Encounter - -| Concept | Meaning | Example | -|---------|---------|---------| -| **Problem** | A decision/optimization task with variables and constraints | "Find the maximum independent set" | -| **Configuration** | An assignment of values to all variables | `[1, 0, 1, 0, 1]` for 5 boolean variables | -| **Metric** | The evaluation result of a configuration | `SolutionSize::Valid(42)` or `bool` | -| **Variant** | Dimensions describing a problem type | `[("graph", "SimpleGraph"), ("weight", "i32")]` | -| **Reduction** | Polynomial-time transformation from problem A to problem B | SAT → MaximumIndependentSet | -| **Overhead** | How problem size grows during reduction | n' = n + 10m (n vars, m clauses) | - ---- - -## 0a. Design Philosophy - -### Why Generic Over Graph Topologies? - -**Problem**: Many graph problems exist on different graph structures: -- Simple graphs (general case) -- Grid graphs (VLSI, image processing) -- Unit disk graphs (wireless networks) -- Hypergraphs (set systems) - -**Solution**: Don't rewrite the same problem logic for each topology. Instead: - -```rust -// One problem definition works for ALL topologies -pub struct MaximumIndependentSet { - graph: G, // Can be SimpleGraph, GridGraph, UnitDiskGraph, ... - weights: Vec, // Can be i32, f64, One, ... -} -``` - -**Benefits**: -- ✅ Code reuse -- ✅ Type safety (compiler checks graph compatibility) -- ✅ Monomorphization (no runtime overhead) - -### Why Separate Types for Weight? - -**Problem**: Problems exist in both weighted and unweighted forms: -- Unweighted: all vertices have weight 1 (simpler, sometimes restricted variant) -- Weighted: vertices have different weights (general case) - -**Solution**: Use the weight type as a parameter: - -```rust -MaximumIndependentSet::::new(5, edges) // Unweighted variant -MaximumIndependentSet::::new(5, edges) // Weighted variant (all weights = 1 initially) -MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]) // Custom weights -``` - -The `One` type is a unit weight marker where `One::to_sum()` always returns `1i32`. The type alias `Unweighted = One` is also available. - -**Benefits**: -- ✅ Semantic clarity: type system enforces which variant you're using -- ✅ Metadata: variant() method can report exact variant for reduction graph -- ✅ Specialization: can optimize for `One` if needed - -### Why Traits Instead of Inheritance? - -Rust doesn't have traditional OOP inheritance, so we use traits to define contracts: - -```rust -pub trait Problem: Clone { - type Metric: Clone; - fn dims(&self) -> Vec; - fn evaluate(&self, config: &[usize]) -> Self::Metric; - // ... -} - -// Now we can write generic code that works with ANY problem type! -fn solve_any_problem(p: &P) -> Option> { - // ... -} -``` - -**Why this matters**: -- ✅ Extensible: add new problems without modifying library code -- ✅ Generic: write reduction code once, works for all graph types -- ✅ Type-safe: compiler ensures problems implement required methods - -### Variant System Explained - -The `variant()` method returns metadata about a problem instance: - -```rust -impl Problem for MaximumIndependentSet { - fn variant() -> Vec<(&'static str, &'static str)> { - vec![ - ("graph", "SimpleGraph"), // What graph topology? - ("weight", "i32"), // What weight type? - ] - } -} -``` - -**Why this exists**: -1. **Reduction graph**: The library builds a graph of all possible problem variants and reductions -2. **Runtime metadata**: Know what variant you're working with without exposing Rust types -3. **Documentation**: Automatically discover which problems are available and how they relate - -**Think of it like**: -``` -Problem name: "MaximumIndependentSet" -├── Variant 1: graph=SimpleGraph, weight=One -├── Variant 2: graph=SimpleGraph, weight=i32 -├── Variant 3: graph=GridGraph, weight=i32 -└── Variant 4: graph=UnitDiskGraph, weight=f64 -``` - ---- - -## 0b. How Everything Fits Together - -This diagram shows how all the pieces interconnect: - -``` - ┌─────────────────────────────────────┐ - │ USER CODE │ - │ (Your application) │ - └──────────────┬──────────────────────┘ - │ - ┌──────────────▼──────────────────────┐ - │ PROBLEM INSTANCES │ - │ MaximumIndependentSet │ - │ MinimumVertexCover │ - │ Satisfiability │ - └──────────────┬──────────────────────┘ - │ - ┌──────────────────┼──────────────────────┐ - │ │ │ - ┌───────▼────────┐ ┌──────▼──────┐ ┌───────────▼───────┐ - │ SOLVERS │ │ REDUCTIONS │ │ INTROSPECTION │ - │ │ │ │ │ │ - │ - BruteForce │ │ ReduceTo │ │ - variant() │ - │ - ILPSolver │ │ │ │ - dims() │ - │ - Custom │ │ │ │ - NAME constant │ - └───────┬────────┘ └──────┬──────┘ └───────────┬───────┘ - │ │ │ - │ ┌───────────▼──────────────┐ │ - └─────▶│ TRAIT SYSTEM │◀──────┘ - │ (Core abstraction) │ - │ - Problem │ - │ - OptimizationProblem │ - │ - SatisfactionProblem │ - │ - ReduceTo │ - │ - Solver │ - └───────────┬──────────────┘ - │ - ┌───────────▼──────────────┐ - │ REDUCTION GRAPH │ - │ (Built at startup) │ - │ │ - │ Nodes: │ - │ MIS[SimpleGraph, i32] │ - │ MVC[SimpleGraph, i32] │ - │ SAT │ - │ │ - │ Edges: │ - │ MIS ◀─complement─▶ MVC │ - │ SAT ─reduce─▶ MIS │ - │ ... │ - └──────────────────────────┘ -``` - -### Lifecycle of a Reduction Query - -When you call `.reduce_to()`, here's what happens internally: - -``` -User code: - let reduction = source.reduce_to(); - // type inferred: ReduceTo - -⬇️ Rust compiler searches for impl ReduceTo for SourceProblem - -⬇️ If found, the impl is called: - fn reduce_to(&self) -> Self::Result { - // Construct target problem from source - // Set up solution extraction mapping - // Return result type - } - -⬇️ Result type provides: - - target_problem(): reference to constructed problem - - extract_solution(target_sol): map back to source - -⬇️ Your code can now: - // Solve target - let target_solution = solver.find_best(target_problem); - - // Extract source solution - let source_sol = reduction.extract_solution(&target_solution); - // source_sol is now valid for original problem! -``` - -### Where Variant Information Flows - -``` -Problem Type Definition: - impl Problem for MaximumIndependentSet { - fn variant() -> Vec<(&str, &str)> { - vec![("graph", "SimpleGraph"), ("weight", "i32")] - } - } - -⬇️ Inventory System (at compile time): - Collects all Problem implementations - Extracts their variant() info - Builds reduction metadata - -⬇️ Reduction Graph (at runtime): - Nodes represent [Problem + Variant] pairs: - - MIS[SimpleGraph, i32] - - MIS[SimpleGraph, f64] - - MIS[GridGraph, i32] - - MVC[SimpleGraph, i32] - - ... - - Edges represent available reductions: - - MIS[SimpleGraph, i32] --complement--> MVC[SimpleGraph, i32] - - MIS[SimpleGraph, i32] --to-set-packing--> SetPacking[...] - - ... - -⬇️ Your Application: - Can query: "What problems can I reduce this to?" - Answer: "MVC[SimpleGraph, i32], SetPacking[...], ..." - - Can query: "What's the overhead of reducing A to B?" - Answer: "num_vars: 1n + 0m, num_constraints: 5m" -``` - ---- - -## 1. Core Traits - -This section explains the fundamental traits that define the contract for problem types. - -### Understanding Trait Hierarchy - -``` -┌─────────────────────────────────────┐ -│ Problem (base trait) │ -│ - const NAME │ -│ - type Metric │ -│ - dims(), evaluate() │ -│ - num_variables() (derived) │ -│ - variant() │ -└──────────────┬──────────────────────┘ - │ - ┌──────────┴──────────────────────────────────┐ - │ │ -┌───▼───────────────────────────┐ ┌──────────────▼───────────────┐ -│ OptimizationProblem │ │ SatisfactionProblem │ -│ (Metric = SolutionSize) │ │ (Metric = bool) │ -│ + type Value │ │ (marker trait, no methods) │ -│ + direction() │ │ │ -└───────────────────────────────┘ └──────────────────────────────┘ - -Solver - - find_best() for OptimizationProblem - - find_satisfying() for SatisfactionProblem - -ReduceTo - - reduce_to() → ReductionResult -``` - -**Key relationship**: -- All problems implement `Problem` -- Optimization problems additionally implement `OptimizationProblem` -- Decision problems additionally implement `SatisfactionProblem` -- Solvers dispatch on the problem type -- Reductions connect problem types - -### 1.1 Problem Trait - -**Location**: `src/traits.rs` - -The foundational trait that all problems must implement. - -```rust -pub trait Problem: Clone { - /// Base name of this problem type (e.g., "MaximumIndependentSet"). - const NAME: &'static str; - - /// The evaluation metric type. - type Metric: Clone; - - /// Configuration space dimensions. Each entry is the cardinality of that variable. - fn dims(&self) -> Vec; - - /// Evaluate the problem on a configuration. - fn evaluate(&self, config: &[usize]) -> Self::Metric; - - /// Number of variables (derived from dims). - fn num_variables(&self) -> usize { - self.dims().len() - } - - /// Returns variant attributes derived from type parameters. - fn variant() -> Vec<(&'static str, &'static str)>; -} -``` - -#### Associated Types - -| Type | Bounds | Purpose | -|------|--------|---------| -| `Metric` | `Clone` | Evaluation result type — `SolutionSize` for optimization, `bool` for satisfaction | - -#### Required Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `dims()` | `Vec` | Configuration space dimensions (e.g., `[2, 2, 2]` for 3 binary vars) | -| `evaluate(config)` | `Self::Metric` | Evaluate a configuration | -| `variant()` | `Vec<(&str, &str)>` | Describe problem variant (graph type, weight type) | - -#### Provided Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `num_variables()` | `usize` | Number of decision variables (default: `dims().len()`) | - -#### Contract - -- Configuration length must equal `num_variables()` (i.e., `dims().len()`) -- Each configuration value at index `i` must be in `0..dims()[i]` -- `evaluate()` must never panic on valid configs -- Invalid configs: behavior is implementation-defined (may return `SolutionSize::Invalid`, `false`, etc.) - -#### Variant Method Deep Dive - -The `variant()` method is **static** (uses `Self::` not `&self`) and returns metadata about problem variants: - -```rust -impl Problem for MaximumIndependentSet { - fn variant() -> Vec<(&'static str, &'static str)> { - vec![("graph", "SimpleGraph"), ("weight", "i32")] - } -} -``` - -**Why static?** Because we need variant info *before* creating a problem instance. This lets us: -1. Build a reduction graph at startup (knowing all available problems) -2. Match reductions without instantiating problems -3. Provide introspection without overhead - -**Common variant keys**: -- `"graph"` - Graph topology type (e.g., "SimpleGraph", "GridGraph", "UnitDiskGraph") -- `"weight"` - Weight type (e.g., "i32", "f64", "One") - -**How variants are used internally**: -```rust -// The library builds this mapping: -"MaximumIndependentSet" has variants: - - [("graph", "SimpleGraph"), ("weight", "One")] - - [("graph", "SimpleGraph"), ("weight", "i32")] - - [("graph", "SimpleGraph"), ("weight", "f64")] - - [("graph", "GridGraph"), ("weight", "i32")] - - ... - -Then it finds which reductions are available: - - MIS[SimpleGraph, i32] --complement--> MVC[SimpleGraph, i32] - - MIS[SimpleGraph, i32] --subset--> SetPacking[...] - - ... -``` - -#### Creating Configurations - -Understanding how configurations are created and what they represent: - -```rust -// Binary problem: dims() = [2, 2, 2, 2, 2] -// Variables represent yes/no decisions -let config = vec![1, 0, 1, 0, 1]; // Five binary decisions -// config[0] = 1: select vertex 0 -// config[1] = 0: don't select vertex 1 -// config[2] = 1: select vertex 2 -// etc. - -// Multi-flavor problem: dims() = [3, 3, 3, 3, 3] (e.g., k-coloring with k=3) -// Variables represent k-way choices -let config = vec![0, 1, 2, 1, 0]; // Five vertices, up to 3 colors -// config[0] = 0: color vertex 0 with color 0 -// config[1] = 1: color vertex 1 with color 1 -// etc. -``` - -### 1.2 OptimizationProblem Trait - -**Location**: `src/traits.rs` - -Extension for problems with a numeric objective to optimize. - -```rust -pub trait OptimizationProblem: Problem> { - /// The inner objective value type (e.g., `i32`, `f64`). - type Value: PartialOrd + Clone; - - /// Whether to maximize or minimize the metric. - fn direction(&self) -> Direction; -} -``` - -The supertrait bound guarantees `Metric = SolutionSize`, so the solver can call `metric.is_valid()` and `metric.is_better()` directly — no per-problem customization needed. - -#### Associated Types - -| Type | Bounds | Purpose | -|------|--------|---------| -| `Value` | `PartialOrd + Clone` | Inner objective value (e.g., `i32`, `f64`) | - -#### Required Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `direction()` | `Direction` | `Direction::Maximize` or `Direction::Minimize` | - -#### When to Use OptimizationProblem - -**Use OptimizationProblem when**: -- Your problem has a numeric objective to optimize -- Configurations can be feasible or infeasible - -**Examples that use it**: -- `MaximumIndependentSet` (maximize weight, constraint: no adjacent vertices) -- `MinimumVertexCover` (minimize weight, constraint: all edges covered) -- `MaxCut` (maximize cut weight) -- `QUBO` (minimize quadratic objective) - -### 1.3 SatisfactionProblem Trait - -**Location**: `src/traits.rs` - -Marker trait for satisfaction (decision) problems. - -```rust -pub trait SatisfactionProblem: Problem {} -``` - -Satisfaction problems evaluate configurations to `bool`: `true` if the configuration satisfies all constraints, `false` otherwise. - -**Examples that use it**: -- `Satisfiability` (SAT) -- `KSatisfiability` (k-SAT) - -### 1.4 Solver Trait - -**Location**: `src/solvers/mod.rs` - -Interface for all solvers. - -```rust -pub trait Solver { - /// Find one optimal solution for an optimization problem. - fn find_best(&self, problem: &P) -> Option>; - - /// Find any satisfying solution for a satisfaction problem (Metric = bool). - fn find_satisfying>(&self, problem: &P) -> Option>; -} -``` - -#### Contract - -- `find_best()` returns one optimal configuration, or `None` if no feasible solution exists -- `find_satisfying()` returns one satisfying configuration, or `None` if unsatisfiable -- Use `BruteForce::find_all_best()` / `find_all_satisfying()` for all solutions - -### 1.5 ReduceTo Trait - -**Location**: `src/rules/traits.rs` - -Interface for problem reductions. - -```rust -pub trait ReduceTo: Problem { - /// The reduction result type. - type Result: ReductionResult; - - /// Reduce this problem to the target problem type. - fn reduce_to(&self) -> Self::Result; -} - -pub trait ReductionResult: Clone { - /// The source problem type. - type Source: Problem; - /// The target problem type. - type Target: Problem; - - /// Get a reference to the target problem. - fn target_problem(&self) -> &Self::Target; - - /// Extract a solution from target problem space to source problem space. - fn extract_solution(&self, target_solution: &[usize]) -> Vec; -} -``` - -#### Usage - -```rust -// Reduce source problem to target problem -let source = MaximumIndependentSet::::new(5, edges); -let reduction = source.reduce_to(); // type inferred from context -let target = reduction.target_problem(); - -// Solve target and extract source solution -let solver = BruteForce::new(); -if let Some(target_sol) = solver.find_best(target) { - let source_sol = reduction.extract_solution(&target_sol); - // source_sol is now a valid solution to the original problem -} -``` - ---- - -## 2. Problem Parametrization - -### 2.1 Generic Type Parameters - -All graph-based problems are parametrized by: - -```rust -pub struct ProblemName { - graph: G, - weights: Vec, -} -``` - -- **G**: Graph topology (e.g., `SimpleGraph`, `GridGraph`, `UnitDiskGraph`) -- **W**: Weight type (e.g., `i32`, `f64`, `One`) - -#### Weight Type System - -Weights use the `WeightElement` trait: - -```rust -pub trait WeightElement: Clone + Default + 'static { - /// The numeric type used for sums and comparisons. - type Sum: NumericSize; - /// Convert this weight element to the sum type. - fn to_sum(&self) -> Self::Sum; -} -``` - -This decouples the per-element weight type from the accumulation type: -- For `i32`: `Sum = i32`, `to_sum()` returns the value -- For `f64`: `Sum = f64`, `to_sum()` returns the value -- For `One`: `Sum = i32`, `to_sum()` always returns `1` - -The `NumericSize` supertrait bundles common numeric bounds: -```rust -NumericSize: Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static -``` - -### 2.2 Unweighted Problems - -For unweighted variants, use the `One` marker type (or its alias `Unweighted`): - -```rust -use problemreductions::types::One; - -// Unweighted variant — all vertices have weight 1 -let problem = MaximumIndependentSet::::new(5, edges); -``` - ---- - -## 3. Graph Topologies - -**Location**: `src/topology/` - -### 3.1 Graph Trait - -```rust -pub trait Graph: Clone + Send + Sync + 'static { - /// The name of the graph type (e.g., "SimpleGraph", "GridGraph"). - const NAME: &'static str; - - fn num_vertices(&self) -> usize; - fn num_edges(&self) -> usize; - fn neighbors(&self, vertex: usize) -> Vec; - fn has_edge(&self, u: usize, v: usize) -> bool; - fn edges(&self) -> Vec<(usize, usize)>; - // ... other methods -} -``` - -### 3.2 Built-in Topologies - -#### SimpleGraph - -**Location**: `src/topology/graph.rs` - -Standard undirected simple graph (no self-loops, no multi-edges). - -```rust -let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3)]); -``` - -#### GridGraph - -**Location**: `src/topology/grid_graph.rs` - -Vertices arranged in a 2D grid. Vertices are neighbors if adjacent in grid. - -```rust -let graph = GridGraph::new(4, 4); // 4x4 grid, 16 vertices total -``` - -#### UnitDiskGraph - -**Location**: `src/topology/unit_disk_graph.rs` - -Vertices in 2D Euclidean space with edges between vertices within distance 1. - -```rust -let vertices = vec![(0.0, 0.0), (0.5, 0.5), (1.5, 1.5)]; -let graph = UnitDiskGraph::new(vertices); -``` - -#### Hypergraph - -**Location**: `src/topology/hypergraph.rs` - -Vertices with hyperedges (edges can contain more than 2 vertices). - ---- - -## 4. Problem Models - -**Location**: `src/models/` - -Problems are organized by category: - -### 4.1 Graph Problems - -**Location**: `src/models/graph/` - -- **MaximumIndependentSet** - Maximum weight independent set (no adjacent vertices) -- **MinimumVertexCover** - Minimum weight vertex cover (cover all edges) -- **MinimumDominatingSet** - Minimum dominating set (every vertex is dominated) -- **MaxCut** - Maximum cut (maximize edges between two partitions) -- **MaximumClique** - Maximum weight clique (all vertices pairwise adjacent) -- **MaximumMatching** - Maximum weight matching (no two edges share vertices) -- **KColoring** - K-vertex coloring (adjacent vertices have different colors) -- **MaximalIS** - Maximal (not maximum) independent set -- **TravelingSalesman** - Minimum weight Hamiltonian cycle - -#### Example: MaximumIndependentSet - -```rust -use problemreductions::models::graph::MaximumIndependentSet; -use problemreductions::topology::SimpleGraph; - -// Unit-weighted (all vertices have weight 1) -let problem = MaximumIndependentSet::::new(5, vec![(0,1), (1,2)]); - -// Custom weights -let problem = MaximumIndependentSet::::with_weights( - 5, - vec![(0,1), (1,2)], - vec![1, 2, 3, 4, 5] -); - -// From existing graph -let graph = SimpleGraph::new(5, edges); -let problem = MaximumIndependentSet::from_graph(graph, weights); - -// From graph with unit weights -let problem = MaximumIndependentSet::::from_graph_unit_weights(graph); -``` - -### 4.2 Satisfiability Problems - -**Location**: `src/models/satisfiability/` - -- **Satisfiability** - Boolean satisfiability (CNF clauses) -- **KSatisfiability** - SAT restricted to k-literal clauses - -#### Example: Satisfiability - -```rust -use problemreductions::models::satisfiability::{Satisfiability, CNFClause}; - -// Clauses in CNF: (x1 ∨ x2) ∧ (¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3) -// Literals are 1-indexed signed integers (positive = true, negative = negated) -let problem = Satisfiability::new(3, vec![ - CNFClause::new(vec![1, 2]), // x1 ∨ x2 - CNFClause::new(vec![-2, 3]), // ¬x2 ∨ x3 - CNFClause::new(vec![-1, -3]), // ¬x1 ∨ ¬x3 -]); -``` - -### 4.3 Set Problems - -**Location**: `src/models/set/` - -- **MinimumSetCovering** - Minimum weight set cover -- **MaximumSetPacking** - Maximum weight set packing - -#### Example: MinimumSetCovering - -```rust -use problemreductions::models::set::MinimumSetCovering; - -// Covering problem: select sets to cover all elements -let problem = MinimumSetCovering::::new( - 3, // universe_size (elements 0..3) - vec![ - vec![0, 1], // Set 0 covers elements 0, 1 - vec![1, 2], // Set 1 covers elements 1, 2 - vec![0, 2], // Set 2 covers elements 0, 2 - ], -); -``` - -### 4.4 Optimization Problems - -**Location**: `src/models/optimization/` - -- **SpinGlass** - Ising model Hamiltonian minimization -- **QUBO** - Quadratic unconstrained binary optimization -- **ILP** - Integer linear programming (requires `ilp` feature) - -### 4.5 Specialized Problems - -**Location**: `src/models/specialized/` - -- **CircuitSAT** - Boolean circuit satisfiability -- **Factoring** - Integer factorization -- **PaintShop** - Minimize color switches -- **BicliqueCover** - Biclique cover on bipartite graphs -- **BMF** - Boolean matrix factorization - ---- - -## 5. Reductions System - -**Location**: `src/rules/` - -### 5.1 Reduction Structure - -```rust -// Define the reduction result type -pub struct ReductionSourceToTarget { - target: TargetProblem, - // ... solution extraction data -} - -impl ReductionResult for ReductionSourceToTarget { - type Source = SourceProblem; - type Target = TargetProblem; - - fn target_problem(&self) -> &TargetProblem { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // Map target solution back to source variables - // ... - } -} - -// Register the reduction -impl ReduceTo for SourceProblem { - type Result = ReductionSourceToTarget; - - fn reduce_to(&self) -> ReductionSourceToTarget { - // Construct target problem from source problem - // Create solution mapping - // ... - } -} - -// Register metadata for graph visualization -inventory::submit! { - ReductionEntry { - source_name: "SourceProblem", - target_name: "TargetProblem", - source_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], - target_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vars", Polynomial { ... }), - ("num_constraints", Polynomial { ... }), - ]), - } -} -``` - -### 5.2 Built-in Reductions - -The library includes reductions between these problem pairs: - -- **MaximumIndependentSet ↔ MinimumVertexCover** - Complement on same graph -- **MaximumIndependentSet → MaximumSetPacking** - Element sets as independent sets -- **MaximumIndependentSet → QUBO** - Penalty encoding -- **MinimumVertexCover → MinimumSetCovering** - Elements from edges -- **MinimumVertexCover → QUBO** - Penalty encoding -- **MaximumMatching → MaximumSetPacking** - Edge representation -- **MaximumSetPacking → QUBO** - Penalty encoding -- **SAT → MaximumIndependentSet** - Variable gadgets -- **SAT → KColoring** - Clause coloring -- **SAT → MinimumDominatingSet** - Domination gadgets -- **SAT ↔ K-SAT** - Clause conversion -- **K-SAT → QUBO** - Direct QUBO encoding -- **KColoring → QUBO** - Color penalty encoding -- **SpinGlass ↔ MaxCut** - Weight transformation -- **SpinGlass ↔ QUBO** - Problem transformation -- **CircuitSAT → SpinGlass** - Logic gadgets -- **Factoring → CircuitSAT** - Multiplication circuit - -Natural-edge reductions (graph subtype relaxation): -- **MIS\ → MIS\** - Identity mapping -- **MIS\ → MIS\** - Graph cast -- **MIS\ → MIS\** - Graph cast - -Feature-gated reductions (require `ilp` feature): -- Various problems → **ILP** (MaximumIndependentSet, MinimumVertexCover, MaximumClique, MaximumMatching, MinimumDominatingSet, MinimumSetCovering, MaximumSetPacking, KColoring, Factoring, TravelingSalesman) -- **ILP → QUBO** - Linearization - -### 5.3 Reduction Overhead - -Every reduction in the library carries **overhead metadata** that describes how the target problem size relates to the source problem size. This is essential for: - -1. **Cost-aware path finding**: When multiple reduction chains exist (e.g., SAT → MIS → QUBO vs SAT → 3-SAT → QUBO), the overhead lets the system pick the chain that produces the smallest target problem for a given input -2. **Documentation**: The paper and reduction graph visualization automatically display overhead formulas -3. **Planning**: Users can estimate target problem size before actually performing the reduction - -#### Core Types - -**Location**: `src/rules/registry.rs` and `src/polynomial.rs` - -```rust -/// Overhead specification for a reduction. -pub struct ReductionOverhead { - /// Output size as polynomials of input size variables. - /// Each entry is (output_field_name, polynomial_formula). - pub output_size: Vec<(&'static str, Polynomial)>, -} -``` - -Each entry maps an **output field name** (a dimension of the target problem) to a **polynomial** of input field names (dimensions of the source problem). For example, when reducing MIS to ILP, the output might specify that ILP's `num_vars` equals the graph's `num_vertices`, and ILP's `num_constraints` equals the graph's `num_edges`. - -Polynomials are built from monomials: - -```rust -/// A monomial: coefficient × Π(variable^exponent) -pub struct Monomial { - pub coefficient: f64, - pub variables: Vec<(&'static str, u8)>, // (variable_name, exponent) -} - -/// A polynomial: Σ monomials -pub struct Polynomial { - pub terms: Vec, -} -``` - -#### The `poly!` Macro - -Instead of constructing `Polynomial` and `Monomial` values manually, use the `poly!` convenience macro: - -```rust -use problemreductions::poly; - -// Single variable: p(x) = num_vertices -poly!(num_vertices) - -// Variable with exponent: p(x) = num_literals² -poly!(num_literals ^ 2) - -// Constant: p(x) = 5 -poly!(5) - -// Scaled variable: p(x) = 3 × num_vertices -poly!(3 * num_vertices) - -// Scaled variable with exponent: p(x) = 9 × n² -poly!(9 * n ^ 2) - -// Product of two variables: p(x) = num_vertices × num_colors -poly!(num_vertices * num_colors) - -// Scaled product: p(x) = 3 × a × b -poly!(3 * a * b) - -// Addition (combine with + operator): -poly!(num_vars) + poly!(num_clauses) // p(x) = num_vars + num_clauses -``` - -#### Specifying Overhead in Reductions - -Overhead is attached to reductions via the `#[reduction]` proc macro attribute: - -```rust -#[reduction( - overhead = { - ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]) - } -)] -impl ReduceTo> for MaximumIndependentSet { - // ... -} -``` - -The field names (e.g., `"num_vertices"`, `"num_edges"`) in the polynomial variables refer to the **source problem's** `ProblemSize` components. The field names as keys (the first element of each tuple) name the **target problem's** size dimensions. - -#### Real-World Examples - -**Identity overhead** (MIS ↔ MVC complement): -```rust -// Same graph, same size — no blowup -ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), // target has same vertices - ("num_edges", poly!(num_edges)), // target has same edges -]) -``` - -**Linear overhead** (MIS → ILP): -```rust -// One ILP variable per vertex, one constraint per edge -ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_edges)), -]) -``` - -**Quadratic overhead** (SAT → MIS): -```rust -// Vertices = number of literals, edges up to literals² -ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_literals)), - ("num_edges", poly!(num_literals ^ 2)), -]) -``` - -**Product overhead** (KColoring → QUBO): -```rust -// One QUBO variable per (vertex, color) pair -ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices * num_colors)), -]) -``` - -**Additive overhead** (SAT → K-SAT clause splitting): -```rust -// Splitting long clauses adds extra variables and clauses -ReductionOverhead::new(vec![ - ("num_clauses", poly!(num_clauses) + poly!(num_literals)), - ("num_vars", poly!(num_vars) + poly!(num_literals)), -]) -``` - -#### Evaluating Overhead at Runtime - -Given a concrete input problem size, you can compute the expected target size: - -```rust -let overhead = ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices)), - ("num_constraints", poly!(num_edges)), -]); - -let input_size = ProblemSize::new(vec![("num_vertices", 100), ("num_edges", 500)]); -let output_size = overhead.evaluate_output_size(&input_size); - -assert_eq!(output_size.get("num_vars"), Some(100)); -assert_eq!(output_size.get("num_constraints"), Some(500)); -``` - -### 5.4 Cost-Aware Path Finding - -**Location**: `src/rules/cost.rs` and `src/rules/graph.rs` - -The reduction graph supports Dijkstra-based shortest-path search with customizable cost functions. This lets you find the cheapest multi-step reduction chain between any two problem types, where "cheapest" is defined by the overhead formulas evaluated on your actual input size. - -#### PathCostFn Trait - -```rust -pub trait PathCostFn { - /// Compute cost of taking an edge given current problem size. - fn edge_cost(&self, overhead: &ReductionOverhead, current_size: &ProblemSize) -> f64; -} -``` - -#### Built-in Cost Functions - -| Cost Function | Description | Use Case | -|--------------|-------------|----------| -| `Minimize("field")` | Minimize a single output field | "I want the fewest QUBO variables" | -| `MinimizeWeighted(vec)` | Minimize weighted sum of fields | "Balance variables and constraints" | -| `MinimizeMax(vec)` | Minimize the maximum of fields | "No single dimension should blow up" | -| `MinimizeLexicographic(vec)` | Lexicographic minimization | "Minimize vars first, break ties by constraints" | -| `MinimizeSteps` | Minimize number of reduction hops | "Shortest chain regardless of size" | -| `CustomCost(closure)` | User-defined cost from closure | Any custom objective | - -#### Usage Example - -```rust -use problemreductions::rules::{ReductionGraph, Minimize}; -use problemreductions::types::ProblemSize; - -let graph = ReductionGraph::build(); - -// Find cheapest path from SAT to QUBO, minimizing QUBO variables -let input_size = ProblemSize::new(vec![ - ("num_vars", 10), - ("num_clauses", 20), - ("num_literals", 60), // 20 clauses × 3 literals -]); - -let path = graph.find_cheapest_path( - ("Satisfiability", ""), - ("QUBO", ""), - &input_size, - &Minimize("num_vars"), -); - -if let Some(path) = path { - println!("Best chain has {} steps", path.len()); - for step in &path.edges { - println!(" {} → {}", step.source_name, step.target_name); - } -} -``` - -#### How It Works - -The path finder uses Dijkstra's algorithm on the reduction graph: - -``` -1. Start at source node with input_size -2. For each outgoing edge, compute: - - edge_cost = cost_fn.edge_cost(&edge.overhead, ¤t_size) - - new_size = edge.overhead.evaluate_output_size(¤t_size) -3. Propagate new_size as current_size for the next hop -4. Continue until reaching the target node -5. Return the minimum-cost path -``` - -This means the cost function sees the **accumulated** problem size at each step, not just the original input. A chain A → B → C correctly accounts for B's intermediate size when computing the cost of B → C. - ---- - -## 6. Solution Representation - -### 6.1 Configuration Format - -A configuration is `Vec` where: -- Length equals `num_variables()` (i.e., `dims().len()`) -- Each value at index `i` is in `[0, dims()[i])` - -```rust -// Binary problems (dims = [2, 2, 2, 2, 2]) -config[i] == 0 // Variable i NOT selected -config[i] == 1 // Variable i selected - -// Multi-flavor problems (e.g., k-coloring, dims = [k, k, k, ...]) -config[i] ∈ [0, k) // Color assigned to vertex i -``` - -### 6.2 SolutionSize Enum - -**Location**: `src/types.rs` - -```rust -pub enum SolutionSize { - /// A valid (feasible) solution with the given objective value. - Valid(T), - /// An invalid (infeasible) solution that violates constraints. - Invalid, -} - -impl SolutionSize { - pub fn is_valid(&self) -> bool; - pub fn size(&self) -> Option<&T>; - pub fn unwrap(self) -> T; // panics if Invalid - pub fn map U>(self, f: F) -> SolutionSize; -} - -impl SolutionSize { - /// Returns true if self is a better solution than other for the given direction. - pub fn is_better(&self, other: &Self, direction: Direction) -> bool; -} -``` - -**Key differences from a struct-based approach**: -- `SolutionSize::Invalid` carries no size — invalid configs simply have no meaningful objective -- Pattern matching: `if let SolutionSize::Valid(size) = result { ... }` -- `is_better()` considers: Valid always beats Invalid; two Invalids are equally bad - -### 6.3 Direction Enum - -**Location**: `src/types.rs` - -```rust -pub enum Direction { - Maximize, - Minimize, -} -``` - -Used by `OptimizationProblem::direction()` and `SolutionSize::is_better()`. - ---- - -## 7. Error Handling - -### 7.1 Error Types - -**Location**: `src/error.rs` - -```rust -#[derive(Error, Debug, Clone, PartialEq)] -pub enum ProblemError { - #[error("invalid configuration size: expected {expected}, got {got}")] - InvalidConfigSize { expected: usize, got: usize }, - - #[error("invalid flavor value {value} at index {index}: expected 0..{num_flavors}")] - InvalidFlavor { index: usize, value: usize, num_flavors: usize }, - - #[error("invalid problem: {0}")] - InvalidProblem(String), - - #[error("invalid weights length: expected {expected}, got {got}")] - InvalidWeightsLength { expected: usize, got: usize }, - - #[error("empty problem: {0}")] - EmptyProblem(String), - - #[error("index out of bounds: {index} >= {bound}")] - IndexOutOfBounds { index: usize, bound: usize }, - - #[error("I/O error: {0}")] - IoError(String), - - #[error("serialization error: {0}")] - SerializationError(String), -} -``` - -### 7.2 Panic vs Result Strategy - -The library uses **panics for programming errors**: - -| Situation | Handling | Rationale | -|-----------|----------|-----------| -| Invalid vertex indices | Panic | Programming error | -| Weight length mismatch | Panic | Programming error | -| Invalid config length | Return `SolutionSize::Invalid` or `false` | Runtime validation | -| Constraint violation | Return `SolutionSize::Invalid` | Normal operation | - -**Example**: -```rust -pub fn with_weights(..., weights: Vec) -> Self { - assert_eq!(weights.len(), num_vertices, - "weights length must match num_vertices"); - // ... -} -``` - ---- - -## 8. Common Patterns & Workflows - -### Pattern 1: Solving a Problem Directly - -The simplest workflow: define a problem, then solve it. - -```rust -use problemreductions::prelude::*; -use problemreductions::models::graph::MaximumIndependentSet; -use problemreductions::topology::SimpleGraph; - -// Step 1: Create the problem -let problem = MaximumIndependentSet::::new( - 5, // 5 vertices - vec![(0, 1), (1, 2), (2, 3)] // Edges -); - -// Step 2: Verify problem properties -println!("Variables: {}", problem.num_variables()); // Output: 5 -println!("Direction: {:?}", problem.direction()); // Maximize -println!("Variant: {:?}", MaximumIndependentSet::::variant()); - -// Step 3: Solve it -let solver = BruteForce::new(); -let optimal_solutions = solver.find_all_best(&problem); - -// Step 4: Evaluate and interpret -for solution in &optimal_solutions { - let result = problem.evaluate(solution); - if let SolutionSize::Valid(size) = result { - println!("Solution: {:?}, Size: {}", solution, size); - } -} -``` - -### Pattern 2: Using Reductions - -Transform a problem you're interested in to another problem, solve it, extract the solution. - -```rust -use problemreductions::rules::ReduceTo; - -// Step 1: Create source problem -let source = MaximumIndependentSet::::new(5, edges); - -// Step 2: Reduce to target problem (must implement ReduceTo) -let reduction: ReductionISToVC<_, _> = source.reduce_to(); -let target = reduction.target_problem(); - -// Step 3: Solve the target problem -let solver = BruteForce::new(); -let target_solutions = solver.find_all_best(target); - -// Step 4: Extract source solutions from target solutions -for target_sol in &target_solutions { - let source_sol = reduction.extract_solution(target_sol); - let result = source.evaluate(&source_sol); - if let SolutionSize::Valid(size) = result { - println!("Original problem solution: {:?}, size: {}", source_sol, size); - } -} -``` - -**Why do this?** -- Different solvers might work better for the target problem -- Reductions let you leverage algorithms designed for other problems -- Understanding reductions helps verify solution correctness - -### Pattern 3: Implementing a Problem - -Create a new NP-hard problem type. - -```rust -use problemreductions::topology::{Graph, SimpleGraph}; -use problemreductions::traits::{Problem, OptimizationProblem}; -use problemreductions::types::{Direction, SolutionSize, WeightElement}; -use num_traits::Zero; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MyProblem { - graph: G, - weights: Vec, -} - -impl MyProblem { - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self - where - W: From, - { - let graph = SimpleGraph::new(num_vertices, edges); - let weights = vec![W::from(1); num_vertices]; - Self { graph, weights } - } -} - -impl Problem for MyProblem -where - G: Graph, - W: WeightElement, -{ - const NAME: &'static str = "MyProblem"; - type Metric = SolutionSize; - - fn variant() -> Vec<(&'static str, &'static str)> { - vec![ - ("graph", G::NAME), - ("weight", crate::variant::short_type_name::()), - ] - } - - fn dims(&self) -> Vec { - vec![2; self.graph.num_vertices()] - } - - fn evaluate(&self, config: &[usize]) -> SolutionSize { - // Check constraints - // ... your constraint logic here ... - - // Compute objective - let mut total = W::Sum::zero(); - for (i, &selected) in config.iter().enumerate() { - if selected == 1 { - total += self.weights[i].to_sum(); - } - } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MyProblem -where - G: Graph, - W: WeightElement, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize - } -} -``` - -### Pattern 4: Implementing a Reduction - -Transform problem A into problem B, with solution extraction. - -```rust -use problemreductions::rules::{ReduceTo, ReductionResult}; - -// Result type holds both the transformed problem and mapping data -pub struct ReductionISToVC { - target: MinimumVertexCover, -} - -// Implement the result trait for extraction -impl ReductionResult for ReductionISToVC -where - G: Graph, - W: WeightElement, -{ - type Source = MaximumIndependentSet; - type Target = MinimumVertexCover; - - fn target_problem(&self) -> &MinimumVertexCover { - &self.target - } - - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // For complement: if target selects v, source selects NOT v - target_solution.iter().map(|&x| 1 - x).collect() - } -} - -// Implement the reduction -impl ReduceTo> for MaximumIndependentSet -where - G: Graph, - W: WeightElement, -{ - type Result = ReductionISToVC; - - fn reduce_to(&self) -> ReductionISToVC { - let target = MinimumVertexCover::from_graph( - self.graph().clone(), - self.weights(), - ); - ReductionISToVC { target } - } -} -``` - -### Pattern 5: Solving Satisfaction Problems - -```rust -use problemreductions::prelude::*; - -let sat = Satisfiability::new(3, vec![ - CNFClause::new(vec![1, 2]), - CNFClause::new(vec![-1, 3]), -]); - -let solver = BruteForce::new(); - -// Find one satisfying assignment -if let Some(solution) = solver.find_satisfying(&sat) { - println!("Satisfying: {:?}", solution); -} - -// Find all satisfying assignments -let all = solver.find_all_satisfying(&sat); -println!("Found {} satisfying assignments", all.len()); -``` - ---- - -## 9. Extension Guide - -### 9.1 Adding a New Graph Problem - -See [Pattern 3](#pattern-3-implementing-a-problem) above for the full implementation pattern. - -Key steps: -1. Create `src/models/graph/my_problem.rs` -2. Implement `Problem` trait with `type Metric = SolutionSize` -3. Implement `OptimizationProblem` with `direction()` -4. Register schema with `inventory::submit! { ProblemSchemaEntry { ... } }` -5. Register module in `src/models/graph/mod.rs` - -### 9.2 Adding a Reduction - -**Location**: `src/rules/_.rs` - -See [Pattern 4](#pattern-4-implementing-a-reduction) above for the implementation pattern. - -Key steps: -1. Create `src/rules/_.rs` -2. Define result struct implementing `ReductionResult` -3. Implement `ReduceTo` on the source problem -4. Register metadata with `inventory::submit! { ReductionEntry { ... } }` -5. Register module in `src/rules/mod.rs` - -Use the `#[reduction]` proc macro attribute for automatic inventory registration: -```rust -#[reduction( - overhead = { - ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]) - } -)] -impl ReduceTo for SourceProblem { - // ... -} -``` - -### 9.3 Adding Tests - -```rust -#[cfg(test)] -mod tests { - use super::*; - use crate::solvers::BruteForce; - - #[test] - fn test_source_to_target_closed_loop() { - // 1. Create small instance - let problem = SourceProblem::::new(5, vec![(0,1), (1,2)]); - - // 2. Reduce - let reduction = problem.reduce_to(); - let target = reduction.target_problem(); - - // 3. Solve both directly - let solver = BruteForce::new(); - let source_solutions = solver.find_all_best(&problem); - let target_solutions = solver.find_all_best(target); - - // 4. Extract and verify - for sol in &target_solutions { - let extracted = reduction.extract_solution(sol); - let result = problem.evaluate(&extracted); - assert!(result.is_valid()); - } - } -} -``` - ---- - -## 10. Internal Modules - -### 10.1 Configuration Utilities - -**Location**: `src/config.rs` - -```rust -/// Iterator over all configs for uniform flavor count -pub struct ConfigIterator { /* ... */ } - -impl ConfigIterator { - pub fn new(num_variables: usize, num_flavors: usize) -> Self; - pub fn total(&self) -> usize; -} - -impl Iterator for ConfigIterator { - type Item = Vec; -} - -/// Iterator over all configs for per-variable dimensions -pub struct DimsIterator { /* ... */ } - -impl DimsIterator { - pub fn new(dims: Vec) -> Self; - pub fn total(&self) -> usize; -} - -impl Iterator for DimsIterator { - type Item = Vec; -} - -/// Convert between index and configuration -pub fn index_to_config(index: usize, num_variables: usize, num_flavors: usize) -> Vec; -pub fn config_to_index(config: &[usize], num_flavors: usize) -> usize; - -/// Convert between config and bits -pub fn config_to_bits(config: &[usize]) -> Vec; -pub fn bits_to_config(bits: &[bool]) -> Vec; -``` - -`DimsIterator` is used by `BruteForce` internally to enumerate all configurations based on `problem.dims()`. - -### 10.2 ProblemSize Metadata - -**Location**: `src/types.rs` - -```rust -pub struct ProblemSize { - pub components: Vec<(String, usize)>, -} - -impl ProblemSize { - pub fn new(components: Vec<(&str, usize)>) -> Self; - pub fn get(&self, name: &str) -> Option; -} -``` - -### 10.3 BruteForce Solver - -**Location**: `src/solvers/brute_force.rs` - -```rust -#[derive(Debug, Clone, Default)] -pub struct BruteForce; - -impl BruteForce { - pub fn new() -> Self; - - /// Find all optimal solutions for an optimization problem. - pub fn find_all_best(&self, problem: &P) -> Vec>; - - /// Find all satisfying solutions for a satisfaction problem. - pub fn find_all_satisfying>(&self, problem: &P) -> Vec>; -} - -impl Solver for BruteForce { - /// Returns one optimal solution (or None). - fn find_best(&self, problem: &P) -> Option>; - - /// Returns one satisfying solution (or None). - fn find_satisfying>(&self, problem: &P) -> Option>; -} -``` - -### 10.4 Variant and Type Name Utilities - -**Location**: `src/variant.rs` - -```rust -/// Extract short type name from full path (e.g., "my_crate::types::One" → "One") -pub fn short_type_name() -> &'static str; - -/// Convert const generic usize to static str (for values 1-10) -pub const fn const_usize_str() -> &'static str; -``` - -Used internally by `Problem::variant()` implementations to extract clean type names. - -### 10.5 Polynomial Representation - -**Location**: `src/polynomial.rs` - -Used for representing reduction overhead formulas. - -```rust -pub struct Polynomial { - pub terms: Vec, -} - -pub struct PolynomialTerm { - pub coefficient: f64, - pub variables: Vec, - pub exponent: f64, -} -``` - -### 10.6 Truth Table - -**Location**: `src/truth_table.rs` - -Utilities for working with boolean truth tables (used by CircuitSAT). - ---- - -## 11. Testing Utilities - -**Location**: `src/testing/` - -### 11.1 Test Case Structs - -```rust -/// A test case for graph problems -pub struct GraphTestCase { - pub num_vertices: usize, - pub edges: Vec<(usize, usize)>, - pub weights: Option>, - pub valid_solution: Vec, - pub expected_size: W, - pub optimal_size: Option, -} - -impl GraphTestCase { - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>, - valid_solution: Vec, expected_size: W) -> Self; - pub fn with_weights(num_vertices: usize, edges: Vec<(usize, usize)>, - weights: Vec, valid_solution: Vec, - expected_size: W) -> Self; - pub fn with_optimal(self, optimal: W) -> Self; -} - -/// A test case for SAT problems -pub struct SatTestCase { - pub num_vars: usize, - pub clauses: Vec>, - pub satisfying_assignment: Option>, - pub is_satisfiable: bool, -} - -impl SatTestCase { - pub fn satisfiable(num_vars: usize, clauses: Vec>, - satisfying_assignment: Vec) -> Self; - pub fn unsatisfiable(num_vars: usize, clauses: Vec>) -> Self; -} -``` - -### 11.2 Test Macros - -```rust -// Generate comprehensive test suite for graph problems -graph_problem_tests! { - problem_type: MaximumIndependentSet, - test_cases: [ - (triangle, 3, [(0, 1), (1, 2), (0, 2)], [1, 0, 0], 1, true), - (path, 3, [(0, 1), (1, 2)], [1, 0, 1], 2, true), - ] -} - -// Test that two problems are complements (e.g., IS + VC = n) -complement_test! { - name: test_is_vc_complement, - problem_a: MaximumIndependentSet, - problem_b: MinimumVertexCover, - test_graphs: [ - (3, [(0, 1), (1, 2)]), - (4, [(0, 1), (1, 2), (2, 3)]), - ] -} - -// Quick single-instance validation -quick_problem_test!( - MaximumIndependentSet, - new(3, vec![(0, 1)]), - solution: [0, 0, 1], - expected_value: 1, - is_max: true -); -``` - ---- - -## 12. Performance Considerations - -### 12.1 Brute Force Complexity - -- **Time**: O(∏ dims[i] × cost_per_evaluation) -- **Space**: O(num_variables) per configuration + result storage -- **Practical limit**: ~20 binary variables - -### 12.2 Graph Representation - -Uses adjacency list internally (petgraph): - -- **Neighbor lookup**: O(degree) -- **Memory**: O(V + E) -- **Best for**: Sparse graphs - -### 12.3 Monomorphization - -Use concrete types to avoid dynamic dispatch: - -```rust -// Good: Monomorphized, fast -MaximumIndependentSet::::new(...) -MaximumIndependentSet::::new(...) - -// Also good: Generic function, monomorphized per call site -fn solve(p: &P) -> Option> { - BruteForce::new().find_best(p) -} -``` - ---- - -## 13. Known Limitations - -| Limitation | Implication | Workaround | -| --- | --- | --- | -| Vertex indices must be 0..n | No automatic remapping | Preprocess input | -| BruteForce solver is O(2^n) | Impractical for large instances | Use ILP solver (enabled by default in CLI, feature-gated in library) | -| No parallel evaluation | Single-threaded | Future: parallel config iteration | - ---- - -## Quick Reference - -### Creating Problems - -```rust -// Unit-weighted graph problem -let is = MaximumIndependentSet::::new(5, vec![(0,1), (1,2)]); - -// Weighted graph problem -let is = MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]); - -// SAT problem -let sat = Satisfiability::new(3, vec![ - CNFClause::new(vec![1, 2]), - CNFClause::new(vec![-1, 3]), - CNFClause::new(vec![-2, -3]), -]); -``` - -### Evaluating Solutions - -```rust -let config = vec![1, 0, 1, 0, 1]; -let result = problem.evaluate(&config); -match result { - SolutionSize::Valid(size) => println!("Size: {}", size), - SolutionSize::Invalid => println!("Infeasible"), -} -``` - -### Solving - -```rust -let solver = BruteForce::new(); - -// One optimal solution -if let Some(sol) = solver.find_best(&problem) { - println!("{:?}", sol); -} - -// All optimal solutions -let all = solver.find_all_best(&problem); -``` - -### Using Reductions - -```rust -let source = MaximumIndependentSet::::new(5, edges); -let reduction = source.reduce_to(); -let target = reduction.target_problem(); - -let solver = BruteForce::new(); -if let Some(target_sol) = solver.find_best(target) { - let source_sol = reduction.extract_solution(&target_sol); - println!("Source solution: {:?}", source_sol); -} -``` - -### Getting Problem Information - -```rust -let problem = MaximumIndependentSet::::new(3, vec![(0,1), (1,2)]); -println!("Name: {}", MaximumIndependentSet::::NAME); -println!("Variant: {:?}", MaximumIndependentSet::::variant()); -println!("Variables: {}", problem.num_variables()); -println!("Dims: {:?}", problem.dims()); -println!("Direction: {:?}", problem.direction()); -``` - -### CLI Quick Reference - -```bash -# Explore -pred list # all problem types -pred show MIS # problem details -pred path MIS QUBO # find reduction path - -# Create → Solve (one-liner) -pred create MIS --edges 0-1,1-2,2-3 | pred solve - - -# Create → Reduce → Solve -pred create MIS --edges 0-1,1-2,2-3 -o p.json -pred reduce p.json --to QUBO -o bundle.json -pred solve bundle.json - -# Evaluate a configuration -pred evaluate p.json --config 1,0,1,0 -``` - -## 14. FAQ & Troubleshooting - -### Common Questions - -#### Q: How do I choose between MaximumIndependentSet, MinimumVertexCover, and other graph problems? - -**A:** They're often equivalent via reductions: - -```text -Independent Set: Select vertices with no edges between them - - Goal: MAXIMIZE weight - - Constraint: No two selected vertices are adjacent - -Vertex Cover: Select vertices that touch all edges - - Goal: MINIMIZE weight - - Constraint: Every edge has at least one endpoint selected - -Clique: Select vertices that are ALL adjacent to each other - - Goal: MAXIMIZE weight - - Constraint: Every pair of selected vertices is adjacent - -Key insight: Their solutions are complements! - IS_solution = [1, 0, 1, 0] (vertices 0, 2 selected) - VC_solution = [0, 1, 0, 1] (vertices 1, 3 selected) - IS + VC = [1, 1, 1, 1] (all vertices) -``` - -**Choose based on your application**: - -- Independent Set: "find non-interfering items" (assignments, scheduling) -- Vertex Cover: "find minimum monitors" (surveillance, redundancy) -- Clique: "find communities" (social networks, dense subgraphs) - -#### Q: What's the difference between One and i32 weight type? - -**A:** - -```rust -// Both create problems with all weights = 1 -MaximumIndependentSet::::new(5, edges) -MaximumIndependentSet::::new(5, edges) // defaults to weight 1 - -// Difference is semantic and in the variant: -One: "This problem is inherently unweighted, all vertices equal" - variant() reports ("weight", "One") -i32: "This problem can have any integer weights" - variant() reports ("weight", "i32") - -// You can change weights in i32 version: -MaximumIndependentSet::::with_weights(5, edges, vec![1,2,3,4,5]) -``` - -**Use One when**: -- Problem is theoretically unweighted (pure complexity question) - -**Use i32 when**: -- Problem naturally has weights (generalization) -- You need flexibility - -#### Q: Why does reducing MaximumIndependentSet to MinimumVertexCover give a larger problem? - -**A:** In this case, it doesn't! It's a complement: - -```rust -// Same graph, same vertices -let is = MaximumIndependentSet::::new(5, edges); -let reduction: ReductionISToVC<_, _> = is.reduce_to(); - -// The reduced problem has the SAME structure -// We just flip 0→1 and 1→0 in the solution extraction -``` - -But in other reductions, overhead is real: - -```text -SAT (3 variables, 5 clauses) → MaximumIndependentSet - -Source: 3 variables, 5 clauses -Target: ~30 variables! (3 + 5*5) - Why? Each clause needs gadget vertices - -This is why some problems are "harder to solve": -- Direct brute force on SAT: 2^3 = 8 configurations -- After reduction: 2^30 = 1 billion configurations -``` - -#### Q: How do I know if a reduction is correct? - -**A:** Every reduction must pass the **closed-loop test**: - -```rust -#[test] -fn test_reduction_closed_loop() { - // 1. Create source problem - let source = SourceProblem::new(...); - - // 2. Get optimal solutions to source (direct) - let solver = BruteForce::new(); - let source_solutions = solver.find_all_best(&source); - - // 3. Reduce and solve target - let reduction = source.reduce_to(); - let target = reduction.target_problem(); - let target_solutions = solver.find_all_best(target); - - // 4. Extract and verify: target solutions map to valid source solutions - for target_sol in &target_solutions { - let extracted = reduction.extract_solution(target_sol); - let source_result = source.evaluate(&extracted); - assert!(source_result.is_valid()); - } - - // 5. Both should have same optimal value - // This proves the reduction preserves optimality -} -``` - -#### Q: Can I implement my own solver? - -**A:** Yes! Implement the `Solver` trait: - -```rust -use problemreductions::solvers::Solver; -use problemreductions::traits::{OptimizationProblem, Problem}; - -pub struct MyHeuristic { - max_iterations: usize, -} - -impl Solver for MyHeuristic { - fn find_best(&self, problem: &P) -> Option> { - // Your optimization algorithm here - None // Placeholder - } - - fn find_satisfying>(&self, problem: &P) -> Option> { - // Your satisfaction algorithm here - None // Placeholder - } -} -``` - -#### Q: What if I only want to work with certain graph types? - -**A:** You can constrain the generic parameter: - -```rust -// This function works with ANY graph topology -fn count_variables( - problem: &MaximumIndependentSet -) -> usize { - problem.num_variables() -} - -// This function works ONLY with SimpleGraph -fn my_special_solver( - problem: &MaximumIndependentSet -) -> Option> { - // Can access SimpleGraph-specific features here - let graph = problem.graph(); - // ... - None -} -``` - -#### Q: How do I serialize/deserialize problems? - -**A:** All problems implement `Serialize` and `Deserialize`: - -```rust -use serde_json; - -let problem = MaximumIndependentSet::::new(5, edges); - -// To JSON -let json = serde_json::to_string(&problem).unwrap(); - -// From JSON (if you know the type) -let loaded: MaximumIndependentSet = - serde_json::from_str(&json).unwrap(); -``` - -**Limitation**: Type information is lost. You must know the problem type when deserializing. - ---- - -### Debugging Tips - -#### Problem: "Variable out of bounds" panic - -**Cause**: You're creating edges or weights with invalid vertex indices. - -```rust -// WRONG: Vertex 5 doesn't exist (only 0-4) -let problem = MaximumIndependentSet::::new(5, vec![(0, 5)]); - -// CORRECT: Valid vertex indices -let problem = MaximumIndependentSet::::new(5, vec![(0, 4)]); -``` - -#### Problem: "Weights length must match" panic - -**Cause**: Weights vector has wrong length. - -```rust -let vertices = 5; -let edges = vec![(0, 1)]; - -// WRONG: 3 weights for 5 vertices -let p = MaximumIndependentSet::with_weights(vertices, edges, vec![1, 2, 3]); - -// CORRECT: 5 weights for 5 vertices -let p = MaximumIndependentSet::with_weights(vertices, edges, vec![1, 2, 3, 4, 5]); -``` - -#### Problem: Configuration returns Invalid even though it looks correct - -**Cause**: Likely constraint violation, not configuration validity. - -```rust -let problem = MaximumIndependentSet::::new( - 3, - vec![(0, 1), (1, 2)] // Path: 0-1-2 -); - -// INVALID: Both 0 and 1 selected, but edge (0,1) forbids this -let config = vec![1, 1, 0]; -let result = problem.evaluate(&config); -assert!(!result.is_valid()); // SolutionSize::Invalid - -// VALID: Vertices 0 and 2 are not adjacent -let config = vec![1, 0, 1]; -let result = problem.evaluate(&config); -assert!(result.is_valid()); // SolutionSize::Valid(2) -``` - -#### Problem: Reduction seems to lose solutions - -**Cause**: Check if your solution extraction is correct. - -```rust -// In your ReductionResult implementation: -impl ReductionResult for MyReduction { - type Source = SourceProblem; - type Target = TargetProblem; - - fn extract_solution(&self, target_sol: &[usize]) -> Vec { - // WRONG: Just return target solution unchanged - target_sol.to_vec() // Wrong for complement! - - // CORRECT: Apply proper transformation - target_sol.iter().map(|&x| 1 - x).collect() // Flip bits for complement - } -} -``` - ---- - -## 15. Complete End-to-End Example - -Here's a comprehensive example showing how all the pieces work together: - -```rust -use problemreductions::prelude::*; -use problemreductions::models::graph::MaximumIndependentSet; -use problemreductions::topology::SimpleGraph; -use problemreductions::solvers::BruteForce; - -fn main() { - // ============ STEP 1: Create the problem ============ - // We have a simple graph with 4 vertices and 3 edges - let problem = MaximumIndependentSet::::new( - 4, - vec![(0, 1), (1, 2), (2, 3)] // Linear chain: 0-1-2-3 - ); - - println!("=== Problem Definition ==="); - println!("Name: {}", MaximumIndependentSet::::NAME); - println!("Variant: {:?}", MaximumIndependentSet::::variant()); - println!("Variables: {}", problem.num_variables()); - println!("Dims: {:?}", problem.dims()); - println!("Direction: {:?}", problem.direction()); - - // ============ STEP 2: Manually test some configurations ============ - println!("\n=== Manual Configuration Tests ==="); - - // Configuration 1: Select only vertex 0 - let config1 = vec![1, 0, 0, 0]; - let result1 = problem.evaluate(&config1); - println!("Config [1,0,0,0]: {:?}", result1); // Valid(1) - - // Configuration 2: Select vertices 0 and 2 (non-adjacent) - let config2 = vec![1, 0, 1, 0]; - let result2 = problem.evaluate(&config2); - println!("Config [1,0,1,0]: {:?}", result2); // Valid(2) - - // Configuration 3: Select vertices 0 and 1 (adjacent - invalid!) - let config3 = vec![1, 1, 0, 0]; - let result3 = problem.evaluate(&config3); - println!("Config [1,1,0,0]: {:?}", result3); // Invalid - - // ============ STEP 3: Solve optimally using BruteForce ============ - println!("\n=== Solving with BruteForce ==="); - let solver = BruteForce::new(); - let optimal_solutions = solver.find_all_best(&problem); - - println!("Found {} optimal solutions:", optimal_solutions.len()); - for (i, solution) in optimal_solutions.iter().enumerate() { - let result = problem.evaluate(solution); - println!(" Solution {}: {:?} ({:?})", i, solution, result); - } - - // ============ STEP 4: Use reduction to solve via another problem ============ - println!("\n=== Solving via Reduction ==="); - let reduction: ReductionISToVC<_, _> = problem.reduce_to(); - let target_problem = reduction.target_problem(); - - println!("Reduced to MinimumVertexCover"); - println!("MVC variables: {}", target_problem.num_variables()); - println!("MVC direction: {:?}", target_problem.direction()); - - // Solve the target problem - let target_solutions = solver.find_all_best(target_problem); - println!("Found {} MVC solutions", target_solutions.len()); - - // Extract back to original problem - for (i, target_sol) in target_solutions.iter().enumerate() { - let extracted = reduction.extract_solution(target_sol); - let result = problem.evaluate(&extracted); - - println!(" MVC solution {}: {:?} → IS: {:?} ({:?})", - i, target_sol, extracted, result); - } - - // ============ STEP 5: Verify consistency ============ - println!("\n=== Verification ==="); - let direct_size = optimal_solutions.iter() - .filter_map(|sol| problem.evaluate(sol).size().copied()) - .max() - .unwrap_or(0); - - let via_reduction_size = target_solutions.iter() - .filter_map(|target_sol| { - let extracted = reduction.extract_solution(target_sol); - problem.evaluate(&extracted).size().copied() - }) - .max() - .unwrap_or(0); - - println!("Direct IS solve: {} vertices selected", direct_size); - println!("Via MVC reduction: {} vertices selected", via_reduction_size); - println!("Consistent: {}", direct_size == via_reduction_size); -} -``` - -### Understanding the Example - -1. **Problem Creation**: We create a MaximumIndependentSet problem on a 4-vertex path graph -2. **Manual Testing**: We evaluate three configurations to understand constraints -3. **Direct Solve**: Use BruteForce to find all optimal solutions -4. **Reduction**: Transform to MinimumVertexCover and solve via reduction -5. **Verification**: Check that both approaches agree - -**Key insights**: - -- MIS and MVC are **complement problems** on the same graph -- MIS **maximizes** (Direction::Maximize) selection -- MVC **minimizes** (Direction::Minimize) selection -- Solutions are related: IS_sol = NOT(VC_sol) -- Both methods find the same optimal value! - ---- - -## 16. CLI Tool (`pred`) - -The `pred` CLI tool provides a command-line interface for exploring NP-hard problem reductions without writing Rust code. It's published as a separate crate (`problemreductions-cli`, binary name `pred`). - -### 16.1 Installation - -```bash -# From crates.io (default: HiGHS ILP backend) -cargo install problemreductions-cli - -# With alternative ILP backends -cargo install problemreductions-cli --features coin-cbc -cargo install problemreductions-cli --features scip -cargo install problemreductions-cli --no-default-features --features clarabel - -# From source -make cli # builds target/release/pred -``` - -### 16.2 Global Flags - -All commands support these flags: - -| Flag | Description | -| --- | --- | -| `-o, --output ` | Save output as JSON to file (implies JSON mode) | -| `--json` | Output JSON to stdout instead of human-readable text | -| `-q, --quiet` | Suppress informational messages on stderr | - -### 16.3 Problem Aliases - -Short aliases are supported everywhere a problem name is expected: - -| Alias | Full Name | -| --- | --- | -| `MIS` | MaximumIndependentSet | -| `MVC` | MinimumVertexCover | -| `SAT` | Satisfiability | -| `3SAT` | KSatisfiability (K=3) | -| `KSAT` | KSatisfiability | -| `TSP` | TravelingSalesman | - -Unknown names trigger fuzzy-match suggestions. - -### 16.4 Graph Exploration - -#### `pred list` — List all problem types - -```bash -pred list # human-readable table -pred list --json | jq '.' # JSON output -``` - -#### `pred show ` — Problem details - -```bash -pred show MIS # variants, fields, reductions -pred show MIS/UnitDiskGraph # specific graph variant -``` - -#### `pred to` / `pred from` — Explore reduction neighbors - -```bash -pred to MIS # 1-hop: what MIS reduces to -pred to MIS --hops 2 # 2-hop reachable targets -pred from QUBO # what reduces to QUBO -pred from QUBO --hops 3 -``` - -Output is an ASCII tree visualization. - -#### `pred path ` — Find reduction paths - -```bash -pred path MIS QUBO # cheapest path -pred path MIS QUBO --all # all paths -pred path MIS QUBO -o path.json # save for use with `pred reduce --via` -pred path MIS QUBO --cost minimize:num_variables # custom cost function -``` - -Cost functions: -- `minimize-steps` (default) — fewest reduction hops -- `minimize:` — minimize a specific target size field (e.g., `num_variables`) - -#### `pred export-graph` — Export full reduction graph - -```bash -pred export-graph -o reduction_graph.json -``` - -### 16.5 Creating Problem Instances - -#### `pred create [OPTIONS]` - -**Graph problems** (MIS, MVC, MaxCut, MaxClique, MaximumMatching, MinimumDominatingSet, SpinGlass, TSP): - -```bash -pred create MIS --edges 0-1,1-2,2-3 -o problem.json -pred create MIS --edges 0-1,1-2 --weights 2,1,3 -o weighted.json -``` - -**SAT problems**: - -```bash -pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json -pred create 3SAT --num-vars 4 --clauses "1,2,3;-1,2,-3" -o 3sat.json -``` - -**QUBO**: - -```bash -pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json -``` - -**KColoring**: - -```bash -pred create KColoring --k 3 --edges 0-1,1-2,2-0 -o kcol.json -``` - -**Factoring**: - -```bash -pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json -``` - -**Random graph generation**: - -```bash -pred create MIS --random --num-vertices 10 --edge-prob 0.3 -pred create MIS --random --num-vertices 10 --seed 42 -o big.json -``` - -### 16.6 Evaluating and Inspecting - -#### `pred evaluate --config ` - -```bash -pred evaluate problem.json --config 1,0,1,0 -pred evaluate problem.json --config 1,0,1,0 -o result.json -``` - -#### `pred inspect ` - -Shows problem type, size metrics, available solvers, and reduction targets. - -```bash -pred inspect problem.json -pred inspect bundle.json -``` - -### 16.7 Solving - -#### `pred solve [OPTIONS]` - -```bash -pred solve problem.json # ILP solver (default, auto-reduces) -pred solve problem.json --solver brute-force # exhaustive search -pred solve problem.json --timeout 10 # abort after 10 seconds -pred solve bundle.json # solve a reduction bundle -``` - -The ILP solver auto-reduces non-ILP problems to ILP before solving. When given a reduction bundle (from `pred reduce`), it solves the target and maps the solution back. - -### 16.8 Reducing - -#### `pred reduce --to [OPTIONS]` - -```bash -pred reduce problem.json --to QUBO -o reduced.json -pred reduce problem.json --to ILP -o reduced.json -pred reduce problem.json --via path.json -o reduced.json # explicit route -``` - -Output is a reduction bundle containing source, target, and path metadata. Feed it to `pred solve` to solve and extract the original solution. - -### 16.9 Piping and Stdin - -All file-accepting commands support `-` for stdin: - -```bash -pred create MIS --edges 0-1,1-2 | pred solve - -pred create MIS --edges 0-1,1-2 | pred evaluate - --config 1,0,1 -pred create MIS --edges 0-1,1-2 | pred reduce - --to QUBO | pred solve - -pred create MIS --edges 0-1,1-2 | pred inspect - -``` - -### 16.10 Shell Completions - -```bash -# Auto-detect shell -eval "$(pred completions)" - -# Or specify: bash, zsh, fish -eval "$(pred completions zsh)" -``` - -### 16.11 End-to-End CLI Workflow - -```bash -# 1. Explore the reduction graph -pred show MIS -pred path MIS QUBO - -# 2. Create a problem instance -pred create MIS --edges 0-1,1-2,2-3,3-4,4-0 -o problem.json - -# 3. Solve directly (auto-reduces to ILP) -pred solve problem.json -o solution.json - -# 4. Or: explicit reduction + solve -pred reduce problem.json --to QUBO -o bundle.json -pred solve bundle.json --solver brute-force - -# 5. Verify a known configuration -pred evaluate problem.json --config 1,0,1,0,0 -``` - -### 16.12 JSON Output Formats - -All commands support `--json` or `-o` for structured output: - -- **Problem JSON**: `{"type": "...", "variant": {...}, "data": {...}}` -- **Reduction bundle**: `{"source": {...}, "target": {...}, "path": [...]}` -- **Solution JSON**: `{"problem": "...", "solver": "...", "solution": [...], "evaluation": "..."}` - ---- diff --git a/mydocs/RUST_FEATURES.md b/mydocs/RUST_FEATURES.md deleted file mode 100644 index 7d4bbb3a..00000000 --- a/mydocs/RUST_FEATURES.md +++ /dev/null @@ -1,1266 +0,0 @@ -# Rust Features Guide - -This guide explains all key Rust language features used in the problem-reductions library. Perfect for Rust newcomers to understand the codebase. - -## Table of Contents - -1. [Traits](#1-traits) -2. [Generics](#2-generics) -3. [Associated Types](#3-associated-types) -4. [Trait Bounds](#4-trait-bounds) -5. [PhantomData](#5-phantomdata) -6. [Type Aliases](#6-type-aliases) -7. [Enums](#7-enums) -8. [Pattern Matching](#8-pattern-matching) -9. [Derive Macros](#9-derive-macros) -10. [Declarative Macros](#10-declarative-macros) -11. [Modules and Visibility](#11-modules-and-visibility) -12. [Iterators](#12-iterators) -13. [Closures](#13-closures) -14. [Error Handling](#14-error-handling) -15. [Lifetimes](#15-lifetimes) -16. [Const and Static](#16-const-and-static) -17. [Marker Traits](#17-marker-traits) -18. [Builder Pattern](#18-builder-pattern) -19. [Serde Serialization](#19-serde-serialization) -20. [Common Standard Library Types](#20-common-standard-library-types) - ---- - -## 1. Traits - -Traits define shared behavior. They're similar to interfaces in other languages. - -### Basic Trait Definition - -```rust -// Define a trait with required methods -pub trait Problem { - fn num_variables(&self) -> usize; - fn num_flavors(&self) -> usize; -} - -// Implement the trait for a type -impl Problem for MyProblem { - fn num_variables(&self) -> usize { - self.variables.len() - } - - fn num_flavors(&self) -> usize { - 2 // Binary problem - } -} -``` - -### Provided Methods (Default Implementations) - -```rust -pub trait Problem { - fn num_variables(&self) -> usize; // Required - - // Provided method with default implementation - fn variables(&self) -> std::ops::Range { - 0..self.num_variables() - } -} -``` - -Implementors get `variables()` for free but can override it. - -### Trait Inheritance (Supertraits) - -```rust -// ConstraintSatisfactionProblem requires Problem to be implemented first -pub trait ConstraintSatisfactionProblem: Problem { - fn constraints(&self) -> Vec; -} -``` - -**Used in this library**: -- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` -- `src/solvers/mod.rs` - `Solver` -- `src/models/graph/template.rs` - `GraphConstraint` - ---- - -## 2. Generics - -Generics allow writing code that works with multiple types. - -### Generic Structs - -```rust -// W is a type parameter (defaults to i32) -pub struct GraphProblem { - weights: Vec, - // ... -} - -// Usage -let problem: GraphProblem = ...; -let problem: GraphProblem = ...; // Different weight type -let problem: GraphProblem = ...; // Uses default W = i32 -``` - -### Generic Functions - -```rust -// T is a type parameter -fn find_best(items: &[T]) -> Option<&T> -where - T: PartialOrd, // T must be comparable -{ - items.iter().max() -} -``` - -### Generic Impl Blocks - -```rust -// Implement for all W types that meet the bounds -impl GraphProblem { - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { - // ... - } -} -``` - -**Used in this library**: -- `GraphProblem` - generic over constraint type and weight type -- `SolutionSize` - generic over size type -- `Solver::find_best` - generic over problem type - ---- - -## 3. Associated Types - -Associated types are type placeholders in traits, defined by implementors. - -```rust -pub trait Problem { - // Associated type - implementor chooses the concrete type - type Size: Clone + PartialOrd; - - fn solution_size(&self, config: &[usize]) -> SolutionSize; -} - -// Implementation specifies the concrete type -impl Problem for IndependentSet { - type Size = i32; // This problem uses i32 for sizes - - fn solution_size(&self, config: &[usize]) -> SolutionSize { - // ... - } -} -``` - -### Why Associated Types vs Generics? - -```rust -// With associated type (cleaner) -fn solve(problem: &P) -> P::Size { ... } - -// With generics (more verbose) -fn solve, S>(problem: &P) -> S { ... } -``` - -Associated types are used when there's exactly one type that makes sense per implementation. - -**Used in this library**: -- `Problem::Size` - the objective value type - ---- - -## 4. Trait Bounds - -Trait bounds constrain generic types to those implementing certain traits. - -### Where Clauses - -```rust -impl Problem for GraphProblem -where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, -{ - // W must implement all these traits -} -``` - -### Multiple Bounds - -```rust -// Using + to require multiple traits -fn process(item: T) { ... } - -// Equivalent where clause (more readable for many bounds) -fn process(item: T) -where - T: Clone + Debug + Send, -{ ... } -``` - -### Common Trait Bounds in This Library - -| Bound | Meaning | -|-------|---------| -| `Clone` | Can be duplicated with `.clone()` | -| `Copy` | Can be copied implicitly (bit-by-bit) | -| `Default` | Has a default value via `Default::default()` | -| `PartialOrd` | Can be compared with `<`, `>`, etc. | -| `Send` | Safe to send between threads | -| `Sync` | Safe to share references between threads | -| `'static` | Contains no borrowed references (or they're `'static`) | - -**Used in this library**: -- `W: Clone + Default + PartialOrd + Num + Zero + AddAssign` -- `C: GraphConstraint` (which requires `Clone + Send + Sync + 'static`) - ---- - -## 5. PhantomData - -`PhantomData` is a zero-size type that tells the compiler "this struct logically owns a T". - -### The Problem - -```rust -// This won't compile - C is unused! -pub struct GraphProblem { - weights: Vec, - // C is not used anywhere in fields -} -``` - -### The Solution - -```rust -use std::marker::PhantomData; - -pub struct GraphProblem { - weights: Vec, - _constraint: PhantomData, // Zero-size, just marks that C is "used" -} - -// Creating PhantomData -let _constraint = PhantomData::; -// or -let _constraint: PhantomData = PhantomData; -``` - -### Why Use It? - -1. **Type safety**: Different `C` types create different `GraphProblem` types -2. **Zero runtime cost**: `PhantomData` has size 0 -3. **Compiler satisfaction**: Makes unused type parameters valid - -**Used in this library**: -- `GraphProblem` uses `PhantomData` to carry the constraint type - ---- - -## 6. Type Aliases - -Type aliases create new names for existing types. - -### Basic Alias - -```rust -// Create a shorter name -pub type IndependentSetT = GraphProblem; - -// Usage - these are equivalent: -let p1: GraphProblem = ...; -let p2: IndependentSetT = ...; -let p3: IndependentSetT = ...; // Uses default W = i32 -``` - -### Result Type Alias (Common Pattern) - -```rust -// In error.rs -pub type Result = std::result::Result; - -// Usage - cleaner than writing the full type -fn load_problem() -> Result { ... } -``` - -**Used in this library**: -- `IndependentSetT`, `VertexCoverT`, etc. -- `Result` in error handling - ---- - -## 7. Enums - -Enums define types that can be one of several variants. - -### Basic Enum - -```rust -pub enum EnergyMode { - LargerSizeIsBetter, // Maximization - SmallerSizeIsBetter, // Minimization -} - -// Usage -let mode = EnergyMode::LargerSizeIsBetter; -``` - -### Enum with Data - -```rust -pub enum ProblemCategory { - Graph(GraphSubcategory), // Contains a GraphSubcategory - Satisfiability(SatSubcategory), // Contains a SatSubcategory - Set(SetSubcategory), -} - -// Usage -let cat = ProblemCategory::Graph(GraphSubcategory::Independent); -``` - -### Enum Methods - -```rust -impl EnergyMode { - pub fn is_maximization(&self) -> bool { - matches!(self, EnergyMode::LargerSizeIsBetter) - } - - pub fn is_better(&self, a: &T, b: &T) -> bool { - match self { - EnergyMode::LargerSizeIsBetter => a > b, - EnergyMode::SmallerSizeIsBetter => a < b, - } - } -} -``` - -**Used in this library**: -- `EnergyMode` - optimization direction -- `ProblemCategory` - problem classification -- `ComplexityClass` - P, NP-complete, etc. -- `ProblemError` - error types - ---- - -## 8. Pattern Matching - -Pattern matching extracts data from enums and other types. - -### Match Expression - -```rust -match self.energy_mode() { - EnergyMode::LargerSizeIsBetter => { - // Maximization logic - } - EnergyMode::SmallerSizeIsBetter => { - // Minimization logic - } -} -``` - -### If Let (Single Pattern) - -```rust -// Only handle one variant -if let Some(weight) = self.weights.get(i) { - println!("Weight: {}", weight); -} - -// Equivalent to: -match self.weights.get(i) { - Some(weight) => println!("Weight: {}", weight), - None => {} -} -``` - -### Matches! Macro - -```rust -// Returns bool - cleaner than match for simple checks -pub fn is_hard(&self) -> bool { - matches!( - self, - ComplexityClass::NpComplete | ComplexityClass::NpHard - ) -} -``` - -### Destructuring - -```rust -// Extract data from enum variants -match category { - ProblemCategory::Graph(sub) => { - println!("Graph subcategory: {:?}", sub); - } - _ => {} // Wildcard - matches anything else -} - -// Destructure structs -let SolutionSize { size, is_valid } = problem.solution_size(&config); -``` - -**Used throughout the library** for handling enums and Option/Result types. - ---- - -## 9. Derive Macros - -Derive macros automatically implement traits for your types. - -### Common Derives - -```rust -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ComplexityClass { - P, - NpComplete, - NpHard, -} -``` - -| Derive | What It Does | -|--------|--------------| -| `Debug` | Enables `{:?}` formatting for printing | -| `Clone` | Adds `.clone()` method for deep copy | -| `Copy` | Enables implicit copying (for small types) | -| `PartialEq` | Enables `==` and `!=` comparison | -| `Eq` | Marks that equality is reflexive (a == a) | -| `Hash` | Enables use as HashMap key | -| `Default` | Adds `Default::default()` constructor | - -### Serde Derives - -```rust -use serde::{Serialize, Deserialize}; - -#[derive(Serialize, Deserialize)] -pub struct GraphProblem { - weights: Vec, - #[serde(skip)] // Don't serialize this field - _constraint: PhantomData, -} -``` - -**Used in this library**: -- Most types derive `Debug`, `Clone` -- Enums often derive `Copy`, `PartialEq`, `Eq`, `Hash` -- Problem types derive `Serialize`, `Deserialize` - ---- - -## 10. Declarative Macros - -Declarative macros (`macro_rules!`) generate code at compile time. - -### Basic Macro - -```rust -#[macro_export] // Makes macro available to users of the crate -macro_rules! quick_problem_test { - // Pattern to match - ( - $problem_type:ty, // Type - $constructor:ident($($args:expr),*), // Identifier and expressions - solution: [$($sol:expr),*], // Expressions - expected_size: $size:expr, - is_valid: $valid:expr - ) => { - // Code to generate - { - let problem = <$problem_type>::$constructor($($args),*); - let solution = vec![$($sol),*]; - let result = problem.solution_size(&solution); - assert_eq!(result.size, $size); - assert_eq!(result.is_valid, $valid); - } - }; -} -``` - -### Macro Fragment Types - -| Fragment | Matches | -|----------|---------| -| `$name:ident` | Identifier (variable/function name) | -| `$e:expr` | Expression | -| `$t:ty` | Type | -| `$p:pat` | Pattern | -| `$b:block` | Block `{ ... }` | -| `$s:stmt` | Statement | -| `$l:lifetime` | Lifetime `'a` | -| `$m:meta` | Attribute content | -| `$tt:tt` | Single token tree | - -### Repetition - -```rust -// $(...),* means "zero or more, comma-separated" -// $(...),+ means "one or more, comma-separated" -// $(...);* means "zero or more, semicolon-separated" - -macro_rules! create_vec { - ($($element:expr),*) => { - vec![$($element),*] - }; -} - -let v = create_vec![1, 2, 3]; // Expands to vec![1, 2, 3] -``` - -### Complex Macro Example - -```rust -#[macro_export] -macro_rules! graph_problem_tests { - ( - problem_type: $problem:ty, - constraint_type: $constraint:ty, - test_cases: [ - $( - ($name:ident, $n:expr, [$($edge:expr),*], [$($sol:expr),*], $size:expr, $is_max:expr) - ),* $(,)? // Optional trailing comma - ] - ) => { - mod generated_tests { - use super::*; - - $( // Repeat for each test case - mod $name { - use super::*; - - #[test] - fn test_creation() { - let problem = <$problem>::new($n, vec![$($edge),*]); - assert_eq!(problem.num_variables(), $n); - } - - #[test] - fn test_solution() { - let problem = <$problem>::new($n, vec![$($edge),*]); - let solution = vec![$($sol),*]; - let result = problem.solution_size(&solution); - assert_eq!(result.size, $size); - } - } - )* - } - }; -} -``` - -**Used in this library**: -- `src/testing/macros.rs` - `graph_problem_tests!`, `complement_test!`, `quick_problem_test!` - ---- - -## 11. Modules and Visibility - -Rust organizes code into modules with explicit visibility. - -### Module Declaration - -```rust -// In lib.rs or main.rs -pub mod models; // Load from models/mod.rs or models.rs -pub mod solvers; -mod internal; // Private module (no pub) - -// In models/mod.rs -pub mod graph; // Load from models/graph/mod.rs -pub mod sat; -``` - -### Visibility Modifiers - -| Modifier | Meaning | -|----------|---------| -| (none) | Private to current module | -| `pub` | Public to everyone | -| `pub(crate)` | Public within this crate only | -| `pub(super)` | Public to parent module | -| `pub(in path)` | Public to specific path | - -```rust -pub struct MyStruct { - pub public_field: i32, - private_field: i32, // Private - pub(crate) crate_field: i32, // Crate-public -} -``` - -### Re-exports - -```rust -// In lib.rs - make nested items available at crate root -pub use models::graph::IndependentSetT; -pub use solvers::BruteForce; - -// Users can now write: -use problemreductions::IndependentSetT; -// Instead of: -use problemreductions::models::graph::IndependentSetT; -``` - -### Prelude Pattern - -```rust -// In prelude.rs - common imports bundled together -pub use crate::traits::{Problem, ConstraintSatisfactionProblem}; -pub use crate::types::{EnergyMode, SolutionSize}; -pub use crate::solvers::BruteForce; - -// Users import everything at once: -use problemreductions::prelude::*; -``` - -**Used in this library**: -- `src/lib.rs` - module declarations and re-exports -- `src/prelude.rs` - common imports -- Each subdirectory has `mod.rs` for organization - ---- - -## 12. Iterators - -Iterators provide a way to process sequences of elements. - -### Iterator Trait - -```rust -pub trait Iterator { - type Item; - fn next(&mut self) -> Option; - // Many provided methods... -} -``` - -### Common Iterator Methods - -```rust -let numbers = vec![1, 2, 3, 4, 5]; - -// map - transform each element -let doubled: Vec = numbers.iter().map(|x| x * 2).collect(); - -// filter - keep elements matching predicate -let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect(); - -// fold - accumulate into single value -let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); - -// enumerate - add indices -for (i, x) in numbers.iter().enumerate() { - println!("Index {}: {}", i, x); -} - -// any/all - boolean checks -let has_five = numbers.iter().any(|&x| x == 5); -let all_positive = numbers.iter().all(|&x| x > 0); - -// find - get first matching element -let first_even = numbers.iter().find(|&&x| x % 2 == 0); - -// chain - combine iterators -let combined: Vec = vec![1, 2].into_iter() - .chain(vec![3, 4].into_iter()) - .collect(); -``` - -### iter() vs into_iter() vs iter_mut() - -```rust -let mut v = vec![1, 2, 3]; - -// iter() - borrows, yields &T -for x in v.iter() { /* x is &i32 */ } - -// iter_mut() - mutable borrow, yields &mut T -for x in v.iter_mut() { *x += 1; } - -// into_iter() - takes ownership, yields T -for x in v.into_iter() { /* x is i32, v is consumed */ } -``` - -### Custom Iterator - -```rust -pub struct ConfigIterator { - current: usize, - total: usize, - num_variables: usize, - num_flavors: usize, -} - -impl Iterator for ConfigIterator { - type Item = Vec; - - fn next(&mut self) -> Option { - if self.current >= self.total { - return None; - } - let config = index_to_config(self.current, self.num_variables, self.num_flavors); - self.current += 1; - Some(config) - } -} -``` - -**Used in this library**: -- `ConfigIterator` for enumerating all configurations -- Extensive use of iterator combinators in solvers and constraint evaluation - ---- - -## 13. Closures - -Closures are anonymous functions that can capture their environment. - -### Basic Closure - -```rust -// Closure with inferred types -let add = |a, b| a + b; -let result = add(2, 3); // 5 - -// Closure with explicit types -let add: fn(i32, i32) -> i32 = |a, b| a + b; - -// Multi-line closure -let complex = |x| { - let y = x * 2; - y + 1 -}; -``` - -### Capturing Variables - -```rust -let factor = 2; - -// Borrow by reference (Fn trait) -let multiply = |x| x * factor; - -// Borrow mutably (FnMut trait) -let mut count = 0; -let mut increment = || { count += 1; }; - -// Take ownership (FnOnce trait) -let data = vec![1, 2, 3]; -let consume = move || { - println!("{:?}", data); - // data is now owned by the closure -}; -``` - -### Closures as Arguments - -```rust -// Using iterator methods -let doubled: Vec = numbers.iter() - .map(|x| x * 2) // Closure as argument - .filter(|x| x > &5) // Another closure - .collect(); - -// Custom function taking closure -fn apply_twice(f: F, x: i32) -> i32 -where - F: Fn(i32) -> i32, // F is a closure that takes and returns i32 -{ - f(f(x)) -} -``` - -**Used in this library**: -- Iterator chains in constraint evaluation -- Map/filter operations on solutions - ---- - -## 14. Error Handling - -Rust uses `Result` and `Option` for error handling instead of exceptions. - -### Option Type - -```rust -enum Option { - Some(T), - None, -} - -// Usage -fn find_weight(&self, index: usize) -> Option<&W> { - self.weights.get(index) // Returns None if out of bounds -} - -// Handling Option -match result { - Some(value) => println!("Found: {}", value), - None => println!("Not found"), -} - -// Shortcuts -let value = result.unwrap(); // Panics if None -let value = result.unwrap_or(default); // Use default if None -let value = result.expect("message"); // Panics with message if None -let value = result?; // Return None early if None -``` - -### Result Type - -```rust -enum Result { - Ok(T), - Err(E), -} - -// Usage -fn parse_config(s: &str) -> Result { - if s.is_empty() { - return Err(ParseError::Empty); - } - Ok(Config::new(s)) -} - -// Handling Result -match result { - Ok(value) => println!("Success: {:?}", value), - Err(e) => println!("Error: {:?}", e), -} - -// The ? operator - propagate errors -fn load_and_process() -> Result { - let config = parse_config(input)?; // Returns Err early if error - let data = load_data(&config)?; - Ok(process(data)) -} -``` - -### thiserror Crate - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ProblemError { - #[error("invalid configuration size: expected {expected}, got {got}")] - InvalidConfigSize { expected: usize, got: usize }, - - #[error("invalid problem: {0}")] - InvalidProblem(String), -} -``` - -### Panic vs Result - -```rust -// Use panic for programming errors (bugs) -assert_eq!(weights.len(), num_vertices); // Panics if false - -// Use Result for recoverable errors -fn load_file(path: &str) -> Result { ... } -``` - -**Used in this library**: -- `ProblemError` enum with `thiserror` -- `assert!` and `assert_eq!` for invariant checking -- `Option` for optional weights and metadata - ---- - -## 15. Lifetimes - -Lifetimes ensure references are valid for as long as they're used. - -### Basic Lifetime Annotation - -```rust -// 'a is a lifetime parameter -fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { - if x.len() > y.len() { x } else { y } -} -// Return value lives as long as both inputs -``` - -### Lifetime in Structs - -```rust -// Struct containing a reference needs lifetime -struct ProblemRef<'a> { - data: &'a [usize], -} - -impl<'a> ProblemRef<'a> { - fn new(data: &'a [usize]) -> Self { - Self { data } - } -} -``` - -### Static Lifetime - -```rust -// 'static means the reference lives for the entire program -const NAME: &'static str = "Independent Set"; - -// Often written without 'static (it's inferred for string literals) -const NAME: &str = "Independent Set"; -``` - -### Lifetime Elision - -Rust infers lifetimes in common cases: - -```rust -// These are equivalent: -fn first(s: &str) -> &str { ... } -fn first<'a>(s: &'a str) -> &'a str { ... } - -// Rules: -// 1. Each input reference gets its own lifetime -// 2. If one input reference, output gets same lifetime -// 3. If &self or &mut self, output gets self's lifetime -``` - -**Used in this library**: -- `&'static str` for constant strings in `ProblemInfo` -- `&'static [&'static str]` for aliases arrays -- Mostly elided (automatic) in method signatures - ---- - -## 16. Const and Static - -### Const - -Compile-time constants, inlined everywhere they're used. - -```rust -impl GraphConstraint for IndependentSetConstraint { - const NAME: &'static str = "Independent Set"; - const ENERGY_MODE: EnergyMode = EnergyMode::LargerSizeIsBetter; -} - -// In traits - associated constants -pub trait GraphConstraint { - const NAME: &'static str; // Must be provided by implementor - const ALIASES: &'static [&'static str] = &[]; // Default value -} -``` - -### Const Fn - -Functions that can be evaluated at compile time. - -```rust -impl ProblemInfo { - // Can be called in const context - pub const fn new(name: &'static str, description: &'static str) -> Self { - Self { - name, - description, - complexity_class: ComplexityClass::NpComplete, - // ... - } - } - - pub const fn with_complexity(mut self, class: ComplexityClass) -> Self { - self.complexity_class = class; - self - } -} - -// Can create ProblemInfo at compile time -const MY_INFO: ProblemInfo = ProblemInfo::new("My Problem", "Description") - .with_complexity(ComplexityClass::NpComplete); -``` - -### Static - -Global variables with a fixed memory address. - -```rust -// Mutable static requires unsafe -static mut COUNTER: usize = 0; - -// Prefer const or lazy_static for most cases -``` - -**Used in this library**: -- Associated `const` in `GraphConstraint` trait -- `const fn` builder methods in `ProblemInfo` -- `&'static str` for string constants - ---- - -## 17. Marker Traits - -Marker traits indicate properties without providing methods. - -### Send and Sync - -```rust -// Send: Safe to transfer ownership between threads -// Sync: Safe to share references between threads (&T is Send) - -pub trait GraphConstraint: Clone + Send + Sync + 'static { - // Implementations must be thread-safe -} -``` - -### Sized - -```rust -// Most types are Sized (known size at compile time) -// ?Sized allows dynamically-sized types (like trait objects) -fn process(value: &T) { ... } -``` - -### Copy - -```rust -#[derive(Copy, Clone)] -pub struct Point { - x: i32, - y: i32, -} - -// Copy types are implicitly copied (not moved) -let p1 = Point { x: 1, y: 2 }; -let p2 = p1; // p1 is copied, both are valid -``` - -**Used in this library**: -- `GraphConstraint: Send + Sync + 'static` for thread safety -- Small enums derive `Copy` (e.g., `ComplexityClass`, `EnergyMode`) - ---- - -## 18. Builder Pattern - -A pattern for constructing complex objects step by step. - -### Basic Builder - -```rust -pub struct ProblemInfo { - pub name: &'static str, - pub description: &'static str, - pub complexity_class: ComplexityClass, - pub aliases: &'static [&'static str], -} - -impl ProblemInfo { - // Start with required fields - pub const fn new(name: &'static str, description: &'static str) -> Self { - Self { - name, - description, - complexity_class: ComplexityClass::Unknown, - aliases: &[], - } - } - - // Builder methods return Self for chaining - pub const fn with_complexity(mut self, class: ComplexityClass) -> Self { - self.complexity_class = class; - self - } - - pub const fn with_aliases(mut self, aliases: &'static [&'static str]) -> Self { - self.aliases = aliases; - self - } -} - -// Usage - method chaining -let info = ProblemInfo::new("My Problem", "Description") - .with_complexity(ComplexityClass::NpComplete) - .with_aliases(&["MP", "MyProb"]); -``` - -**Used in this library**: -- `ProblemInfo` builder for metadata -- `GraphTestCase` builder for test cases - ---- - -## 19. Serde Serialization - -Serde provides automatic serialization/deserialization. - -### Basic Usage - -```rust -use serde::{Serialize, Deserialize}; - -#[derive(Serialize, Deserialize)] -pub struct Problem { - name: String, - size: usize, -} - -// Serialize to JSON -let json = serde_json::to_string(&problem)?; - -// Deserialize from JSON -let problem: Problem = serde_json::from_str(&json)?; -``` - -### Field Attributes - -```rust -#[derive(Serialize, Deserialize)] -pub struct GraphProblem { - weights: Vec, - - #[serde(skip)] // Don't serialize this field - _constraint: PhantomData, - - #[serde(default)] // Use Default if missing - optional_field: Option, - - #[serde(rename = "numVertices")] // Different name in JSON - num_vertices: usize, -} -``` - -### Custom Serialization - -```rust -use serde::{Serializer, Deserializer}; - -#[derive(Serialize, Deserialize)] -pub struct MyType { - #[serde(serialize_with = "serialize_special")] - special_field: SpecialType, -} - -fn serialize_special(value: &SpecialType, serializer: S) -> Result -where - S: Serializer, -{ - // Custom serialization logic - serializer.serialize_str(&value.to_string()) -} -``` - -**Used in this library**: -- Problem types derive `Serialize, Deserialize` -- `#[serde(skip)]` for `PhantomData` fields -- Category enums are serializable - ---- - -## 20. Common Standard Library Types - -### Vec - -Dynamic array. - -```rust -let mut v: Vec = Vec::new(); -v.push(1); -v.push(2); - -// Macro shorthand -let v = vec![1, 2, 3]; - -// Access -let first = v[0]; -let maybe = v.get(0); // Returns Option<&T> - -// Iterate -for x in &v { ... } -for x in v.iter() { ... } -for x in v.into_iter() { ... } // Consumes v -``` - -### HashMap - -Key-value store. - -```rust -use std::collections::HashMap; - -let mut map = HashMap::new(); -map.insert("key", 42); - -let value = map.get("key"); // Option<&V> -let value = map["key"]; // Panics if missing - -for (key, value) in &map { ... } -``` - -### Range - -```rust -// Range types -0..5 // 0, 1, 2, 3, 4 (exclusive end) -0..=5 // 0, 1, 2, 3, 4, 5 (inclusive end) -..5 // RangeTo -5.. // RangeFrom -.. // RangeFull - -// Usage -for i in 0..5 { ... } -let slice = &array[1..3]; -``` - -### String vs &str - -```rust -// &str - string slice, borrowed, immutable -let s: &str = "hello"; - -// String - owned, growable -let s: String = String::from("hello"); -let s: String = "hello".to_string(); - -// Convert -let slice: &str = &owned_string; -let owned: String = slice.to_string(); -``` - -### Box - -Heap allocation for single values. - -```rust -// Allocate on heap -let boxed: Box = Box::new(5); - -// Useful for recursive types -enum List { - Cons(i32, Box), - Nil, -} -``` - ---- - -## Summary: Most Important Features - -For extending this library, focus on these features: - -1. **Traits** - Define shared behavior (`Problem`, `GraphConstraint`) -2. **Generics** - Write flexible code (`GraphProblem`) -3. **Associated Types** - Type placeholders in traits (`Problem::Size`) -4. **Trait Bounds** - Constrain generic types (`W: Clone + Default`) -5. **PhantomData** - Carry unused type parameters -6. **Type Aliases** - Convenient names (`IndependentSetT`) -7. **Enums** - Multiple variants (`EnergyMode`, `ProblemCategory`) -8. **Derive Macros** - Auto-implement traits (`#[derive(Debug, Clone)]`) -9. **Declarative Macros** - Code generation (`graph_problem_tests!`) -10. **Iterators** - Process collections functionally - -## Further Reading - -- [The Rust Book](https://doc.rust-lang.org/book/) -- [Rust By Example](https://doc.rust-lang.org/rust-by-example/) -- [Rustlings](https://github.com/rust-lang/rustlings) - Small exercises -- [std documentation](https://doc.rust-lang.org/std/) diff --git a/problemreductions-cli/src/commands/reduce.rs b/problemreductions-cli/src/commands/reduce.rs index 211560d8..53032303 100644 --- a/problemreductions-cli/src/commands/reduce.rs +++ b/problemreductions-cli/src/commands/reduce.rs @@ -34,7 +34,10 @@ fn load_path_file(path_file: &Path) -> Result { anyhow::bail!("Path file must contain at least one reduction step"); } - Ok(ReductionPath { steps }) + Ok(ReductionPath { + steps, + overheads: vec![], + }) } fn parse_path_node(node: &serde_json::Value) -> Result { diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index 1a06352c..63b8ae0b 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -45,13 +45,9 @@ pub fn solve(input: &Path, solver_name: &str, timeout: u64, out: &OutputConfig) let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let result = match parsed { - SolveInput::Problem(pj) => solve_problem( - &pj.problem_type, - &pj.variant, - pj.data, - &solver_name, - &out, - ), + SolveInput::Problem(pj) => { + solve_problem(&pj.problem_type, &pj.variant, pj.data, &solver_name, &out) + } SolveInput::Bundle(b) => solve_bundle(b, &solver_name, &out), }; tx.send(result).ok(); @@ -167,6 +163,7 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) variant: s.variant.clone(), }) .collect(), + overheads: vec![], }; let chain = graph diff --git a/src/lib.rs b/src/lib.rs index 75079f4f..775b7e30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ pub mod config; pub mod error; pub mod export; pub mod expr; -pub mod graph_types; pub mod io; pub mod models; diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index 04e89f25..54f82606 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -16,7 +16,7 @@ use crate::models::optimization::{LinearConstraint, ObjectiveSense, VarBounds, ILP}; use crate::models::specialized::{BooleanExpr, BooleanOp, CircuitSAT}; -use crate::poly; + use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -175,8 +175,8 @@ impl ILPBuilder { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", poly!(num_variables) + poly!(num_assignments)), - ("num_constraints", poly!(num_variables) + poly!(num_assignments)), + ("num_vars", "num_variables + num_assignments"), + ("num_constraints", "num_variables + num_assignments"), ]) } )] diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index b250bc5a..89a8c68e 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -124,8 +124,8 @@ fn reduce_kcoloring_to_ilp( #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_vars", "num_vertices * num_colors"), - ("num_constraints", "num_vertices + num_edges * num_colors"), + ("num_vars", "num_vertices ^ 2"), + ("num_constraints", "num_vertices + num_vertices * num_edges"), ]) } )] diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index 4d6f20f1..d436eac3 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -106,7 +106,7 @@ fn reduce_kcoloring_to_qubo( // Register only the KN variant in the reduction graph #[reduction( - overhead = { ReductionOverhead::new(vec![("num_vars", "num_vertices * num_colors")]) } + overhead = { ReductionOverhead::new(vec![("num_vars", "num_vertices ^ 2")]) } )] impl ReduceTo> for KColoring { type Result = ReductionKColoringToQUBO; diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 3d44ce42..13705adf 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -513,7 +513,10 @@ impl ReductionGraph { let edge_cost = cost_fn.edge_cost(overhead, ¤t_size); let new_cost = cost.0 + edge_cost; - let new_size = overhead.evaluate_output_size(¤t_size); + let new_size = match overhead.evaluate_output_size(¤t_size) { + Ok(s) => s, + Err(_) => continue, + }; if new_cost < *costs.get(&next).unwrap_or(&f64::INFINITY) { costs.insert(next, new_cost); diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index d43ccc93..c785d4fb 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -15,7 +15,7 @@ //! minimize Σ_i Q_ii · x_i + Σ_{i HashSet<&'static str> { + pub fn input_variable_names(&self) -> HashSet<&str> { self.output_size .iter() .flat_map(|(_, expr)| expr.variable_names()) diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index b82084f8..1cd199e8 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -5,7 +5,7 @@ use crate::models::satisfiability::Satisfiability; use crate::models::specialized::{Assignment, BooleanExpr, Circuit, CircuitSAT}; -use crate::poly; + use crate::reduction; use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -38,8 +38,8 @@ impl ReductionResult for ReductionSATToCircuit { #[reduction( overhead = { ReductionOverhead::new(vec![ - ("num_variables", poly!(num_vars) + poly!(num_clauses) + poly!(1)), - ("num_assignments", poly!(num_clauses) + poly!(2)), + ("num_variables", "num_vars + num_clauses + 1"), + ("num_assignments", "num_clauses + 2"), ]) } )] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index ed29b5f4..7e28a3e9 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -1,7 +1,6 @@ //! Tests for ReductionGraph: discovery, path finding, and typed API. use crate::models::satisfiability::KSatisfiability; -use crate::poly; use crate::prelude::*; use crate::rules::{MinimizeSteps, ReductionGraph, TraversalDirection}; use crate::topology::{SimpleGraph, TriangularSubgraph}; @@ -327,37 +326,34 @@ fn test_3sat_to_mis_triangular_overhead() { assert_eq!(edges.len(), 3); // Edge 0: K3SAT → SAT (identity) + assert_eq!(edges[0].get("num_vars").unwrap().to_string(), "num_vars"); assert_eq!( - edges[0].get("num_vars").unwrap().normalized(), - poly!(num_vars) + edges[0].get("num_clauses").unwrap().to_string(), + "num_clauses" ); assert_eq!( - edges[0].get("num_clauses").unwrap().normalized(), - poly!(num_clauses) - ); - assert_eq!( - edges[0].get("num_literals").unwrap().normalized(), - poly!(num_literals) + edges[0].get("num_literals").unwrap().to_string(), + "num_literals" ); // Edge 1: SAT → MIS{SimpleGraph,i32} assert_eq!( - edges[1].get("num_vertices").unwrap().normalized(), - poly!(num_literals) + edges[1].get("num_vertices").unwrap().to_string(), + "num_literals" ); assert_eq!( - edges[1].get("num_edges").unwrap().normalized(), - poly!(num_literals ^ 2) + edges[1].get("num_edges").unwrap().to_string(), + "num_literals ^ 2" ); // Edge 2: MIS{SimpleGraph,i32} → MIS{TriangularSubgraph,i32} assert_eq!( - edges[2].get("num_vertices").unwrap().normalized(), - poly!(num_vertices ^ 2) + edges[2].get("num_vertices").unwrap().to_string(), + "num_vertices * num_vertices" ); assert_eq!( - edges[2].get("num_edges").unwrap().normalized(), - poly!(num_vertices ^ 2) + edges[2].get("num_edges").unwrap().to_string(), + "num_vertices * num_vertices" ); // Compose overheads symbolically along the path. @@ -370,12 +366,12 @@ fn test_3sat_to_mis_triangular_overhead() { // Composed: num_vertices = L², num_edges = L² let composed = graph.compose_path_overhead(&path); assert_eq!( - composed.get("num_vertices").unwrap().normalized(), - poly!(num_literals ^ 2) + composed.get("num_vertices").unwrap().to_string(), + "num_literals * num_literals" ); assert_eq!( - composed.get("num_edges").unwrap().normalized(), - poly!(num_literals ^ 2) + composed.get("num_edges").unwrap().to_string(), + "num_literals * num_literals" ); } @@ -387,8 +383,8 @@ fn test_validate_overhead_variables_valid() { use crate::rules::validate_overhead_variables; let overhead = ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vars)), - ("num_edges", poly!(num_vars ^ 2)), + ("num_vertices", "num_vars"), + ("num_edges", "num_vars ^ 2"), ]); // Should not panic: inputs {num_vars} ⊆ source, outputs {num_vertices, num_edges} ⊆ target validate_overhead_variables( @@ -406,7 +402,7 @@ fn test_validate_overhead_variables_missing_input() { use crate::rules::registry::ReductionOverhead; use crate::rules::validate_overhead_variables; - let overhead = ReductionOverhead::new(vec![("num_vertices", poly!(num_colors))]); + let overhead = ReductionOverhead::new(vec![("num_vertices", "num_colors")]); validate_overhead_variables( "Source", "Target", @@ -422,7 +418,7 @@ fn test_validate_overhead_variables_missing_output() { use crate::rules::registry::ReductionOverhead; use crate::rules::validate_overhead_variables; - let overhead = ReductionOverhead::new(vec![("num_gates", poly!(num_vars))]); + let overhead = ReductionOverhead::new(vec![("num_gates", "num_vars")]); validate_overhead_variables( "Source", "Target", @@ -437,7 +433,7 @@ fn test_validate_overhead_variables_skips_output_when_empty() { use crate::rules::registry::ReductionOverhead; use crate::rules::validate_overhead_variables; - let overhead = ReductionOverhead::new(vec![("anything", poly!(num_vars))]); + let overhead = ReductionOverhead::new(vec![("anything", "num_vars")]); // Should not panic: target_size_names is empty so output check is skipped validate_overhead_variables("Source", "Target", &overhead, &["num_vars"], &[]); }