diff --git a/Cargo.lock b/Cargo.lock index ed7e189..fdf8142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -509,6 +518,15 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -575,7 +593,9 @@ dependencies = [ "bitvec", "criterion", "good_lp", + "inventory", "num-traits", + "ordered-float", "petgraph", "proptest", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index 32cb5de..d300b13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ serde_json = "1.0" thiserror = "1.0" num-traits = "0.2" good_lp = { version = "1.8", default-features = false, features = ["highs"], optional = true } +inventory = "0.3" +ordered-float = "5.0" [dev-dependencies] rand = "0.8" diff --git a/docs/plans/2026-01-26-set-theoretic-reductions-design.md b/docs/plans/2026-01-26-set-theoretic-reductions-design.md new file mode 100644 index 0000000..6f2a024 --- /dev/null +++ b/docs/plans/2026-01-26-set-theoretic-reductions-design.md @@ -0,0 +1,842 @@ +# Set-Theoretic Reduction Path Finding + +## Overview + +This design addresses issues #10 and #11 by introducing: + +1. **Parametric Problem Modeling** - Problems carry graph type and weight type as parameters +2. **Set-Theoretic Path Finding** - Reduction rules apply when source ⊆ rule source AND rule target ⊆ target +3. **Automatic Registration** - Using `inventory` crate for distributed reduction registration +4. **Polynomial Overhead** - Output size expressed as polynomials over named input variables +5. **Customizable Cost Functions** - User-defined optimization goals for path finding + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Subsumption model | Graph topology + weight types combined | Most rigorous, catches invalid paths | +| Graph hierarchy | Trait markers with inventory registration | Compile-time safety + runtime queries | +| Weight hierarchy | Standard `From` trait | Leverages existing Rust ecosystem | +| Registration | `inventory` crate | Simple, works across crate boundaries | +| Overhead model | Polynomial with integer exponents | Sufficient for v1, captures most cases | +| Cost optimization | User-provided cost function trait | Different solvers have different needs | + +--- + +## 1. Type Hierarchy + +### 1.1 Graph Type Markers + +```rust +/// Marker trait for graph types +pub trait GraphMarker: 'static { + const NAME: &'static str; +} + +/// Compile-time subtype relationship +pub trait GraphSubtype: GraphMarker {} + +// Concrete graph types +pub struct SimpleGraph; +pub struct PlanarGraph; +pub struct UnitDiskGraph; +pub struct BipartiteGraph; + +impl GraphMarker for SimpleGraph { const NAME: &'static str = "SimpleGraph"; } +impl GraphMarker for PlanarGraph { const NAME: &'static str = "PlanarGraph"; } +impl GraphMarker for UnitDiskGraph { const NAME: &'static str = "UnitDiskGraph"; } +impl GraphMarker for BipartiteGraph { const NAME: &'static str = "BipartiteGraph"; } +``` + +### 1.2 Graph Subtype Registration + +```rust +pub struct GraphSubtypeEntry { + pub subtype: &'static str, + pub supertype: &'static str, +} + +inventory::collect!(GraphSubtypeEntry); + +/// Declare both trait impl and runtime registration +macro_rules! declare_graph_subtype { + ($sub:ty => $sup:ty) => { + impl GraphSubtype<$sup> for $sub {} + inventory::submit!(GraphSubtypeEntry { + subtype: <$sub as GraphMarker>::NAME, + supertype: <$sup as GraphMarker>::NAME, + }); + }; +} + +// Hierarchy declarations +declare_graph_subtype!(UnitDiskGraph => PlanarGraph); +declare_graph_subtype!(UnitDiskGraph => SimpleGraph); +declare_graph_subtype!(PlanarGraph => SimpleGraph); +declare_graph_subtype!(BipartiteGraph => SimpleGraph); +``` + +### 1.3 Weight Types + +```rust +/// Marker for numeric weight types +pub trait NumericWeight: Clone + 'static {} + +impl NumericWeight for bool {} +impl NumericWeight for i32 {} +impl NumericWeight for i64 {} +impl NumericWeight for f32 {} +impl NumericWeight for f64 {} + +// Weight subsumption uses standard From trait: +// i32 → f64 valid (From for f64 exists) +// f64 → i32 invalid (no From impl) +``` + +--- + +## 2. Parametric Problem Modeling + +### 2.1 Problem Definition + +```rust +pub trait Problem { + type GraphType: GraphMarker; + type Weight: NumericWeight; + type Size: Ord; + + const NAME: &'static str; + + fn problem_size(&self) -> ProblemSize; + fn num_variables(&self) -> usize; + fn num_flavors(&self) -> usize; + fn energy_mode(&self) -> EnergyMode; + fn solution_size(&self, config: &[usize]) -> SolutionSize; +} +``` + +### 2.2 Problem Types with Parameters + +```rust +/// Independent Set with graph type and weight type +pub struct IndependentSet { + graph: SimpleWeightedGraph, + _phantom: PhantomData, +} + +impl Problem for IndependentSet { + type GraphType = G; + type Weight = W; + type Size = W; + + const NAME: &'static str = "IndependentSet"; + + fn problem_size(&self) -> ProblemSize { + ProblemSize::new() + .with("n", self.graph.num_vertices()) + .with("m", self.graph.num_edges()) + } +} + +/// SpinGlass with graph type and weight type +pub struct SpinGlass { + couplings: Vec<(usize, usize, W)>, + fields: Vec, + _phantom: PhantomData, +} + +/// Satisfiability (no graph type, just weight) +pub struct Satisfiability { + clauses: Vec, + num_variables: usize, + _phantom: PhantomData, +} + +impl Problem for Satisfiability { + type GraphType = SimpleGraph; // Default for non-graph problems + type Weight = W; + + fn problem_size(&self) -> ProblemSize { + ProblemSize::new() + .with("v", self.num_variables) + .with("c", self.clauses.len()) + } +} +``` + +### 2.3 ProblemSize with Named Fields + +```rust +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct ProblemSize { + fields: HashMap<&'static str, usize>, +} + +impl ProblemSize { + pub fn new() -> Self { + Self { fields: HashMap::new() } + } + + pub fn with(mut self, name: &'static str, value: usize) -> Self { + self.fields.insert(name, value); + self + } + + pub fn get(&self, name: &str) -> usize { + self.fields.get(name).copied().unwrap_or(0) + } +} +``` + +--- + +## 3. Polynomial Overhead Model + +### 3.1 Polynomial Representation + +```rust +/// A monomial: coefficient * product of (variable^exponent) +#[derive(Clone, Debug)] +pub struct Monomial { + pub coefficient: f64, + pub variables: Vec<(&'static str, u8)>, // (name, exponent) +} + +/// A polynomial: sum of monomials +#[derive(Clone, Debug)] +pub struct Polynomial { + pub terms: Vec, +} + +impl Polynomial { + pub fn constant(c: f64) -> Self { + Self { terms: vec![Monomial { coefficient: c, variables: vec![] }] } + } + + pub fn var(name: &'static str) -> Self { + Self { terms: vec![Monomial { coefficient: 1.0, variables: vec![(name, 1)] }] } + } + + pub fn var_pow(name: &'static str, exp: u8) -> Self { + Self { terms: vec![Monomial { coefficient: 1.0, variables: vec![(name, exp)] }] } + } + + pub fn scale(mut self, c: f64) -> Self { + for term in &mut self.terms { + term.coefficient *= c; + } + self + } + + pub fn add(mut self, other: Self) -> Self { + self.terms.extend(other.terms); + self + } + + pub fn evaluate(&self, size: &ProblemSize) -> f64 { + self.terms.iter().map(|mono| { + let var_product: f64 = mono.variables.iter() + .map(|(name, exp)| (size.get(name) as f64).powi(*exp as i32)) + .product(); + mono.coefficient * var_product + }).sum() + } +} +``` + +### 3.2 Polynomial Macro + +```rust +macro_rules! poly { + // Single variable: poly!(n) + ($name:ident) => { + Polynomial::var(stringify!($name)) + }; + // Variable with exponent: poly!(n^2) + ($name:ident ^ $exp:literal) => { + Polynomial::var_pow(stringify!($name), $exp) + }; + // Scaled: poly!(3 * n) + ($c:literal * $name:ident) => { + Polynomial::var(stringify!($name)).scale($c as f64) + }; + // Scaled with exponent: poly!(9 * n^2) + ($c:literal * $name:ident ^ $exp:literal) => { + Polynomial::var_pow(stringify!($name), $exp).scale($c as f64) + }; + // Addition: poly!(n + m) + ($a:ident + $($rest:tt)+) => { + poly!($a).add(poly!($($rest)+)) + }; + // Scaled addition: poly!(3 * n + 9 * m^2) + ($c:literal * $a:ident + $($rest:tt)+) => { + poly!($c * $a).add(poly!($($rest)+)) + }; + ($c:literal * $a:ident ^ $e:literal + $($rest:tt)+) => { + poly!($c * $a ^ $e).add(poly!($($rest)+)) + }; +} +``` + +### 3.3 Reduction Overhead + +```rust +#[derive(Clone, Debug)] +pub struct ReductionOverhead { + /// Output size as polynomials of input size variables + pub output_size: Vec<(&'static str, Polynomial)>, +} + +impl ReductionOverhead { + pub fn evaluate_output_size(&self, input: &ProblemSize) -> ProblemSize { + let mut output = ProblemSize::new(); + for (name, poly) in &self.output_size { + output.fields.insert(name, poly.evaluate(input) as usize); + } + output + } +} +``` + +--- + +## 4. Reduction Registration + +### 4.1 ReductionEntry + +```rust +pub struct ReductionEntry { + pub source_name: &'static str, + pub target_name: &'static str, + pub source_graph: &'static str, + pub target_graph: &'static str, + pub overhead: ReductionOverhead, +} + +inventory::collect!(ReductionEntry); +``` + +### 4.2 Registration Macro + +```rust +macro_rules! register_reduction { + ( + $source:ty => $target:ty, + output: { $($out_name:ident : $out_poly:expr),* $(,)? } + ) => { + inventory::submit! { + ReductionEntry { + source_name: <$source as Problem>::NAME, + target_name: <$target as Problem>::NAME, + source_graph: <<$source as Problem>::GraphType as GraphMarker>::NAME, + target_graph: <<$target as Problem>::GraphType as GraphMarker>::NAME, + overhead: ReductionOverhead { + output_size: vec![ + $((stringify!($out_name), $out_poly)),* + ], + }, + } + } + }; +} +``` + +### 4.3 Usage Examples + +```rust +// MaxCut → SpinGlass (1:1 mapping) +impl ReduceTo> for MaxCut { + // ... implementation +} + +register_reduction!( + MaxCut => SpinGlass, + output: { + n: poly!(n), + m: poly!(m), + } +); + +// SAT → IndependentSet (blowup) +impl ReduceTo> for Satisfiability { + // ... implementation +} + +register_reduction!( + Satisfiability => IndependentSet, + output: { + n: poly!(3 * c), // 3 vertices per clause + m: poly!(c + 9 * c^2), // intra + inter clause edges + } +); +``` + +--- + +## 5. ReductionGraph + +### 5.1 Graph Structure + +```rust +use petgraph::graph::DiGraph; +use std::collections::{HashMap, HashSet}; + +pub struct ReductionGraph { + /// Problem name → node index + nodes: HashMap<&'static str, NodeIndex>, + + /// Directed graph of reductions + graph: DiGraph<&'static str, ReductionEdge>, + + /// Graph subtype hierarchy (transitive closure) + graph_hierarchy: HashMap<&'static str, HashSet<&'static str>>, +} + +pub struct ReductionEdge { + pub source_graph: &'static str, + pub target_graph: &'static str, + pub overhead: ReductionOverhead, +} +``` + +### 5.2 Initialization + +```rust +impl ReductionGraph { + pub fn new() -> Self { + let mut rg = Self { + nodes: HashMap::new(), + graph: DiGraph::new(), + graph_hierarchy: Self::build_graph_hierarchy(), + }; + + // Collect all registered reductions + for entry in inventory::iter:: { + rg.add_reduction(entry); + } + + rg + } + + fn build_graph_hierarchy() -> HashMap<&'static str, HashSet<&'static str>> { + let mut supertypes: HashMap<&'static str, HashSet<&'static str>> = HashMap::new(); + + // Direct relationships from inventory + for entry in inventory::iter:: { + supertypes.entry(entry.subtype) + .or_default() + .insert(entry.supertype); + } + + // Compute transitive closure + loop { + let mut changed = false; + let types: Vec<_> = supertypes.keys().copied().collect(); + + for sub in &types { + let current_supers: Vec<_> = supertypes.get(sub) + .map(|s| s.iter().copied().collect()) + .unwrap_or_default(); + + for sup in current_supers { + if let Some(sup_supers) = supertypes.get(sup).cloned() { + let entry = supertypes.entry(sub).or_default(); + for ss in sup_supers { + if entry.insert(ss) { + changed = true; + } + } + } + } + } + + if !changed { break; } + } + + supertypes + } + + pub fn is_graph_subtype(&self, sub: &str, sup: &str) -> bool { + sub == sup || self.graph_hierarchy + .get(sub) + .map(|s| s.contains(sup)) + .unwrap_or(false) + } +} +``` + +### 5.3 Set-Theoretic Path Validation + +```rust +impl ReductionGraph { + /// Check if reduction rule C→D can be used for A→B + /// Requires: A ⊆ C (source) and D ⊆ B (target) + pub fn rule_applicable( + &self, + want_source_graph: &str, // A's graph type + want_target_graph: &str, // B's graph type + rule_source_graph: &str, // C's graph type + rule_target_graph: &str, // D's graph type + ) -> bool { + // A's graph must be subtype of C's graph (input constraint) + let source_ok = self.is_graph_subtype(want_source_graph, rule_source_graph); + + // D's graph must be subtype of B's graph (output acceptable) + let target_ok = self.is_graph_subtype(rule_target_graph, want_target_graph); + + source_ok && target_ok + } +} +``` + +--- + +## 6. Cost Functions + +### 6.1 PathCostFn Trait + +```rust +pub trait PathCostFn { + fn edge_cost(&self, edge: &ReductionEdge, current_size: &ProblemSize) -> f64; +} +``` + +### 6.2 Built-in Cost Functions + +```rust +/// Minimize a single output field +pub struct Minimize(pub &'static str); + +impl PathCostFn for Minimize { + fn edge_cost(&self, edge: &ReductionEdge, size: &ProblemSize) -> f64 { + edge.overhead.evaluate_output_size(size).get(self.0) as f64 + } +} + +/// Minimize weighted sum of output fields +pub struct MinimizeWeighted(pub &'static [(&'static str, f64)]); + +impl PathCostFn for MinimizeWeighted { + fn edge_cost(&self, edge: &ReductionEdge, size: &ProblemSize) -> f64 { + let output = edge.overhead.evaluate_output_size(size); + self.0.iter() + .map(|(field, weight)| weight * output.get(field) as f64) + .sum() + } +} + +/// Minimize the maximum of specified fields +pub struct MinimizeMax(pub &'static [&'static str]); + +impl PathCostFn for MinimizeMax { + fn edge_cost(&self, edge: &ReductionEdge, size: &ProblemSize) -> f64 { + let output = edge.overhead.evaluate_output_size(size); + self.0.iter() + .map(|field| output.get(field) as f64) + .fold(0.0, f64::max) + } +} + +/// Lexicographic: minimize first field, break ties with subsequent +pub struct MinimizeLexicographic(pub &'static [&'static str]); + +impl PathCostFn for MinimizeLexicographic { + fn edge_cost(&self, edge: &ReductionEdge, size: &ProblemSize) -> f64 { + let output = edge.overhead.evaluate_output_size(size); + let mut cost = 0.0; + let mut scale = 1.0; + for field in self.0 { + cost += scale * output.get(field) as f64; + scale *= 1e-10; + } + cost + } +} + +/// Minimize number of reduction steps +pub struct MinimizeSteps; + +impl PathCostFn for MinimizeSteps { + fn edge_cost(&self, _edge: &ReductionEdge, _size: &ProblemSize) -> f64 { + 1.0 + } +} + +/// Custom cost function from closure +pub struct CustomCost(pub F); + +impl f64> PathCostFn for CustomCost { + fn edge_cost(&self, edge: &ReductionEdge, size: &ProblemSize) -> f64 { + (self.0)(edge, size) + } +} +``` + +--- + +## 7. Path Finding + +### 7.1 Dijkstra with Custom Cost + +```rust +use std::cmp::Reverse; +use std::collections::BinaryHeap; +use ordered_float::OrderedFloat; + +impl ReductionGraph { + pub fn find_cheapest_path( + &self, + source: (&str, &str), // (problem_name, graph_type) + target: (&str, &str), + input_size: &ProblemSize, + cost_fn: &C, + ) -> Option { + let mut costs: HashMap<&str, f64> = HashMap::new(); + let mut sizes: HashMap<&str, ProblemSize> = HashMap::new(); + let mut prev: HashMap<&str, (&str, EdgeIndex)> = HashMap::new(); + let mut heap = BinaryHeap::new(); + + costs.insert(source.0, 0.0); + sizes.insert(source.0, input_size.clone()); + heap.push(Reverse((OrderedFloat(0.0), source.0))); + + while let Some(Reverse((cost, node))) = heap.pop() { + if node == target.0 { + return Some(self.reconstruct_path(&prev, source.0, target.0)); + } + + if cost.0 > *costs.get(node).unwrap_or(&f64::INFINITY) { + continue; + } + + let current_size = sizes.get(node).unwrap(); + + for edge_idx in self.graph.edges_from(self.nodes[node]) { + let edge = &self.graph[edge_idx]; + let next_node = self.graph.edge_target(edge_idx); + + // Set-theoretic validation + if !self.rule_applicable( + source.1, target.1, + edge.source_graph, edge.target_graph, + ) { + continue; + } + + let edge_cost = cost_fn.edge_cost(edge, current_size); + let new_cost = cost.0 + edge_cost; + let new_size = edge.overhead.evaluate_output_size(current_size); + + if new_cost < *costs.get(next_node).unwrap_or(&f64::INFINITY) { + costs.insert(next_node, new_cost); + sizes.insert(next_node, new_size); + prev.insert(next_node, (node, edge_idx)); + heap.push(Reverse((OrderedFloat(new_cost), next_node))); + } + } + } + + None + } +} +``` + +### 7.2 Convenience Methods + +```rust +impl ReductionGraph { + pub fn find_path_minimizing( + &self, + source: (&str, &str), + target: (&str, &str), + input_size: &ProblemSize, + field: &'static str, + ) -> Option { + self.find_cheapest_path(source, target, input_size, &Minimize(field)) + } + + pub fn find_shortest_path( + &self, + source: (&str, &str), + target: (&str, &str), + input_size: &ProblemSize, + ) -> Option { + self.find_cheapest_path(source, target, input_size, &MinimizeSteps) + } +} +``` + +--- + +## 8. Type Conversion and Execution + +### 8.1 Weight Conversion + +```rust +pub trait ConvertWeights { + type Output; + fn convert_weights(self) -> Self::Output; +} + +impl> + ConvertWeights for IndependentSet +{ + type Output = IndependentSet; + + fn convert_weights(self) -> Self::Output { + IndependentSet { + graph: self.graph.map_weights(W2::from), + _phantom: PhantomData, + } + } +} +``` + +### 8.2 Path Execution + +```rust +pub struct ReductionPath { + pub steps: Vec, + pub total_cost: f64, +} + +pub struct ReductionStep { + pub source: &'static str, + pub target: &'static str, + pub source_graph: &'static str, + pub target_graph: &'static str, +} + +/// High-level API for automatic path finding and execution +pub trait ReduceVia: Problem { + fn reduce_via(self, graph: &ReductionGraph) -> ComposedReductionResult + where + Self::GraphType: GraphSubtype, + T::Weight: From; +} +``` + +### 8.3 Solution Extraction + +```rust +pub struct ComposedReductionResult { + steps: Vec>, + final_problem: T, + _phantom: PhantomData, +} + +impl ComposedReductionResult { + pub fn target_problem(&self) -> &T { + &self.final_problem + } + + pub fn extract_solution(&self, target_solution: &[bool]) -> Vec { + let mut solution = target_solution.to_vec(); + for step in self.steps.iter().rev() { + solution = step.extract_solution(&solution); + } + solution + } +} +``` + +--- + +## 9. Usage Examples + +### 9.1 Basic Reduction + +```rust +let graph = ReductionGraph::new(); + +// Create a problem +let is: IndependentSet = create_unit_disk_is(); + +// Find path to SpinGlass with f64 weights +let path = graph.find_path_minimizing( + ("IndependentSet", "UnitDiskGraph"), + ("SpinGlass", "SimpleGraph"), + &is.problem_size(), + "n", // minimize output spins +); + +println!("Path: {:?}", path); +``` + +### 9.2 Custom Cost Function + +```rust +// Weighted combination: vertices matter 2x more than edges +let path = graph.find_cheapest_path( + ("SAT", "SimpleGraph"), + ("SpinGlass", "SimpleGraph"), + &sat.problem_size(), + &MinimizeWeighted(&[("n", 2.0), ("m", 1.0)]), +); +``` + +### 9.3 Full Pipeline + +```rust +let sat: Satisfiability = create_sat_problem(); +let graph = ReductionGraph::new(); + +// Reduce to SpinGlass +let result = sat.reduce_via::>(&graph); +let spinglass = result.target_problem(); + +// Solve SpinGlass +let sg_solution = solve_spinglass(spinglass); + +// Extract back to SAT +let sat_solution = result.extract_solution(&sg_solution); +``` + +--- + +## 10. Migration Path + +### Phase 1: Add New Types (Non-Breaking) +- Add `GraphMarker` trait and graph type markers +- Add `NumericWeight` marker trait +- Keep existing problem types working + +### Phase 2: Parametrize Problems +- Update problem structs to take `` parameters +- Remove old type aliases +- Update all reduction implementations + +### Phase 3: Add Registration System +- Add `inventory` dependency +- Create registration macros +- Add polynomial overhead to all reductions + +### Phase 4: Update ReductionGraph +- Implement set-theoretic path finding +- Add cost function support +- Implement path execution with weight conversion + +--- + +## 11. Dependencies + +```toml +[dependencies] +inventory = "0.3" +petgraph = "0.6" +ordered-float = "4.0" +``` + +--- + +## 12. Open Questions + +1. **KSatisfiability**: Should `KSatisfiability<3>` and `KSatisfiability<4>` be different nodes? Current design merges them. + +2. **Non-graph problems**: Problems like SAT have no natural graph type. Using `SimpleGraph` as default is a workaround. + +3. **Weight conversion losses**: `f64 → i32` is blocked by `From`. Should we support lossy conversions with explicit opt-in? + +4. **Cross-crate extensions**: The `inventory` approach works across crates, but users need to ensure their graph subtypes are registered. diff --git a/docs/plans/2026-01-26-set-theoretic-reductions-impl.md b/docs/plans/2026-01-26-set-theoretic-reductions-impl.md new file mode 100644 index 0000000..025e136 --- /dev/null +++ b/docs/plans/2026-01-26-set-theoretic-reductions-impl.md @@ -0,0 +1,1178 @@ +# Set-Theoretic Reduction Path Finding - Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement parametric problem modeling with set-theoretic reduction path finding, automatic registration, and customizable cost functions. + +**Architecture:** Problems carry `` type parameters. Reductions are auto-registered via `inventory` crate with polynomial overhead metadata. Path finding uses Dijkstra with set-theoretic validation (A ⊆ C, D ⊆ B) and user-defined cost functions. + +**Tech Stack:** Rust, petgraph, inventory, ordered-float + +**Reference:** See `docs/plans/2026-01-26-set-theoretic-reductions-design.md` for full design rationale. + +--- + +## Phase 1: Foundation Types + +### Task 1.1: Add Dependencies + +**Files:** +- Modify: `Cargo.toml` + +**Step 1: Add inventory and ordered-float dependencies** + +```toml +[dependencies] +# ... existing deps ... +inventory = "0.3" +ordered-float = "4.0" +``` + +**Step 2: Run cargo check** + +```bash +cargo check +``` + +Expected: Compiles with new dependencies + +**Step 3: Commit** + +```bash +git add Cargo.toml +git commit -m "deps: Add inventory and ordered-float crates" +``` + +--- + +### Task 1.2: Create Graph Marker Traits + +**Files:** +- Create: `src/graph_types.rs` +- Modify: `src/lib.rs` + +**Step 1: Write the failing test** + +Create `src/graph_types.rs`: + +```rust +//! Graph type markers for parametric problem modeling. + +/// Marker trait for graph types. +pub trait GraphMarker: 'static + Clone + Send + Sync { + /// The name of this graph type for runtime queries. + const NAME: &'static str; +} + +/// Compile-time subtype relationship between graph types. +pub trait GraphSubtype: GraphMarker {} + +// Reflexive: every type is a subtype of itself +impl GraphSubtype for G {} + +/// Simple (arbitrary) graph - the most general graph type. +#[derive(Debug, Clone, Copy, Default)] +pub struct SimpleGraph; + +impl GraphMarker for SimpleGraph { + const NAME: &'static str = "SimpleGraph"; +} + +/// Planar graph - can be drawn on a plane without edge crossings. +#[derive(Debug, Clone, Copy, Default)] +pub struct PlanarGraph; + +impl GraphMarker for PlanarGraph { + const NAME: &'static str = "PlanarGraph"; +} + +/// Unit disk graph - vertices are points, edges connect points within unit distance. +#[derive(Debug, Clone, Copy, Default)] +pub struct UnitDiskGraph; + +impl GraphMarker for UnitDiskGraph { + const NAME: &'static str = "UnitDiskGraph"; +} + +/// Bipartite graph - vertices can be partitioned into two sets with edges only between sets. +#[derive(Debug, Clone, Copy, Default)] +pub struct BipartiteGraph; + +impl GraphMarker for BipartiteGraph { + const NAME: &'static str = "BipartiteGraph"; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_graph_marker_names() { + assert_eq!(SimpleGraph::NAME, "SimpleGraph"); + assert_eq!(PlanarGraph::NAME, "PlanarGraph"); + assert_eq!(UnitDiskGraph::NAME, "UnitDiskGraph"); + assert_eq!(BipartiteGraph::NAME, "BipartiteGraph"); + } + + #[test] + fn test_reflexive_subtype() { + fn assert_subtype, B: GraphMarker>() {} + + // Every type is a subtype of itself + assert_subtype::(); + assert_subtype::(); + assert_subtype::(); + } +} +``` + +**Step 2: Run test to verify it compiles** + +```bash +cargo test graph_types --lib +``` + +Expected: PASS + +**Step 3: Add module to lib.rs** + +In `src/lib.rs`, add: + +```rust +pub mod graph_types; +``` + +**Step 4: Run tests** + +```bash +cargo test graph_types --lib +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/graph_types.rs src/lib.rs +git commit -m "feat: Add graph marker traits for parametric problems" +``` + +--- + +### Task 1.3: Register Graph Subtype Hierarchy + +**Files:** +- Modify: `src/graph_types.rs` + +**Step 1: Add inventory-based registration** + +Add to `src/graph_types.rs`: + +```rust +use inventory; + +/// Runtime registration of graph subtype relationships. +pub struct GraphSubtypeEntry { + pub subtype: &'static str, + pub supertype: &'static str, +} + +inventory::collect!(GraphSubtypeEntry); + +/// Macro to declare both compile-time trait and runtime registration. +#[macro_export] +macro_rules! declare_graph_subtype { + ($sub:ty => $sup:ty) => { + impl $crate::graph_types::GraphSubtype<$sup> for $sub {} + + ::inventory::submit! { + $crate::graph_types::GraphSubtypeEntry { + subtype: <$sub as $crate::graph_types::GraphMarker>::NAME, + supertype: <$sup as $crate::graph_types::GraphMarker>::NAME, + } + } + }; +} + +// Declare the graph type hierarchy +declare_graph_subtype!(UnitDiskGraph => PlanarGraph); +declare_graph_subtype!(UnitDiskGraph => SimpleGraph); +declare_graph_subtype!(PlanarGraph => SimpleGraph); +declare_graph_subtype!(BipartiteGraph => SimpleGraph); +``` + +**Step 2: Add test for runtime hierarchy** + +```rust +#[test] +fn test_subtype_entries_registered() { + let entries: Vec<_> = inventory::iter::.collect(); + + // Should have at least 4 entries + assert!(entries.len() >= 4); + + // Check specific relationships + assert!(entries.iter().any(|e| + e.subtype == "UnitDiskGraph" && e.supertype == "SimpleGraph" + )); + assert!(entries.iter().any(|e| + e.subtype == "PlanarGraph" && e.supertype == "SimpleGraph" + )); +} +``` + +**Step 3: Run tests** + +```bash +cargo test graph_types --lib +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/graph_types.rs +git commit -m "feat: Add inventory-based graph subtype registration" +``` + +--- + +### Task 1.4: Create NumericWeight Trait + +**Files:** +- Modify: `src/types.rs` + +**Step 1: Add NumericWeight trait** + +Add to `src/types.rs`: + +```rust +/// Marker trait for numeric weight types. +/// +/// Weight subsumption uses Rust's `From` trait: +/// - `i32 → f64` is valid (From for f64 exists) +/// - `f64 → i32` is invalid (no lossless conversion) +pub trait NumericWeight: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static {} + +impl NumericWeight for bool {} +impl NumericWeight for i8 {} +impl NumericWeight for i16 {} +impl NumericWeight for i32 {} +impl NumericWeight for i64 {} +impl NumericWeight for f32 {} +impl NumericWeight for f64 {} +``` + +**Step 2: Add test** + +```rust +#[test] +fn test_numeric_weight_impls() { + fn assert_numeric_weight() {} + + assert_numeric_weight::(); + assert_numeric_weight::(); + assert_numeric_weight::(); +} +``` + +**Step 3: Run tests** + +```bash +cargo test numeric_weight --lib +``` + +Expected: PASS + +**Step 4: Export in prelude** + +In `src/lib.rs` prelude, add: + +```rust +pub use crate::types::NumericWeight; +``` + +**Step 5: Commit** + +```bash +git add src/types.rs src/lib.rs +git commit -m "feat: Add NumericWeight marker trait" +``` + +--- + +### Task 1.5: Create Polynomial Type + +**Files:** +- Create: `src/polynomial.rs` +- Modify: `src/lib.rs` + +**Step 1: Create polynomial module** + +Create `src/polynomial.rs`: + +```rust +//! Polynomial representation for reduction overhead. + +use crate::types::ProblemSize; + +/// A monomial: coefficient × Π(variable^exponent) +#[derive(Clone, Debug, PartialEq)] +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 + } +} + +/// A polynomial: Σ monomials +#[derive(Clone, Debug, PartialEq)] +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)] } + } + + pub fn scale(mut self, c: f64) -> Self { + for term in &mut self.terms { + term.coefficient *= c; + } + self + } + + pub fn add(mut self, other: Self) -> Self { + self.terms.extend(other.terms); + self + } + + pub fn evaluate(&self, size: &ProblemSize) -> f64 { + self.terms.iter().map(|m| m.evaluate(size)).sum() + } +} + +/// 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) + }; +} + +#[cfg(test)] +mod tests { + 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) + .add(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) + .add(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 + } +} +``` + +**Step 2: Add module to lib.rs** + +```rust +pub mod polynomial; +``` + +**Step 3: Run tests** + +```bash +cargo test polynomial --lib +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/polynomial.rs src/lib.rs +git commit -m "feat: Add Polynomial type for reduction overhead" +``` + +--- + +## Phase 2: Reduction Registration + +### Task 2.1: Create ReductionEntry and Registration + +**Files:** +- Create: `src/rules/registry.rs` +- Modify: `src/rules/mod.rs` + +**Step 1: Create registry module** + +Create `src/rules/registry.rs`: + +```rust +//! Automatic reduction registration via inventory. + +use crate::polynomial::Polynomial; +use inventory; + +/// Overhead specification for a reduction. +#[derive(Clone, Debug)] +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)>, +} + +impl ReductionOverhead { + pub fn new(output_size: Vec<(&'static str, Polynomial)>) -> Self { + Self { output_size } + } + + /// Evaluate output size given input size. + pub fn evaluate_output_size(&self, input: &crate::types::ProblemSize) -> crate::types::ProblemSize { + let fields: Vec<_> = self.output_size.iter() + .map(|(name, poly)| (*name, poly.evaluate(input) as usize)) + .collect(); + crate::types::ProblemSize::new(fields) + } +} + +impl Default for ReductionOverhead { + fn default() -> Self { + Self { output_size: vec![] } + } +} + +/// A registered reduction entry. +pub struct ReductionEntry { + /// Base name of source problem (e.g., "IndependentSet"). + pub source_name: &'static str, + /// Base name of target problem (e.g., "VertexCovering"). + pub target_name: &'static str, + /// Graph type of source problem (e.g., "SimpleGraph"). + pub source_graph: &'static str, + /// Graph type of target problem. + pub target_graph: &'static str, + /// Overhead information. + pub overhead: ReductionOverhead, +} + +inventory::collect!(ReductionEntry); + +/// Macro for registering a reduction alongside its impl. +#[macro_export] +macro_rules! register_reduction { + ( + $source:ty => $target:ty, + output: { $($out_name:ident : $out_poly:expr),* $(,)? } + ) => { + ::inventory::submit! { + $crate::rules::registry::ReductionEntry { + source_name: <$source as $crate::traits::Problem>::NAME, + target_name: <$target as $crate::traits::Problem>::NAME, + source_graph: <<$source as $crate::traits::Problem>::GraphType as $crate::graph_types::GraphMarker>::NAME, + target_graph: <<$target as $crate::traits::Problem>::GraphType as $crate::graph_types::GraphMarker>::NAME, + overhead: $crate::rules::registry::ReductionOverhead::new(vec![ + $((stringify!($out_name), $out_poly)),* + ]), + } + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::ProblemSize; + use crate::poly; + + #[test] + fn test_reduction_overhead_evaluate() { + let overhead = ReductionOverhead::new(vec![ + ("n", poly!(3 * m)), + ("m", poly!(m^2)), + ]); + + let input = ProblemSize::new(vec![("m", 4)]); + let output = overhead.evaluate_output_size(&input); + + assert_eq!(output.get("n"), Some(12)); // 3 * 4 + assert_eq!(output.get("m"), Some(16)); // 4^2 + } +} +``` + +**Step 2: Add module to rules/mod.rs** + +```rust +pub mod registry; +pub use registry::{ReductionEntry, ReductionOverhead}; +``` + +**Step 3: Run tests** + +```bash +cargo test registry --lib +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/rules/registry.rs src/rules/mod.rs +git commit -m "feat: Add reduction registration with inventory" +``` + +--- + +### Task 2.2: Create Cost Function Traits + +**Files:** +- Create: `src/rules/cost.rs` +- Modify: `src/rules/mod.rs` + +**Step 1: Create cost module** + +Create `src/rules/cost.rs`: + +```rust +//! Cost functions for reduction path optimization. + +use crate::rules::registry::ReductionOverhead; +use crate::types::ProblemSize; + +/// User-defined cost function for path optimization. +pub trait PathCostFn { + /// Compute cost of taking an edge given current problem size. + fn edge_cost(&self, overhead: &ReductionOverhead, current_size: &ProblemSize) -> f64; +} + +/// Minimize a single output field. +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 + } +} + +/// 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); + 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); + 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); + 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 + } +} + +/// Minimize number of reduction steps. +pub struct MinimizeSteps; + +impl PathCostFn for MinimizeSteps { + fn edge_cost(&self, _overhead: &ReductionOverhead, _size: &ProblemSize) -> f64 { + 1.0 + } +} + +/// Custom cost function from closure. +pub struct CustomCost(pub F); + +impl f64> PathCostFn for CustomCost { + fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { + (self.0)(overhead, size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::polynomial::Polynomial; + + fn test_overhead() -> ReductionOverhead { + ReductionOverhead::new(vec![ + ("n", Polynomial::var("n").scale(2.0)), + ("m", Polynomial::var("m")), + ]) + } + + #[test] + fn test_minimize_single() { + let cost_fn = Minimize("n"); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 20.0); // 2 * 10 + } + + #[test] + fn test_minimize_weighted() { + let cost_fn = MinimizeWeighted(vec![("n", 1.0), ("m", 2.0)]); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + // output n = 20, output m = 5 + // cost = 1.0 * 20 + 2.0 * 5 = 30 + assert_eq!(cost_fn.edge_cost(&overhead, &size), 30.0); + } + + #[test] + fn test_minimize_steps() { + let cost_fn = MinimizeSteps; + let size = ProblemSize::new(vec![("n", 100)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 1.0); + } +} +``` + +**Step 2: Add module to rules/mod.rs** + +```rust +pub mod cost; +pub use cost::{PathCostFn, Minimize, MinimizeWeighted, MinimizeMax, MinimizeLexicographic, MinimizeSteps, CustomCost}; +``` + +**Step 3: Run tests** + +```bash +cargo test cost --lib +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/rules/cost.rs src/rules/mod.rs +git commit -m "feat: Add PathCostFn trait and built-in cost functions" +``` + +--- + +## Phase 3: Update Problem Trait + +### Task 3.1: Add NAME and GraphType to Problem Trait + +**Files:** +- Modify: `src/traits.rs` + +**Step 1: Update Problem trait** + +Add to `Problem` trait: + +```rust +use crate::graph_types::{GraphMarker, SimpleGraph}; +use crate::types::NumericWeight; + +pub trait Problem: Clone { + /// Base name of this problem type (e.g., "IndependentSet"). + const NAME: &'static str; + + /// The graph type this problem operates on. + type GraphType: GraphMarker; + + /// The weight type for this problem. + type Weight: NumericWeight; + + /// The type used for objective/size values. + type Size: Clone + PartialOrd + Num + Zero + AddAssign; + + // ... existing methods ... +} +``` + +**Step 2: This will cause compilation errors - fix in next tasks** + +Note: All problem implementations will need updating. Proceed to Phase 4. + +--- + +## Phase 4: Update Problem Implementations + +### Task 4.1: Update IndependentSet + +**Files:** +- Modify: `src/models/graph/independent_set.rs` + +**Step 1: Update struct definition** + +```rust +use crate::graph_types::{GraphMarker, SimpleGraph}; +use crate::types::NumericWeight; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndependentSet { + graph: UnGraph<(), ()>, + weights: Vec, + #[serde(skip)] + _phantom: PhantomData, +} +``` + +**Step 2: Update impl blocks** + +```rust +impl IndependentSet { + pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self + where + W: From, + { + // ... existing implementation + Self { graph, weights, _phantom: PhantomData } + } +} + +impl Problem for IndependentSet { + const NAME: &'static str = "IndependentSet"; + type GraphType = G; + type Weight = W; + type Size = W; + // ... rest of impl +} +``` + +**Step 3: Run tests** + +```bash +cargo test independent_set --lib +``` + +Expected: May have compilation errors to fix + +**Step 4: Fix any remaining issues and commit** + +```bash +git add src/models/graph/independent_set.rs +git commit -m "refactor: Add GraphMarker parameter to IndependentSet" +``` + +--- + +### Task 4.2-4.10: Update Remaining Problems + +Apply the same pattern to: +- `VertexCovering` +- `MaxCut` +- `Matching` +- `DominatingSet` +- `Coloring` +- `SpinGlass` +- `QUBO` +- `SetPacking` +- `SetCovering` +- `Satisfiability` +- `KSatisfiability` +- `CircuitSAT` +- `Factoring` + +Each follows the same pattern: +1. Add `G: GraphMarker` parameter (default `SimpleGraph`) +2. Add `PhantomData` field +3. Update `Problem` impl with `NAME`, `GraphType`, `Weight` +4. Test and commit + +--- + +## Phase 5: Update ReductionGraph + +### Task 5.1: Rewrite ReductionGraph with Set-Theoretic Path Finding + +**Files:** +- Modify: `src/rules/graph.rs` + +**Step 1: Add new imports and fields** + +```rust +use crate::graph_types::GraphSubtypeEntry; +use crate::rules::registry::ReductionEntry; +use crate::rules::cost::PathCostFn; +use ordered_float::OrderedFloat; +use std::cmp::Reverse; +use std::collections::BinaryHeap; +``` + +**Step 2: Add graph hierarchy** + +```rust +pub struct ReductionGraph { + graph: DiGraph<&'static str, ReductionEdge>, + name_indices: HashMap<&'static str, NodeIndex>, + type_to_name: HashMap, + graph_hierarchy: HashMap<&'static str, HashSet<&'static str>>, +} + +pub struct ReductionEdge { + pub source_graph: &'static str, + pub target_graph: &'static str, + pub overhead: ReductionOverhead, +} +``` + +**Step 3: Implement set-theoretic validation** + +```rust +impl ReductionGraph { + fn build_graph_hierarchy() -> HashMap<&'static str, HashSet<&'static str>> { + let mut supertypes: HashMap<&'static str, HashSet<&'static str>> = HashMap::new(); + + for entry in inventory::iter:: { + supertypes.entry(entry.subtype) + .or_default() + .insert(entry.supertype); + } + + // Compute transitive closure + loop { + let mut changed = false; + let types: Vec<_> = supertypes.keys().copied().collect(); + + for sub in &types { + let current: Vec<_> = supertypes.get(sub) + .map(|s| s.iter().copied().collect()) + .unwrap_or_default(); + + for sup in current { + if let Some(sup_supers) = supertypes.get(sup).cloned() { + for ss in sup_supers { + if supertypes.entry(sub).or_default().insert(ss) { + changed = true; + } + } + } + } + } + + if !changed { break; } + } + + supertypes + } + + pub fn is_graph_subtype(&self, sub: &str, sup: &str) -> bool { + sub == sup || self.graph_hierarchy + .get(sub) + .map(|s| s.contains(sup)) + .unwrap_or(false) + } + + /// Check if reduction rule can be used: A ⊆ C and D ⊆ B + pub fn rule_applicable( + &self, + want_source_graph: &str, + want_target_graph: &str, + rule_source_graph: &str, + rule_target_graph: &str, + ) -> bool { + self.is_graph_subtype(want_source_graph, rule_source_graph) + && self.is_graph_subtype(rule_target_graph, want_target_graph) + } +} +``` + +**Step 4: Implement Dijkstra with custom cost** + +```rust +impl ReductionGraph { + pub fn find_cheapest_path( + &self, + source: (&str, &str), // (problem_name, graph_type) + target: (&str, &str), + input_size: &ProblemSize, + cost_fn: &C, + ) -> Option { + let src_idx = *self.name_indices.get(source.0)?; + let dst_idx = *self.name_indices.get(target.0)?; + + let mut costs: HashMap = HashMap::new(); + let mut sizes: HashMap = HashMap::new(); + let mut prev: HashMap = HashMap::new(); + let mut heap = BinaryHeap::new(); + + costs.insert(src_idx, 0.0); + sizes.insert(src_idx, input_size.clone()); + heap.push(Reverse((OrderedFloat(0.0), src_idx))); + + while let Some(Reverse((cost, node))) = heap.pop() { + if node == dst_idx { + return Some(self.reconstruct_path(&prev, src_idx, dst_idx)); + } + + if cost.0 > *costs.get(&node).unwrap_or(&f64::INFINITY) { + continue; + } + + let current_size = sizes.get(&node)?; + + for edge_idx in self.graph.edges(node) { + let edge = &self.graph[edge_idx.id()]; + let next = edge_idx.target(); + + if !self.rule_applicable( + source.1, target.1, + edge.source_graph, edge.target_graph, + ) { + continue; + } + + let edge_cost = cost_fn.edge_cost(&edge.overhead, current_size); + let new_cost = cost.0 + edge_cost; + let new_size = edge.overhead.evaluate_output_size(current_size); + + if new_cost < *costs.get(&next).unwrap_or(&f64::INFINITY) { + costs.insert(next, new_cost); + sizes.insert(next, new_size); + prev.insert(next, (node, edge_idx.id())); + heap.push(Reverse((OrderedFloat(new_cost), next))); + } + } + } + + None + } +} +``` + +**Step 5: Run tests** + +```bash +cargo test graph --lib +``` + +Expected: PASS (may need to update tests) + +**Step 6: Commit** + +```bash +git add src/rules/graph.rs +git commit -m "refactor: Implement set-theoretic path finding with cost functions" +``` + +--- + +## Phase 6: Update Reduction Implementations + +### Task 6.1: Update Reduction Implementations with Registration + +For each reduction in `src/rules/`, add registration: + +Example for `spinglass_maxcut.rs`: + +```rust +use crate::{register_reduction, poly}; + +// After the impl ReduceTo block: +register_reduction!( + MaxCut => SpinGlass, + output: { + n: poly!(n), + m: poly!(m), + } +); +``` + +Apply to all reduction files. + +--- + +## Phase 7: Integration Tests + +### Task 7.1: Add Integration Tests + +**Files:** +- Create: `tests/set_theoretic_tests.rs` + +```rust +use problemreductions::prelude::*; +use problemreductions::graph_types::*; +use problemreductions::rules::{ReductionGraph, Minimize, MinimizeSteps}; + +#[test] +fn test_unit_disk_to_simple_path() { + let graph = ReductionGraph::new(); + + // Unit disk IS should find path to Simple SpinGlass + let path = graph.find_cheapest_path( + ("IndependentSet", "UnitDiskGraph"), + ("SpinGlass", "SimpleGraph"), + &ProblemSize::new(vec![("n", 100), ("m", 200)]), + &MinimizeSteps, + ); + + assert!(path.is_some()); +} + +#[test] +fn test_simple_cannot_use_unit_disk_rule() { + let graph = ReductionGraph::new(); + + // If only UnitDiskGraph rules exist, SimpleGraph source should fail + // (This tests that A ⊆ C is enforced) +} + +#[test] +fn test_weight_conversion() { + // Test that i32 -> f64 works but f64 -> i32 doesn't +} +``` + +--- + +## Summary + +**Total Tasks:** ~25 bite-sized tasks across 7 phases + +**Key Files Changed:** +- `Cargo.toml` - dependencies +- `src/graph_types.rs` - new +- `src/polynomial.rs` - new +- `src/types.rs` - NumericWeight +- `src/traits.rs` - Problem trait updates +- `src/models/**/*.rs` - all problem types +- `src/rules/registry.rs` - new +- `src/rules/cost.rs` - new +- `src/rules/graph.rs` - major rewrite +- `src/rules/*.rs` - all reductions + +**Commit Frequency:** After each task step 5 (approx. every 10-15 minutes of work) diff --git a/src/graph_types.rs b/src/graph_types.rs new file mode 100644 index 0000000..c6a3f48 --- /dev/null +++ b/src/graph_types.rs @@ -0,0 +1,160 @@ +//! Graph type markers for parametric problem modeling. + +use inventory; + +/// Marker trait for graph types. +pub trait GraphMarker: 'static + Clone + Send + Sync { + /// The name of this graph type for runtime queries. + const NAME: &'static str; +} + +/// Compile-time subtype relationship between graph types. +pub trait GraphSubtype: GraphMarker {} + +// Reflexive: every type is a subtype of itself +impl GraphSubtype for G {} + +/// Simple (arbitrary) graph - the most general graph type. +#[derive(Debug, Clone, Copy, Default)] +pub struct SimpleGraph; + +impl GraphMarker for SimpleGraph { + const NAME: &'static str = "SimpleGraph"; +} + +/// Planar graph - can be drawn on a plane without edge crossings. +#[derive(Debug, Clone, Copy, Default)] +pub struct PlanarGraph; + +impl GraphMarker for PlanarGraph { + const NAME: &'static str = "PlanarGraph"; +} + +/// Unit disk graph - vertices are points, edges connect points within unit distance. +#[derive(Debug, Clone, Copy, Default)] +pub struct UnitDiskGraph; + +impl GraphMarker for UnitDiskGraph { + const NAME: &'static str = "UnitDiskGraph"; +} + +/// Bipartite graph - vertices can be partitioned into two sets with edges only between sets. +#[derive(Debug, Clone, Copy, Default)] +pub struct BipartiteGraph; + +impl GraphMarker for BipartiteGraph { + const NAME: &'static str = "BipartiteGraph"; +} + +/// Runtime registration of graph subtype relationships. +pub struct GraphSubtypeEntry { + pub subtype: &'static str, + pub supertype: &'static str, +} + +inventory::collect!(GraphSubtypeEntry); + +/// Macro to declare both compile-time trait and runtime registration. +#[macro_export] +macro_rules! declare_graph_subtype { + ($sub:ty => $sup:ty) => { + impl $crate::graph_types::GraphSubtype<$sup> for $sub {} + + ::inventory::submit! { + $crate::graph_types::GraphSubtypeEntry { + subtype: <$sub as $crate::graph_types::GraphMarker>::NAME, + supertype: <$sup as $crate::graph_types::GraphMarker>::NAME, + } + } + }; +} + +// Declare the graph type hierarchy. +// Note: All direct relationships must be declared explicitly for compile-time trait bounds. +// Transitive closure is only computed at runtime in build_graph_hierarchy(). +declare_graph_subtype!(UnitDiskGraph => PlanarGraph); +declare_graph_subtype!(UnitDiskGraph => SimpleGraph); // Needed for compile-time GraphSubtype +declare_graph_subtype!(PlanarGraph => SimpleGraph); +declare_graph_subtype!(BipartiteGraph => SimpleGraph); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_graph_marker_names() { + assert_eq!(SimpleGraph::NAME, "SimpleGraph"); + assert_eq!(PlanarGraph::NAME, "PlanarGraph"); + assert_eq!(UnitDiskGraph::NAME, "UnitDiskGraph"); + assert_eq!(BipartiteGraph::NAME, "BipartiteGraph"); + } + + #[test] + fn test_reflexive_subtype() { + fn assert_subtype, B: GraphMarker>() {} + + // Every type is a subtype of itself + assert_subtype::(); + assert_subtype::(); + assert_subtype::(); + } + + #[test] + fn test_subtype_entries_registered() { + let entries: Vec<_> = inventory::iter::().collect(); + + // Should have at least 4 entries + assert!(entries.len() >= 4); + + // Check specific relationships + assert!(entries.iter().any(|e| + e.subtype == "UnitDiskGraph" && e.supertype == "SimpleGraph" + )); + assert!(entries.iter().any(|e| + e.subtype == "PlanarGraph" && e.supertype == "SimpleGraph" + )); + } + + #[test] + fn test_declared_subtypes() { + fn assert_subtype, B: GraphMarker>() {} + + // Declared relationships + assert_subtype::(); + assert_subtype::(); + assert_subtype::(); + assert_subtype::(); + } + + #[test] + fn test_graph_type_traits() { + // Test Default + let _: SimpleGraph = Default::default(); + let _: PlanarGraph = Default::default(); + let _: UnitDiskGraph = Default::default(); + let _: BipartiteGraph = Default::default(); + + // Test Copy (SimpleGraph implements Copy, so no need to clone) + let g = SimpleGraph; + let _g2 = g; // Copy + let g = SimpleGraph; + let _g2 = g; + let _g3 = g; // still usable + } + + #[test] + fn test_bipartite_entry_registered() { + let entries: Vec<_> = inventory::iter::().collect(); + assert!(entries + .iter() + .any(|e| e.subtype == "BipartiteGraph" && e.supertype == "SimpleGraph")); + } + + #[test] + fn test_unit_disk_to_planar_registered() { + let entries: Vec<_> = inventory::iter::().collect(); + assert!(entries + .iter() + .any(|e| e.subtype == "UnitDiskGraph" && e.supertype == "PlanarGraph")); + } +} diff --git a/src/lib.rs b/src/lib.rs index c513354..224f677 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,8 +62,10 @@ pub mod config; pub mod error; +pub mod graph_types; pub mod io; pub mod models; +pub mod polynomial; pub mod registry; pub mod rules; pub mod solvers; @@ -95,7 +97,7 @@ pub mod prelude { pub use crate::rules::{ReduceTo, ReductionResult}; pub use crate::traits::{csp_solution_size, ConstraintSatisfactionProblem, Problem}; pub use crate::types::{ - EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize, + EnergyMode, LocalConstraint, LocalSolutionSize, NumericWeight, ProblemSize, SolutionSize, }; } diff --git a/src/models/graph/coloring.rs b/src/models/graph/coloring.rs index c718099..483513e 100644 --- a/src/models/graph/coloring.rs +++ b/src/models/graph/coloring.rs @@ -3,6 +3,7 @@ //! The K-Coloring problem asks whether a graph can be colored with K colors //! such that no two adjacent vertices have the same color. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -96,6 +97,9 @@ impl Coloring { } impl Problem for Coloring { + const NAME: &'static str = "Coloring"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/graph/dominating_set.rs b/src/models/graph/dominating_set.rs index dd4ad22..ada735c 100644 --- a/src/models/graph/dominating_set.rs +++ b/src/models/graph/dominating_set.rs @@ -3,6 +3,7 @@ //! The Dominating Set problem asks for a minimum weight subset of vertices //! such that every vertex is either in the set or adjacent to a vertex in the set. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -118,8 +119,11 @@ impl DominatingSet { impl Problem for DominatingSet where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "DominatingSet"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -155,7 +159,7 @@ where impl ConstraintSatisfactionProblem for DominatingSet where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // For each vertex v, at least one vertex in N[v] must be selected diff --git a/src/models/graph/independent_set.rs b/src/models/graph/independent_set.rs index cf22f2f..25cda75 100644 --- a/src/models/graph/independent_set.rs +++ b/src/models/graph/independent_set.rs @@ -3,6 +3,7 @@ //! The Independent Set problem asks for a maximum weight subset of vertices //! such that no two vertices in the subset are adjacent. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -111,8 +112,11 @@ impl IndependentSet { impl Problem for IndependentSet where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "IndependentSet"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -148,7 +152,7 @@ where impl ConstraintSatisfactionProblem for IndependentSet where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // For each edge (u, v), at most one of u, v can be selected diff --git a/src/models/graph/matching.rs b/src/models/graph/matching.rs index f33c77b..75986a6 100644 --- a/src/models/graph/matching.rs +++ b/src/models/graph/matching.rs @@ -3,6 +3,7 @@ //! The Maximum Matching problem asks for a maximum weight set of edges //! such that no two edges share a vertex. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -133,8 +134,11 @@ impl Matching { impl Problem for Matching where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "Matching"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -172,7 +176,7 @@ where impl ConstraintSatisfactionProblem for Matching where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { let v2e = self.vertex_to_edges(); diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index 02fae0f..e8c92a4 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -3,6 +3,7 @@ //! The Maximum Cut problem asks for a partition of vertices into two sets //! that maximizes the total weight of edges crossing the partition. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -125,8 +126,11 @@ impl MaxCut { impl Problem for MaxCut where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "MaxCut"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index 5258cb2..87a96b9 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -3,6 +3,7 @@ //! The Maximal Independent Set problem asks for an independent set that //! cannot be extended by adding any other vertex. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -112,6 +113,9 @@ impl MaximalIS { } impl Problem for MaximalIS { + const NAME: &'static str = "MaximalIS"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/graph/template.rs b/src/models/graph/template.rs index a075259..3fee08b 100644 --- a/src/models/graph/template.rs +++ b/src/models/graph/template.rs @@ -71,6 +71,7 @@ //! - **Vertex Cover**: `[false, true, true, true]` - at least one selected //! - **Perfect Matching**: Define on edge graph with exactly one selected +use crate::graph_types::SimpleGraph as SimpleGraphMarker; use crate::registry::{ComplexityClass, GraphSubcategory, ProblemCategory, ProblemInfo, ProblemMetadata}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::{ConstraintSatisfactionProblem, Problem}; @@ -307,8 +308,11 @@ impl Problem for GraphProblem where C: GraphConstraint, G: Graph, - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { + const NAME: &'static str = C::NAME; + type GraphType = SimpleGraphMarker; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -346,7 +350,7 @@ impl ConstraintSatisfactionProblem for GraphProblem where C: GraphConstraint, G: Graph, - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { fn constraints(&self) -> Vec { let spec = C::edge_constraint_spec(); diff --git a/src/models/graph/vertex_covering.rs b/src/models/graph/vertex_covering.rs index 4c38a44..71a535d 100644 --- a/src/models/graph/vertex_covering.rs +++ b/src/models/graph/vertex_covering.rs @@ -3,6 +3,7 @@ //! The Vertex Cover problem asks for a minimum weight subset of vertices //! such that every edge has at least one endpoint in the subset. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -96,8 +97,11 @@ impl VertexCovering { impl Problem for VertexCovering where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "VertexCovering"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -133,7 +137,7 @@ where impl ConstraintSatisfactionProblem for VertexCovering where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // For each edge (u, v), at least one of u, v must be selected diff --git a/src/models/optimization/ilp.rs b/src/models/optimization/ilp.rs index 29e315e..b715d14 100644 --- a/src/models/optimization/ilp.rs +++ b/src/models/optimization/ilp.rs @@ -3,6 +3,7 @@ //! ILP optimizes a linear objective over integer variables subject to linear constraints. //! This is a fundamental "hub" problem that many other NP-hard problems can be reduced to. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -329,6 +330,9 @@ impl ILP { } impl Problem for ILP { + const NAME: &'static str = "ILP"; + type GraphType = SimpleGraph; + type Weight = f64; type Size = f64; fn num_variables(&self) -> usize { diff --git a/src/models/optimization/qubo.rs b/src/models/optimization/qubo.rs index ea4c7f5..d2dac8e 100644 --- a/src/models/optimization/qubo.rs +++ b/src/models/optimization/qubo.rs @@ -2,6 +2,7 @@ //! //! QUBO minimizes a quadratic function over binary variables. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -108,8 +109,12 @@ where + num_traits::Num + num_traits::Zero + std::ops::AddAssign - + std::ops::Mul, + + std::ops::Mul + + 'static, { + const NAME: &'static str = "QUBO"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/optimization/spin_glass.rs b/src/models/optimization/spin_glass.rs index 207446a..246056a 100644 --- a/src/models/optimization/spin_glass.rs +++ b/src/models/optimization/spin_glass.rs @@ -2,6 +2,7 @@ //! //! The Spin Glass problem minimizes the Ising Hamiltonian energy. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -105,8 +106,12 @@ where + num_traits::Zero + std::ops::AddAssign + std::ops::Mul - + From, + + From + + 'static, { + const NAME: &'static str = "SpinGlass"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/satisfiability/ksat.rs b/src/models/satisfiability/ksat.rs index fe5c83b..7567b34 100644 --- a/src/models/satisfiability/ksat.rs +++ b/src/models/satisfiability/ksat.rs @@ -3,6 +3,7 @@ //! K-SAT is a special case of SAT where each clause has exactly K literals. //! Common variants include 3-SAT (K=3) and 2-SAT (K=2). +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -168,8 +169,11 @@ impl KSatisfiability { impl Problem for KSatisfiability where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "KSatisfiability"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -209,7 +213,7 @@ where impl ConstraintSatisfactionProblem for KSatisfiability where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { self.clauses diff --git a/src/models/satisfiability/sat.rs b/src/models/satisfiability/sat.rs index 7a7cc30..77ad591 100644 --- a/src/models/satisfiability/sat.rs +++ b/src/models/satisfiability/sat.rs @@ -3,6 +3,7 @@ //! SAT is the problem of determining if there exists an assignment of //! Boolean variables that makes a given Boolean formula true. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -174,8 +175,11 @@ impl Satisfiability { impl Problem for Satisfiability where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "Satisfiability"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -217,7 +221,7 @@ where impl ConstraintSatisfactionProblem for Satisfiability where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // Each clause is a constraint diff --git a/src/models/set/set_covering.rs b/src/models/set/set_covering.rs index 687247f..586597d 100644 --- a/src/models/set/set_covering.rs +++ b/src/models/set/set_covering.rs @@ -3,6 +3,7 @@ //! The Set Covering problem asks for a minimum weight collection of sets //! that covers all elements in the universe. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -111,8 +112,11 @@ impl SetCovering { impl Problem for SetCovering where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "SetCovering"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -151,7 +155,7 @@ where impl ConstraintSatisfactionProblem for SetCovering where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // For each element, at least one set containing it must be selected diff --git a/src/models/set/set_packing.rs b/src/models/set/set_packing.rs index 977acac..1fd5a34 100644 --- a/src/models/set/set_packing.rs +++ b/src/models/set/set_packing.rs @@ -3,6 +3,7 @@ //! The Set Packing problem asks for a maximum weight collection of //! pairwise disjoint sets. +use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -107,8 +108,11 @@ impl SetPacking { impl Problem for SetPacking where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "SetPacking"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { @@ -141,7 +145,7 @@ where impl ConstraintSatisfactionProblem for SetPacking where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { fn constraints(&self) -> Vec { // For each pair of overlapping sets, at most one can be selected diff --git a/src/models/specialized/biclique_cover.rs b/src/models/specialized/biclique_cover.rs index 0298765..bec17f7 100644 --- a/src/models/specialized/biclique_cover.rs +++ b/src/models/specialized/biclique_cover.rs @@ -3,6 +3,7 @@ //! The Biclique Cover problem asks for the minimum number of bicliques //! (complete bipartite subgraphs) needed to cover all edges of a bipartite graph. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -181,6 +182,9 @@ impl BicliqueCover { } impl Problem for BicliqueCover { + const NAME: &'static str = "BicliqueCover"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/bmf.rs b/src/models/specialized/bmf.rs index 390ae53..65c37b0 100644 --- a/src/models/specialized/bmf.rs +++ b/src/models/specialized/bmf.rs @@ -4,6 +4,7 @@ //! the boolean product B ⊙ C approximates A. //! The boolean product `(B ⊙ C)[i,j] = OR_k (B[i,k] AND C[k,j])`. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -157,6 +158,9 @@ impl BMF { } impl Problem for BMF { + const NAME: &'static str = "BMF"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/circuit.rs b/src/models/specialized/circuit.rs index 2a86eb1..06f199a 100644 --- a/src/models/specialized/circuit.rs +++ b/src/models/specialized/circuit.rs @@ -3,6 +3,7 @@ //! CircuitSAT represents a boolean circuit satisfiability problem. //! The goal is to find variable assignments that satisfy the circuit constraints. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -268,8 +269,11 @@ impl CircuitSAT { impl Problem for CircuitSAT where - W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign, + W: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static, { + const NAME: &'static str = "CircuitSAT"; + type GraphType = SimpleGraph; + type Weight = W; type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/factoring.rs b/src/models/specialized/factoring.rs index 593a127..6366941 100644 --- a/src/models/specialized/factoring.rs +++ b/src/models/specialized/factoring.rs @@ -3,6 +3,7 @@ //! The Factoring problem represents integer factorization as a computational problem. //! Given a number N, find two factors (a, b) such that a * b = N. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -95,6 +96,9 @@ fn int_to_bits(n: u64, num_bits: usize) -> Vec { } impl Problem for Factoring { + const NAME: &'static str = "Factoring"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/paintshop.rs b/src/models/specialized/paintshop.rs index c418176..b87620f 100644 --- a/src/models/specialized/paintshop.rs +++ b/src/models/specialized/paintshop.rs @@ -5,6 +5,7 @@ //! one color at its first occurrence and another at its second. //! The goal is to minimize color switches between adjacent positions. +use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -144,6 +145,9 @@ impl PaintShop { } impl Problem for PaintShop { + const NAME: &'static str = "PaintShop"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/polynomial.rs b/src/polynomial.rs new file mode 100644 index 0000000..8a1f4eb --- /dev/null +++ b/src/polynomial.rs @@ -0,0 +1,211 @@ +//! Polynomial representation for reduction overhead. + +use crate::types::ProblemSize; +use std::ops::Add; + +/// A monomial: coefficient × Π(variable^exponent) +#[derive(Clone, Debug, PartialEq)] +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 + } +} + +/// A polynomial: Σ monomials +#[derive(Clone, Debug, PartialEq)] +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)] } + } + + 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() + } +} + +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) + }; +} + +#[cfg(test)] +mod tests { + 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 + } +} diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 31c460e..1025b6e 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -186,7 +186,7 @@ pub struct ReductionCircuitToSG { impl ReductionResult for ReductionCircuitToSG where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Source = CircuitSAT; type Target = SpinGlass; @@ -427,7 +427,7 @@ fn process_assignment( impl ReduceTo> for CircuitSAT where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionCircuitToSG; @@ -469,7 +469,8 @@ mod tests { + AddAssign + From + std::ops::Mul - + std::fmt::Debug, + + std::fmt::Debug + + 'static, { let solver = BruteForce::new(); let solutions = solver.find_best(&gadget.problem); @@ -968,3 +969,20 @@ mod tests { assert!(sg.num_spins() >= 3); // At least c, x, y } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "CircuitSAT", + target_name: "SpinGlass", + source_graph: "Circuit", + target_graph: "SpinGlassGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_spins", poly!(num_assignments)), + ("num_interactions", poly!(num_assignments)), + ]), + } +} diff --git a/src/rules/cost.rs b/src/rules/cost.rs new file mode 100644 index 0000000..745b18b --- /dev/null +++ b/src/rules/cost.rs @@ -0,0 +1,174 @@ +//! Cost functions for reduction path optimization. + +use crate::rules::registry::ReductionOverhead; +use crate::types::ProblemSize; + +/// User-defined cost function for path optimization. +pub trait PathCostFn { + /// Compute cost of taking an edge given current problem size. + fn edge_cost(&self, overhead: &ReductionOverhead, current_size: &ProblemSize) -> f64; +} + +/// Minimize a single output field. +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 + } +} + +/// 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); + 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); + 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); + 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 + } +} + +/// Minimize number of reduction steps. +pub struct MinimizeSteps; + +impl PathCostFn for MinimizeSteps { + fn edge_cost(&self, _overhead: &ReductionOverhead, _size: &ProblemSize) -> f64 { + 1.0 + } +} + +/// Custom cost function from closure. +pub struct CustomCost(pub F); + +impl f64> PathCostFn for CustomCost { + fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 { + (self.0)(overhead, size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::polynomial::Polynomial; + + fn test_overhead() -> ReductionOverhead { + ReductionOverhead::new(vec![ + ("n", Polynomial::var("n").scale(2.0)), + ("m", Polynomial::var("m")), + ]) + } + + #[test] + fn test_minimize_single() { + let cost_fn = Minimize("n"); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 20.0); // 2 * 10 + } + + #[test] + fn test_minimize_weighted() { + let cost_fn = MinimizeWeighted(vec![("n", 1.0), ("m", 2.0)]); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + // output n = 20, output m = 5 + // cost = 1.0 * 20 + 2.0 * 5 = 30 + assert_eq!(cost_fn.edge_cost(&overhead, &size), 30.0); + } + + #[test] + fn test_minimize_steps() { + let cost_fn = MinimizeSteps; + let size = ProblemSize::new(vec![("n", 100)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 1.0); + } + + #[test] + fn test_minimize_max() { + let cost_fn = MinimizeMax(vec!["n", "m"]); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + // output n = 20, output m = 5 + // max(20, 5) = 20 + assert_eq!(cost_fn.edge_cost(&overhead, &size), 20.0); + } + + #[test] + fn test_minimize_lexicographic() { + let cost_fn = MinimizeLexicographic(vec!["n", "m"]); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + // output n = 20, output m = 5 + // cost = 20 * 1.0 + 5 * 1e-10 = 20.0000000005 + let cost = cost_fn.edge_cost(&overhead, &size); + assert!(cost > 20.0 && cost < 20.001); + } + + #[test] + fn test_custom_cost() { + let cost_fn = CustomCost(|overhead: &ReductionOverhead, size: &ProblemSize| { + let output = overhead.evaluate_output_size(size); + (output.get("n").unwrap_or(0) + output.get("m").unwrap_or(0)) as f64 + }); + let size = ProblemSize::new(vec![("n", 10), ("m", 5)]); + let overhead = test_overhead(); + + // output n = 20, output m = 5 + // custom = 20 + 5 = 25 + assert_eq!(cost_fn.edge_cost(&overhead, &size), 25.0); + } + + #[test] + fn test_minimize_missing_field() { + let cost_fn = Minimize("nonexistent"); + let size = ProblemSize::new(vec![("n", 10)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 0.0); + } + + #[test] + fn test_minimize_max_empty() { + let cost_fn = MinimizeMax(vec![]); + let size = ProblemSize::new(vec![("n", 10)]); + let overhead = test_overhead(); + + assert_eq!(cost_fn.edge_cost(&overhead, &size), 0.0); + } +} diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index 901cc22..0ebca92 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -572,3 +572,19 @@ mod tests { ); } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Factoring", + target_name: "CircuitSAT", + source_graph: "Factoring", + target_graph: "Circuit", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_gates", poly!(num_bits_first^2)), + ]), + } +} diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 8035bf5..d96c53e 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -2,12 +2,24 @@ //! //! The graph uses type-erased names (e.g., "SpinGlass" instead of "SpinGlass") //! for topology, allowing path finding regardless of weight type parameters. - +//! +//! This module implements set-theoretic validation for path finding: +//! - Graph hierarchy is built from `GraphSubtypeEntry` registrations +//! - Reduction applicability uses subtype relationships: A <= C and D <= B +//! - Dijkstra's algorithm with custom cost functions for optimal paths + +use crate::graph_types::GraphSubtypeEntry; +use crate::rules::cost::PathCostFn; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; +use crate::types::ProblemSize; +use ordered_float::OrderedFloat; use petgraph::algo::all_simple_paths; use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::visit::EdgeRef; use serde::Serialize; use std::any::TypeId; -use std::collections::HashMap; +use std::cmp::Reverse; +use std::collections::{BinaryHeap, HashMap, HashSet}; /// JSON-serializable representation of the reduction graph. #[derive(Debug, Clone, Serialize)] @@ -73,42 +85,136 @@ impl ReductionPath { } } +/// Edge data for a reduction. +#[derive(Clone, Debug)] +pub struct ReductionEdge { + /// Graph type of source problem (e.g., "SimpleGraph"). + pub source_graph: &'static str, + /// Graph type of target problem. + pub target_graph: &'static str, + /// Overhead information for cost calculations. + pub overhead: ReductionOverhead, +} + /// Runtime graph of all registered reductions. /// /// Uses type-erased names for the graph topology, so `MaxCut` and `MaxCut` /// map to the same node "MaxCut". This allows finding reduction paths regardless /// of weight type parameters. +/// +/// The graph supports: +/// - Auto-discovery of reductions from `inventory::iter::` +/// - Graph hierarchy from `inventory::iter::` +/// - Set-theoretic validation for path finding +/// - Dijkstra with custom cost functions pub struct ReductionGraph { /// Graph with base type names as node data. - graph: DiGraph<&'static str, ()>, + graph: DiGraph<&'static str, ReductionEdge>, /// Map from base type name to node index. name_indices: HashMap<&'static str, NodeIndex>, /// Map from TypeId to base type name (for generic API compatibility). type_to_name: HashMap, + /// Graph hierarchy: subtype -> set of supertypes (transitively closed). + graph_hierarchy: HashMap<&'static str, HashSet<&'static str>>, } impl ReductionGraph { - /// Create a new reduction graph with all registered reductions. + /// Create a new reduction graph with all registered reductions from inventory. pub fn new() -> Self { let mut graph = DiGraph::new(); let mut name_indices = HashMap::new(); let mut type_to_name = HashMap::new(); - // Register all problem types + // Build graph hierarchy from GraphSubtypeEntry registrations + let graph_hierarchy = Self::build_graph_hierarchy(); + + // First, register all problem types (for TypeId mapping) Self::register_types(&mut graph, &mut name_indices, &mut type_to_name); - // Register all reductions as edges + // Then, register reductions from inventory (auto-discovery) + for entry in inventory::iter:: { + // Ensure source node exists + if !name_indices.contains_key(entry.source_name) { + let idx = graph.add_node(entry.source_name); + name_indices.insert(entry.source_name, idx); + } + // Ensure target node exists + if !name_indices.contains_key(entry.target_name) { + let idx = graph.add_node(entry.target_name); + name_indices.insert(entry.target_name, idx); + } + + // Add edge with metadata + let src = name_indices[entry.source_name]; + let dst = name_indices[entry.target_name]; + + // Check if edge already exists (avoid duplicates) + if graph.find_edge(src, dst).is_none() { + graph.add_edge( + src, + dst, + ReductionEdge { + source_graph: entry.source_graph, + target_graph: entry.target_graph, + overhead: entry.overhead(), + }, + ); + } + } + + // Also register manual reductions for backward compatibility Self::register_reductions(&mut graph, &name_indices); Self { graph, name_indices, type_to_name, + graph_hierarchy, } } + /// Build graph hierarchy from GraphSubtypeEntry registrations. + /// Computes the transitive closure of the subtype relationship. + fn build_graph_hierarchy() -> HashMap<&'static str, HashSet<&'static str>> { + let mut supertypes: HashMap<&'static str, HashSet<&'static str>> = HashMap::new(); + + // Collect direct subtype relationships + for entry in inventory::iter:: { + supertypes.entry(entry.subtype).or_default().insert(entry.supertype); + } + + // Compute transitive closure + loop { + let mut changed = false; + let types: Vec<_> = supertypes.keys().copied().collect(); + + for sub in &types { + let current: Vec<_> = supertypes + .get(sub) + .map(|s| s.iter().copied().collect()) + .unwrap_or_default(); + + for sup in current { + if let Some(sup_supers) = supertypes.get(sup).cloned() { + for ss in sup_supers { + if supertypes.entry(sub).or_default().insert(ss) { + changed = true; + } + } + } + } + } + + if !changed { + break; + } + } + + supertypes + } + fn register_types( - graph: &mut DiGraph<&'static str, ()>, + graph: &mut DiGraph<&'static str, ReductionEdge>, name_indices: &mut HashMap<&'static str, NodeIndex>, type_to_name: &mut HashMap, ) { @@ -164,16 +270,25 @@ impl ReductionGraph { } fn register_reductions( - graph: &mut DiGraph<&'static str, ()>, + graph: &mut DiGraph<&'static str, ReductionEdge>, name_indices: &HashMap<&'static str, NodeIndex>, ) { - // Add an edge between two problem types by name. + // Add an edge between two problem types by name (with default overhead). + // This is for backward compatibility with manually registered reductions. macro_rules! add_edge { ($src:expr => $dst:expr) => { if let (Some(&src), Some(&dst)) = (name_indices.get($src), name_indices.get($dst)) { // Avoid duplicate edges if graph.find_edge(src, dst).is_none() { - graph.add_edge(src, dst, ()); + graph.add_edge( + src, + dst, + ReductionEdge { + source_graph: "SimpleGraph", + target_graph: "SimpleGraph", + overhead: ReductionOverhead::default(), + }, + ); } } }; @@ -205,6 +320,130 @@ impl ReductionGraph { add_edge!("Factoring" => "CircuitSAT"); } + /// Check if `sub` is a subtype of `sup` (or equal). + pub fn is_graph_subtype(&self, sub: &str, sup: &str) -> bool { + sub == sup + || self + .graph_hierarchy + .get(sub) + .map(|s| s.contains(sup)) + .unwrap_or(false) + } + + /// Check if a reduction rule can be used. + /// + /// For a reduction from problem A (on graph type G_A) to problem B (on graph type G_B), + /// using a rule that reduces C (on G_C) to D (on G_D): + /// + /// The rule is applicable if: + /// - G_A is a subtype of G_C (our source graph is more specific than rule requires) + /// - G_D is a subtype of G_B (rule produces a graph that fits our target requirement) + pub fn rule_applicable( + &self, + want_source_graph: &str, + want_target_graph: &str, + rule_source_graph: &str, + rule_target_graph: &str, + ) -> bool { + // A <= C: our source must be subtype of rule's source (or equal) + // D <= B: rule's target must be subtype of our target (or equal) + self.is_graph_subtype(want_source_graph, rule_source_graph) + && self.is_graph_subtype(rule_target_graph, want_target_graph) + } + + /// Find the cheapest path using a custom cost function. + /// + /// Uses Dijkstra's algorithm with set-theoretic validation. + /// + /// # Arguments + /// - `source`: (problem_name, graph_type) for source + /// - `target`: (problem_name, graph_type) for target + /// - `input_size`: Initial problem size for cost calculations + /// - `cost_fn`: Custom cost function for path optimization + /// + /// # Returns + /// The cheapest path if one exists that satisfies the graph type constraints. + pub fn find_cheapest_path( + &self, + source: (&str, &str), + target: (&str, &str), + input_size: &ProblemSize, + cost_fn: &C, + ) -> Option { + let src_idx = *self.name_indices.get(source.0)?; + let dst_idx = *self.name_indices.get(target.0)?; + + let mut costs: HashMap = HashMap::new(); + let mut sizes: HashMap = HashMap::new(); + let mut prev: HashMap = HashMap::new(); + let mut heap = BinaryHeap::new(); + + costs.insert(src_idx, 0.0); + sizes.insert(src_idx, input_size.clone()); + heap.push(Reverse((OrderedFloat(0.0), src_idx))); + + while let Some(Reverse((cost, node))) = heap.pop() { + if node == dst_idx { + return Some(self.reconstruct_path(&prev, src_idx, dst_idx)); + } + + if cost.0 > *costs.get(&node).unwrap_or(&f64::INFINITY) { + continue; + } + + let current_size = match sizes.get(&node) { + Some(s) => s.clone(), + None => continue, + }; + + for edge_ref in self.graph.edges(node) { + let edge = edge_ref.weight(); + let next = edge_ref.target(); + + // Check set-theoretic applicability + if !self.rule_applicable(source.1, target.1, edge.source_graph, edge.target_graph) { + continue; + } + + let edge_cost = cost_fn.edge_cost(&edge.overhead, ¤t_size); + let new_cost = cost.0 + edge_cost; + let new_size = edge.overhead.evaluate_output_size(¤t_size); + + if new_cost < *costs.get(&next).unwrap_or(&f64::INFINITY) { + costs.insert(next, new_cost); + sizes.insert(next, new_size); + prev.insert(next, (node, edge_ref.id())); + heap.push(Reverse((OrderedFloat(new_cost), next))); + } + } + } + + None + } + + /// Reconstruct a path from the predecessor map. + fn reconstruct_path( + &self, + prev: &HashMap, + src: NodeIndex, + dst: NodeIndex, + ) -> ReductionPath { + let mut path = vec![self.graph[dst]]; + let mut current = dst; + + while current != src { + if let Some(&(prev_node, _)) = prev.get(¤t) { + path.push(self.graph[prev_node]); + current = prev_node; + } else { + break; + } + } + + path.reverse(); + ReductionPath { type_names: path } + } + /// Find all paths from source to target type. /// /// Uses type-erased names, so `find_paths::, SpinGlass>()` @@ -297,6 +536,11 @@ impl ReductionGraph { pub fn num_reductions(&self) -> usize { self.graph.edge_count() } + + /// Get the graph hierarchy (for inspection/testing). + pub fn graph_hierarchy(&self) -> &HashMap<&'static str, HashSet<&'static str>> { + &self.graph_hierarchy + } } impl Default for ReductionGraph { @@ -390,7 +634,6 @@ impl ReductionGraph { "other" } } - } #[cfg(test)] @@ -398,6 +641,7 @@ mod tests { use super::*; use crate::models::graph::{IndependentSet, VertexCovering}; use crate::models::set::SetPacking; + use crate::rules::cost::MinimizeSteps; #[test] fn test_find_direct_path() { @@ -446,10 +690,14 @@ mod tests { let graph = ReductionGraph::new(); // Different weight types should find the same path (type-erased) - let paths_i32 = - graph.find_paths::, crate::models::optimization::SpinGlass>(); - let paths_f64 = - graph.find_paths::, crate::models::optimization::SpinGlass>(); + let paths_i32 = graph.find_paths::< + crate::models::graph::MaxCut, + crate::models::optimization::SpinGlass, + >(); + let paths_f64 = graph.find_paths::< + crate::models::graph::MaxCut, + crate::models::optimization::SpinGlass, + >(); // Both should find paths since we use type-erased names assert!(!paths_i32.is_empty()); @@ -682,9 +930,7 @@ mod tests { #[test] fn test_empty_path_source_target() { - let path = ReductionPath { - type_names: vec![], - }; + let path = ReductionPath { type_names: vec![] }; assert!(path.is_empty()); assert_eq!(path.len(), 0); assert!(path.source().is_none()); @@ -824,4 +1070,206 @@ mod tests { "Factoring -> CircuitSAT should be unidirectional" ); } + + // New tests for set-theoretic path finding + + #[test] + fn test_graph_hierarchy_built() { + let graph = ReductionGraph::new(); + let hierarchy = graph.graph_hierarchy(); + + // Should have relationships from GraphSubtypeEntry registrations + // UnitDiskGraph -> PlanarGraph -> SimpleGraph + // BipartiteGraph -> SimpleGraph + assert!( + hierarchy.get("UnitDiskGraph").map(|s| s.contains("SimpleGraph")).unwrap_or(false), + "UnitDiskGraph should have SimpleGraph as supertype" + ); + assert!( + hierarchy.get("PlanarGraph").map(|s| s.contains("SimpleGraph")).unwrap_or(false), + "PlanarGraph should have SimpleGraph as supertype" + ); + } + + #[test] + fn test_is_graph_subtype_reflexive() { + let graph = ReductionGraph::new(); + + // Every type is a subtype of itself + assert!(graph.is_graph_subtype("SimpleGraph", "SimpleGraph")); + assert!(graph.is_graph_subtype("PlanarGraph", "PlanarGraph")); + assert!(graph.is_graph_subtype("UnitDiskGraph", "UnitDiskGraph")); + } + + #[test] + fn test_is_graph_subtype_direct() { + let graph = ReductionGraph::new(); + + // Direct subtype relationships + assert!(graph.is_graph_subtype("PlanarGraph", "SimpleGraph")); + assert!(graph.is_graph_subtype("BipartiteGraph", "SimpleGraph")); + assert!(graph.is_graph_subtype("UnitDiskGraph", "PlanarGraph")); + } + + #[test] + fn test_is_graph_subtype_transitive() { + let graph = ReductionGraph::new(); + + // Transitive closure: UnitDiskGraph -> PlanarGraph -> SimpleGraph + assert!(graph.is_graph_subtype("UnitDiskGraph", "SimpleGraph")); + } + + #[test] + fn test_is_graph_subtype_not_supertype() { + let graph = ReductionGraph::new(); + + // SimpleGraph is NOT a subtype of PlanarGraph (only the reverse) + assert!(!graph.is_graph_subtype("SimpleGraph", "PlanarGraph")); + assert!(!graph.is_graph_subtype("SimpleGraph", "UnitDiskGraph")); + } + + #[test] + fn test_rule_applicable_same_graphs() { + let graph = ReductionGraph::new(); + + // Rule for SimpleGraph -> SimpleGraph applies to same + assert!(graph.rule_applicable("SimpleGraph", "SimpleGraph", "SimpleGraph", "SimpleGraph")); + } + + #[test] + fn test_rule_applicable_subtype_source() { + let graph = ReductionGraph::new(); + + // Rule for SimpleGraph -> SimpleGraph applies when source is PlanarGraph + // (because PlanarGraph <= SimpleGraph) + assert!(graph.rule_applicable("PlanarGraph", "SimpleGraph", "SimpleGraph", "SimpleGraph")); + } + + #[test] + fn test_rule_applicable_subtype_target() { + let graph = ReductionGraph::new(); + + // Rule producing PlanarGraph applies when we want SimpleGraph + // (because PlanarGraph <= SimpleGraph) + assert!(graph.rule_applicable("SimpleGraph", "SimpleGraph", "SimpleGraph", "PlanarGraph")); + } + + #[test] + fn test_rule_not_applicable_wrong_source() { + let graph = ReductionGraph::new(); + + // Rule requiring PlanarGraph does NOT apply to SimpleGraph source + // (because SimpleGraph is NOT <= PlanarGraph) + assert!(!graph.rule_applicable("SimpleGraph", "SimpleGraph", "PlanarGraph", "SimpleGraph")); + } + + #[test] + fn test_rule_not_applicable_wrong_target() { + let graph = ReductionGraph::new(); + + // Rule producing SimpleGraph does NOT apply when we need PlanarGraph + // (because SimpleGraph is NOT <= PlanarGraph) + assert!(!graph.rule_applicable("SimpleGraph", "PlanarGraph", "SimpleGraph", "SimpleGraph")); + } + + #[test] + fn test_find_cheapest_path_minimize_steps() { + let graph = ReductionGraph::new(); + let cost_fn = MinimizeSteps; + let input_size = ProblemSize::new(vec![("n", 10), ("m", 20)]); + + // Find path from IndependentSet to VertexCovering on SimpleGraph + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("VertexCovering", "SimpleGraph"), + &input_size, + &cost_fn, + ); + + assert!(path.is_some()); + let path = path.unwrap(); + assert_eq!(path.len(), 1); // Direct path + } + + #[test] + fn test_find_cheapest_path_multi_step() { + let graph = ReductionGraph::new(); + let cost_fn = MinimizeSteps; + let input_size = ProblemSize::new(vec![("num_vertices", 10), ("num_edges", 20)]); + + // Find multi-step path where all edges use compatible graph types + // IndependentSet (SimpleGraph) -> SetPacking (SetSystem) -> IndependentSet (SimpleGraph) + // This tests the algorithm can find multi-step paths with consistent graph types + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("SetPacking", "SetSystem"), + &input_size, + &cost_fn, + ); + + assert!(path.is_some()); + let path = path.unwrap(); + assert_eq!(path.len(), 1); // Direct path: IndependentSet -> SetPacking + } + + #[test] + fn test_find_cheapest_path_no_path() { + let graph = ReductionGraph::new(); + let cost_fn = MinimizeSteps; + let input_size = ProblemSize::new(vec![("n", 10)]); + + // No path from IndependentSet to QUBO + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("QUBO", "SimpleGraph"), + &input_size, + &cost_fn, + ); + + assert!(path.is_none()); + } + + #[test] + fn test_find_cheapest_path_unknown_source() { + let graph = ReductionGraph::new(); + let cost_fn = MinimizeSteps; + let input_size = ProblemSize::new(vec![("n", 10)]); + + let path = graph.find_cheapest_path( + ("UnknownProblem", "SimpleGraph"), + ("VertexCovering", "SimpleGraph"), + &input_size, + &cost_fn, + ); + + assert!(path.is_none()); + } + + #[test] + fn test_find_cheapest_path_unknown_target() { + let graph = ReductionGraph::new(); + let cost_fn = MinimizeSteps; + let input_size = ProblemSize::new(vec![("n", 10)]); + + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("UnknownProblem", "SimpleGraph"), + &input_size, + &cost_fn, + ); + + assert!(path.is_none()); + } + + #[test] + fn test_reduction_edge_struct() { + let edge = ReductionEdge { + source_graph: "PlanarGraph", + target_graph: "SimpleGraph", + overhead: ReductionOverhead::default(), + }; + + assert_eq!(edge.source_graph, "PlanarGraph"); + assert_eq!(edge.target_graph, "SimpleGraph"); + } } diff --git a/src/rules/independentset_setpacking.rs b/src/rules/independentset_setpacking.rs index 3995590..a8cdb30 100644 --- a/src/rules/independentset_setpacking.rs +++ b/src/rules/independentset_setpacking.rs @@ -21,7 +21,7 @@ pub struct ReductionISToSP { impl ReductionResult for ReductionISToSP where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = IndependentSet; type Target = SetPacking; @@ -46,7 +46,7 @@ where impl ReduceTo> for IndependentSet where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionISToSP; @@ -79,7 +79,7 @@ pub struct ReductionSPToIS { impl ReductionResult for ReductionSPToIS where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = SetPacking; type Target = IndependentSet; @@ -104,7 +104,7 @@ where impl ReduceTo> for SetPacking where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSPToIS; @@ -246,3 +246,33 @@ mod tests { assert_eq!(is_problem.num_edges(), 0); } } + +// Register reductions with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "IndependentSet", + target_name: "SetPacking", + source_graph: "SimpleGraph", + target_graph: "SetSystem", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_sets", poly!(num_vertices)), + ("num_elements", poly!(num_vertices)), + ]), + } +} + +inventory::submit! { + ReductionEntry { + source_name: "SetPacking", + target_name: "IndependentSet", + source_graph: "SetSystem", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_sets)), + ("num_edges", poly!(num_sets)), + ]), + } +} diff --git a/src/rules/matching_setpacking.rs b/src/rules/matching_setpacking.rs index fb8a890..dc79b0d 100644 --- a/src/rules/matching_setpacking.rs +++ b/src/rules/matching_setpacking.rs @@ -20,7 +20,7 @@ pub struct ReductionMatchingToSP { impl ReductionResult for ReductionMatchingToSP where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = Matching; type Target = SetPacking; @@ -45,7 +45,7 @@ where impl ReduceTo> for Matching where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionMatchingToSP; @@ -261,3 +261,20 @@ mod tests { assert_eq!(sp_solutions.len(), 3); } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Matching", + target_name: "SetPacking", + source_graph: "SimpleGraph", + target_graph: "SetSystem", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_sets", poly!(num_edges)), + ("num_elements", poly!(num_vertices)), + ]), + } +} diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 534b19d..ac502ad 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -1,5 +1,10 @@ //! Reduction rules between NP-hard problems. +pub mod cost; +pub mod registry; +pub use cost::{CustomCost, Minimize, MinimizeLexicographic, MinimizeMax, MinimizeSteps, MinimizeWeighted, PathCostFn}; +pub use registry::{ReductionEntry, ReductionOverhead}; + mod circuit_spinglass; mod graph; mod traits; @@ -30,7 +35,7 @@ mod setpacking_ilp; #[cfg(feature = "ilp")] mod vertexcovering_ilp; -pub use graph::{EdgeJson, NodeJson, ReductionGraph, ReductionGraphJson, ReductionPath}; +pub use graph::{EdgeJson, NodeJson, ReductionEdge, ReductionGraph, ReductionGraphJson, ReductionPath}; pub use traits::{ReduceTo, ReductionResult}; pub use independentset_setpacking::{ReductionISToSP, ReductionSPToIS}; pub use matching_setpacking::ReductionMatchingToSP; diff --git a/src/rules/registry.rs b/src/rules/registry.rs new file mode 100644 index 0000000..3f421bc --- /dev/null +++ b/src/rules/registry.rs @@ -0,0 +1,138 @@ +//! Automatic reduction registration via inventory. + +use crate::polynomial::Polynomial; +use crate::types::ProblemSize; + +/// Overhead specification for a reduction. +#[derive(Clone, Debug, Default)] +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)>, +} + +impl ReductionOverhead { + pub fn new(output_size: Vec<(&'static str, Polynomial)>) -> Self { + Self { output_size } + } + + /// Evaluate output size given input size. + /// + /// Uses `round()` for the f64 to usize conversion because polynomial 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) + } +} + + +/// A registered reduction entry for static inventory registration. +/// Uses function pointer to lazily create the overhead (avoids static allocation issues). +pub struct ReductionEntry { + /// Base name of source problem (e.g., "IndependentSet"). + pub source_name: &'static str, + /// Base name of target problem (e.g., "VertexCovering"). + pub target_name: &'static str, + /// Graph type of source problem (e.g., "SimpleGraph"). + pub source_graph: &'static str, + /// Graph type of target problem. + pub target_graph: &'static str, + /// Function to create overhead information (lazy evaluation for static context). + pub overhead_fn: fn() -> ReductionOverhead, +} + +impl ReductionEntry { + /// Get the overhead by calling the function. + pub fn overhead(&self) -> ReductionOverhead { + (self.overhead_fn)() + } +} + +impl std::fmt::Debug for ReductionEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ReductionEntry") + .field("source_name", &self.source_name) + .field("target_name", &self.target_name) + .field("source_graph", &self.source_graph) + .field("target_graph", &self.target_graph) + .field("overhead", &self.overhead()) + .finish() + } +} + +inventory::collect!(ReductionEntry); + +#[cfg(test)] +mod tests { + use super::*; + use crate::poly; + + #[test] + fn test_reduction_overhead_evaluate() { + let overhead = ReductionOverhead::new(vec![ + ("n", poly!(3 * m)), + ("m", poly!(m^2)), + ]); + + let input = ProblemSize::new(vec![("m", 4)]); + let output = overhead.evaluate_output_size(&input); + + assert_eq!(output.get("n"), Some(12)); // 3 * 4 + assert_eq!(output.get("m"), Some(16)); // 4^2 + } + + #[test] + fn test_reduction_overhead_default() { + let overhead = ReductionOverhead::default(); + assert!(overhead.output_size.is_empty()); + } + + #[test] + fn test_reduction_entry_overhead() { + let entry = ReductionEntry { + source_name: "TestSource", + target_name: "TestTarget", + source_graph: "SimpleGraph", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![("n", poly!(2 * n))]), + }; + + let overhead = entry.overhead(); + let input = ProblemSize::new(vec![("n", 5)]); + let output = overhead.evaluate_output_size(&input); + assert_eq!(output.get("n"), Some(10)); + } + + #[test] + fn test_reduction_entry_debug() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_graph: "SimpleGraph", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::default(), + }; + + let debug_str = format!("{:?}", entry); + assert!(debug_str.contains("A")); + assert!(debug_str.contains("B")); + } + + #[test] + fn test_reduction_entries_registered() { + let entries: Vec<_> = inventory::iter::().collect(); + + // Should have at least some registered reductions + assert!(entries.len() >= 10); + + // Check specific reductions exist + assert!(entries + .iter() + .any(|e| e.source_name == "IndependentSet" && e.target_name == "VertexCovering")); + } +} diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index de07027..f036fb6 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -228,7 +228,7 @@ pub struct ReductionSATToColoring { impl ReductionResult for ReductionSATToColoring where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = Satisfiability; type Target = Coloring; @@ -310,7 +310,7 @@ impl ReductionSATToColoring { impl ReduceTo for Satisfiability where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSATToColoring; @@ -647,3 +647,20 @@ mod tests { assert_eq!(extracted2, vec![1]); } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Satisfiability", + target_name: "Coloring", + source_graph: "CNF", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(3 * num_vars)), + ("num_colors", poly!(3)), + ]), + } +} diff --git a/src/rules/sat_dominatingset.rs b/src/rules/sat_dominatingset.rs index 8d113d7..3d486ce 100644 --- a/src/rules/sat_dominatingset.rs +++ b/src/rules/sat_dominatingset.rs @@ -43,7 +43,7 @@ pub struct ReductionSATToDS { impl ReductionResult for ReductionSATToDS where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = Satisfiability; type Target = DominatingSet; @@ -128,7 +128,7 @@ impl ReductionSATToDS { impl ReduceTo> for Satisfiability where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSATToDS; @@ -507,3 +507,20 @@ mod tests { assert_eq!(ds_problem.num_edges(), 8); } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Satisfiability", + target_name: "DominatingSet", + source_graph: "CNF", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vars)), + ("num_edges", poly!(num_clauses)), + ]), + } +} diff --git a/src/rules/sat_independentset.rs b/src/rules/sat_independentset.rs index 02cba87..6527b50 100644 --- a/src/rules/sat_independentset.rs +++ b/src/rules/sat_independentset.rs @@ -68,7 +68,7 @@ pub struct ReductionSATToIS { impl ReductionResult for ReductionSATToIS where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = Satisfiability; type Target = IndependentSet; @@ -124,7 +124,7 @@ impl ReductionSATToIS { impl ReduceTo> for Satisfiability where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSATToIS; @@ -497,3 +497,20 @@ mod tests { assert_eq!(literals[1], BoolVar::new(1, true)); // NOT x2 } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Satisfiability", + target_name: "IndependentSet", + source_graph: "CNF", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(7 * num_clauses)), + ("num_edges", poly!(21 * num_clauses)), + ]), + } +} diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 2a28885..87f4578 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -29,7 +29,7 @@ pub struct ReductionSATToKSAT { impl ReductionResult for ReductionSATToKSAT where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Source = Satisfiability; type Target = KSatisfiability; @@ -128,7 +128,7 @@ macro_rules! impl_sat_to_ksat { ($k:expr) => { impl ReduceTo> for Satisfiability where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSATToKSAT<$k, W>; @@ -175,7 +175,7 @@ pub struct ReductionKSATToSAT { impl ReductionResult for ReductionKSATToSAT where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Source = KSatisfiability; type Target = Satisfiability; @@ -200,7 +200,7 @@ where impl ReduceTo> for KSatisfiability where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionKSATToSAT; @@ -538,3 +538,33 @@ mod tests { assert!(!ksat_satisfiable); } } + +// Register reductions with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "Satisfiability", + target_name: "KSatisfiability", + source_graph: "CNF", + target_graph: "KCNF", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_clauses", poly!(num_clauses)), + ("num_vars", poly!(num_vars)), + ]), + } +} + +inventory::submit! { + ReductionEntry { + source_name: "KSatisfiability", + target_name: "Satisfiability", + source_graph: "KCNF", + target_graph: "CNF", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_clauses", poly!(num_clauses)), + ("num_vars", poly!(num_vars)), + ]), + } +} diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 0ef871e..e66a6f6 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -20,7 +20,7 @@ pub struct ReductionMaxCutToSG { impl ReductionResult for ReductionMaxCutToSG where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Source = MaxCut; type Target = SpinGlass; @@ -44,7 +44,7 @@ where impl ReduceTo> for MaxCut where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionMaxCutToSG; @@ -97,7 +97,7 @@ pub struct ReductionSGToMaxCut { impl ReductionResult for ReductionSGToMaxCut where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Source = SpinGlass; type Target = MaxCut; @@ -134,7 +134,7 @@ where impl ReduceTo> for SpinGlass where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionSGToMaxCut; @@ -256,3 +256,33 @@ mod tests { assert_eq!(interactions.len(), 2); } } + +// Register reductions with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "MaxCut", + target_name: "SpinGlass", + source_graph: "SimpleGraph", + target_graph: "SpinGlassGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_spins", poly!(num_vertices)), + ("num_interactions", poly!(num_edges)), + ]), + } +} + +inventory::submit! { + ReductionEntry { + source_name: "SpinGlass", + target_name: "MaxCut", + source_graph: "SpinGlassGraph", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_spins)), + ("num_edges", poly!(num_interactions)), + ]), + } +} diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 426e660..692853d 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -267,3 +267,31 @@ mod tests { assert_eq!(solutions[0], vec![0], "Should prefer x=0 (s=-1)"); } } + +// Register reductions with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "QUBO", + target_name: "SpinGlass", + source_graph: "QUBOMatrix", + target_graph: "SpinGlassGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_spins", poly!(num_vars)), + ]), + } +} + +inventory::submit! { + ReductionEntry { + source_name: "SpinGlass", + target_name: "QUBO", + source_graph: "SpinGlassGraph", + target_graph: "QUBOMatrix", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vars", poly!(num_spins)), + ]), + } +} diff --git a/src/rules/vertexcovering_independentset.rs b/src/rules/vertexcovering_independentset.rs index 8f3bebc..3cedec2 100644 --- a/src/rules/vertexcovering_independentset.rs +++ b/src/rules/vertexcovering_independentset.rs @@ -18,7 +18,7 @@ pub struct ReductionISToVC { impl ReductionResult for ReductionISToVC where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = IndependentSet; type Target = VertexCovering; @@ -44,7 +44,7 @@ where impl ReduceTo> for IndependentSet where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionISToVC; @@ -70,7 +70,7 @@ pub struct ReductionVCToIS { impl ReductionResult for ReductionVCToIS where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = VertexCovering; type Target = IndependentSet; @@ -95,7 +95,7 @@ where impl ReduceTo> for VertexCovering where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionVCToIS; @@ -207,3 +207,33 @@ mod tests { assert_eq!(target_size.get("num_vertices"), Some(5)); } } + +// Register reductions with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "IndependentSet", + target_name: "VertexCovering", + source_graph: "SimpleGraph", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]), + } +} + +inventory::submit! { + ReductionEntry { + source_name: "VertexCovering", + target_name: "IndependentSet", + source_graph: "SimpleGraph", + target_graph: "SimpleGraph", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]), + } +} diff --git a/src/rules/vertexcovering_setcovering.rs b/src/rules/vertexcovering_setcovering.rs index 717cb7c..2f940ce 100644 --- a/src/rules/vertexcovering_setcovering.rs +++ b/src/rules/vertexcovering_setcovering.rs @@ -20,7 +20,7 @@ pub struct ReductionVCToSC { impl ReductionResult for ReductionVCToSC where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { type Source = VertexCovering; type Target = SetCovering; @@ -46,7 +46,7 @@ where impl ReduceTo> for VertexCovering where - W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From, + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, { type Result = ReductionVCToSC; @@ -264,3 +264,20 @@ mod tests { } } } + +// Register reduction with inventory for auto-discovery +use crate::poly; +use crate::rules::registry::{ReductionEntry, ReductionOverhead}; + +inventory::submit! { + ReductionEntry { + source_name: "VertexCovering", + target_name: "SetCovering", + source_graph: "SimpleGraph", + target_graph: "SetSystem", + overhead_fn: || ReductionOverhead::new(vec![ + ("num_sets", poly!(num_vertices)), + ("num_elements", poly!(num_edges)), + ]), + } +} diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index 426c643..f9955de 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -178,6 +178,7 @@ impl BruteForceFloat for BruteForce { #[cfg(test)] mod tests { use super::*; + use crate::graph_types::SimpleGraph; use crate::types::{EnergyMode, ProblemSize}; // Simple maximization problem: maximize sum of selected weights @@ -187,6 +188,9 @@ mod tests { } impl Problem for MaxSumProblem { + const NAME: &'static str = "MaxSumProblem"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { @@ -222,6 +226,9 @@ mod tests { } impl Problem for MinSumProblem { + const NAME: &'static str = "MinSumProblem"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { @@ -257,6 +264,9 @@ mod tests { } impl Problem for SelectAtMostOneProblem { + const NAME: &'static str = "SelectAtMostOneProblem"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { @@ -391,6 +401,9 @@ mod tests { } impl Problem for FloatProblem { + const NAME: &'static str = "FloatProblem"; + type GraphType = SimpleGraph; + type Weight = f64; type Size = f64; fn num_variables(&self) -> usize { @@ -443,6 +456,9 @@ mod tests { struct NearlyEqualProblem; impl Problem for NearlyEqualProblem { + const NAME: &'static str = "NearlyEqualProblem"; + type GraphType = SimpleGraph; + type Weight = f64; type Size = f64; fn num_variables(&self) -> usize { diff --git a/src/traits.rs b/src/traits.rs index b5650de..5d8f5f9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,6 +1,7 @@ //! Core traits for problem definitions. -use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; +use crate::graph_types::GraphMarker; +use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, NumericWeight, ProblemSize, SolutionSize}; use num_traits::{Num, Zero}; use std::ops::AddAssign; @@ -9,6 +10,15 @@ use std::ops::AddAssign; /// This trait defines the interface for computational problems that can be /// solved by enumeration or reduction to other problems. pub trait Problem: Clone { + /// Base name of this problem type (e.g., "IndependentSet"). + const NAME: &'static str; + + /// The graph type this problem operates on. + type GraphType: GraphMarker; + + /// The weight type for this problem. + type Weight: NumericWeight; + /// The type used for objective/size values. type Size: Clone + PartialOrd + Num + Zero + AddAssign; @@ -108,6 +118,7 @@ pub fn csp_solution_size( #[cfg(test)] mod tests { use super::*; + use crate::graph_types::SimpleGraph; // A simple test problem: select binary variables to maximize sum of weights #[derive(Clone)] @@ -116,6 +127,9 @@ mod tests { } impl Problem for SimpleWeightedProblem { + const NAME: &'static str = "SimpleWeightedProblem"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { @@ -151,6 +165,9 @@ mod tests { } impl Problem for SimpleCsp { + const NAME: &'static str = "SimpleCsp"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { @@ -430,6 +447,9 @@ mod tests { } impl Problem for MultiFlavorProblem { + const NAME: &'static str = "MultiFlavorProblem"; + type GraphType = SimpleGraph; + type Weight = i32; type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/types.rs b/src/types.rs index ecb7608..3fc84af 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,6 +3,16 @@ use serde::{Deserialize, Serialize}; use std::fmt; +/// Marker trait for numeric weight types. +/// +/// Weight subsumption uses Rust's `From` trait: +/// - `i32 → f64` is valid (From for f64 exists) +/// - `f64 → i32` is invalid (no lossless conversion) +pub trait NumericWeight: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static {} + +// Blanket implementation for any type satisfying the bounds +impl NumericWeight for T where T: Clone + Default + PartialOrd + num_traits::Num + num_traits::Zero + std::ops::AddAssign + 'static {} + /// Specifies whether larger or smaller objective values are better. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum EnergyMode { @@ -333,4 +343,14 @@ mod tests { assert_eq!(objective.evaluate(&[1, 0]), 2); assert_eq!(objective.evaluate(&[1, 1]), 3); } + + #[test] + fn test_numeric_weight_impls() { + fn assert_numeric_weight() {} + + assert_numeric_weight::(); + assert_numeric_weight::(); + assert_numeric_weight::(); + assert_numeric_weight::(); + } } diff --git a/tests/set_theoretic_tests.rs b/tests/set_theoretic_tests.rs new file mode 100644 index 0000000..f33f85a --- /dev/null +++ b/tests/set_theoretic_tests.rs @@ -0,0 +1,129 @@ +//! Integration tests for set-theoretic reduction path finding. + +use problemreductions::rules::{ReductionGraph, MinimizeSteps}; +use problemreductions::types::ProblemSize; + +#[test] +fn test_reduction_graph_discovers_registered_reductions() { + let graph = ReductionGraph::new(); + + // Should have discovered reductions from inventory + assert!(graph.num_types() >= 10, "Should have at least 10 problem types"); + assert!(graph.num_reductions() >= 15, "Should have at least 15 reductions"); + + // Specific reductions should exist + assert!(graph.has_direct_reduction_by_name("IndependentSet", "VertexCovering")); + assert!(graph.has_direct_reduction_by_name("MaxCut", "SpinGlass")); + assert!(graph.has_direct_reduction_by_name("Satisfiability", "IndependentSet")); +} + +#[test] +fn test_find_path_with_cost_function() { + let graph = ReductionGraph::new(); + let input_size = ProblemSize::new(vec![("n", 100), ("m", 200)]); + + // Find path from IndependentSet to VertexCovering using SimpleGraph + // This is a direct path where both source and target use SimpleGraph + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("VertexCovering", "SimpleGraph"), + &input_size, + &MinimizeSteps, + ); + + assert!(path.is_some(), "Should find path from IS to VC"); + let path = path.unwrap(); + assert_eq!(path.len(), 1, "Should be a 1-step path"); + assert_eq!(path.source(), Some("IndependentSet")); + assert_eq!(path.target(), Some("VertexCovering")); +} + +#[test] +fn test_multi_step_path() { + let graph = ReductionGraph::new(); + + // Use find_shortest_path_by_name which doesn't validate graph types + // Factoring -> CircuitSAT -> SpinGlass is a 2-step path + let path = graph.find_shortest_path_by_name("Factoring", "SpinGlass"); + + assert!(path.is_some(), "Should find path from Factoring to SpinGlass"); + let path = path.unwrap(); + assert_eq!(path.len(), 2, "Should be a 2-step path"); + assert_eq!(path.type_names, vec!["Factoring", "CircuitSAT", "SpinGlass"]); +} + +#[test] +fn test_graph_hierarchy_built() { + let graph = ReductionGraph::new(); + + // Test the graph hierarchy was built from GraphSubtypeEntry + assert!(graph.is_graph_subtype("UnitDiskGraph", "SimpleGraph")); + assert!(graph.is_graph_subtype("PlanarGraph", "SimpleGraph")); + assert!(graph.is_graph_subtype("BipartiteGraph", "SimpleGraph")); + + // Reflexive + assert!(graph.is_graph_subtype("SimpleGraph", "SimpleGraph")); + + // Non-subtype relationships + assert!(!graph.is_graph_subtype("SimpleGraph", "UnitDiskGraph")); +} + +#[test] +fn test_rule_applicability() { + let graph = ReductionGraph::new(); + + // Rule for SimpleGraph applies to UnitDiskGraph source (UnitDisk <= Simple) + assert!(graph.rule_applicable("UnitDiskGraph", "SimpleGraph", "SimpleGraph", "SimpleGraph")); + + // Rule for UnitDiskGraph doesn't apply to SimpleGraph source (Simple is NOT <= UnitDisk) + assert!(!graph.rule_applicable("SimpleGraph", "SimpleGraph", "UnitDiskGraph", "SimpleGraph")); +} + +#[test] +fn test_bidirectional_reductions() { + let graph = ReductionGraph::new(); + + // IS <-> VC should both be registered + assert!(graph.has_direct_reduction_by_name("IndependentSet", "VertexCovering")); + assert!(graph.has_direct_reduction_by_name("VertexCovering", "IndependentSet")); + + // MaxCut <-> SpinGlass should both be registered + assert!(graph.has_direct_reduction_by_name("MaxCut", "SpinGlass")); + assert!(graph.has_direct_reduction_by_name("SpinGlass", "MaxCut")); +} + +#[test] +fn test_problem_size_propagation() { + let graph = ReductionGraph::new(); + let input_size = ProblemSize::new(vec![("num_vertices", 50), ("num_edges", 100)]); + + // Path finding should work with size propagation using compatible graph types + // IndependentSet -> VertexCovering uses SimpleGraph -> SimpleGraph + let path = graph.find_cheapest_path( + ("IndependentSet", "SimpleGraph"), + ("VertexCovering", "SimpleGraph"), + &input_size, + &MinimizeSteps, + ); + + assert!(path.is_some()); + + // Also test that find_shortest_path_by_name works for multi-step paths + let path2 = graph.find_shortest_path_by_name("IndependentSet", "SetPacking"); + assert!(path2.is_some()); +} + +#[test] +fn test_json_export() { + let graph = ReductionGraph::new(); + let json = graph.to_json(); + + // Should have nodes for registered problems + assert!(!json.nodes.is_empty()); + assert!(!json.edges.is_empty()); + + // Categories should be assigned + let categories: std::collections::HashSet<&str> = + json.nodes.iter().map(|n| n.category.as_str()).collect(); + assert!(categories.len() >= 3, "Should have multiple categories"); +}