|
| 1 | +# Redundant Rule Detection Utility |
| 2 | + |
| 3 | +**Date:** 2026-03-09 |
| 4 | +**Issue:** #193 |
| 5 | + |
| 6 | +## Goal |
| 7 | + |
| 8 | +Detect primitive reduction rules whose overhead is dominated (equal or worse) by a composite path through other rules. Prevents adding redundant rules and documents existing redundancies. |
| 9 | + |
| 10 | +## Location |
| 11 | + |
| 12 | +`src/rules/analysis.rs` — new module alongside `graph.rs`, `registry.rs`, `cost.rs`. |
| 13 | + |
| 14 | +## Types |
| 15 | + |
| 16 | +```rust |
| 17 | +/// Asymptotic growth rate classification. |
| 18 | +/// Ordered: Constant < Logarithmic < Polynomial(k) < Exponential(c) < SuperExponential. |
| 19 | +#[derive(Debug, Clone, PartialEq, PartialOrd)] |
| 20 | +enum GrowthRate { |
| 21 | + Constant, |
| 22 | + Logarithmic, |
| 23 | + Polynomial(f64), // degree k |
| 24 | + Exponential(f64), // base c |
| 25 | + SuperExponential, |
| 26 | +} |
| 27 | + |
| 28 | +/// A primitive rule dominated by a composite path. |
| 29 | +struct DominatedRule { |
| 30 | + source: String, |
| 31 | + target: String, |
| 32 | + primitive_overhead: ReductionOverhead, |
| 33 | + dominating_path: ReductionPath, |
| 34 | + composed_overhead: ReductionOverhead, |
| 35 | + comparison: Vec<(String, Ordering)>, // per-field ordering |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +## Key Functions |
| 40 | + |
| 41 | +### `Expr::growth_rate(var: &str) -> Result<GrowthRate, String>` |
| 42 | + |
| 43 | +Recursively classify an expression's growth w.r.t. a single variable. |
| 44 | + |
| 45 | +Rules (recursive — classify children first, then combine): |
| 46 | + |
| 47 | +| Expression | Child classifications | Result | |
| 48 | +|---|---|---| |
| 49 | +| `Const(_)` | — | `Constant` | |
| 50 | +| `Var(v)` where `v == var` | — | `Polynomial(1.0)` | |
| 51 | +| `Var(v)` where `v != var` | — | `Constant` | |
| 52 | +| `Add(a, b)` | any | `max(a, b)` | |
| 53 | +| `Mul(a, b)` | both `Poly(k1, k2)` | `Polynomial(k1 + k2)` | |
| 54 | +| `Mul(a, b)` | one `Exp`, one `Poly` | `Exponential(same base)` | |
| 55 | +| `Mul(a, b)` | otherwise | `Error` | |
| 56 | +| `Pow(base, exp)` | `Poly(k)`, `Const(c)` | `Polynomial(k * c)` | |
| 57 | +| `Pow(base, exp)` | `Const(c)`, `Poly(k)` | `Exponential(c)` | |
| 58 | +| `Pow(base, exp)` | `Const(c)`, `Logarithmic` | `Polynomial(c.ln())` | |
| 59 | +| `Pow(base, exp)` | `Poly`, `Poly` | `SuperExponential` | |
| 60 | +| `Pow(base, exp)` | otherwise | `Error` | |
| 61 | +| `Exp(Log(inner))` | — | `growth_rate(inner)` (cancellation) | |
| 62 | +| `Exp(inner)` | `Constant` | `Constant` | |
| 63 | +| `Exp(inner)` | `Poly(k)`, k >= 1 | `Exponential(e)` | |
| 64 | +| `Exp(inner)` | `Exponential` | `SuperExponential` | |
| 65 | +| `Exp(inner)` | otherwise | `Error` | |
| 66 | +| `Log(inner)` | `Constant` | `Constant` | |
| 67 | +| `Log(inner)` | `Polynomial(_)` | `Logarithmic` | |
| 68 | +| `Log(inner)` | `Exponential(c)` | `Polynomial(1.0)` | |
| 69 | +| `Log(inner)` | otherwise | `Error` | |
| 70 | +| `Sqrt(inner)` | `Polynomial(k)` | `Polynomial(k / 2.0)` | |
| 71 | +| `Sqrt(inner)` | otherwise | `Error` | |
| 72 | + |
| 73 | +Returns `Err` for unclassifiable expressions rather than guessing. |
| 74 | + |
| 75 | +### `compare_overhead(primitive: &ReductionOverhead, composite: &ReductionOverhead) -> Option<Ordering>` |
| 76 | + |
| 77 | +For each common field: |
| 78 | +1. Collect all variables referenced by both expressions |
| 79 | +2. For each variable, compute `growth_rate` of both expressions |
| 80 | +3. Take the max growth rate across all variables for each expression |
| 81 | +4. Compare: composite <= primitive on all fields → `Some(Equal | Less)`; otherwise `None` |
| 82 | + |
| 83 | +### `find_dominated_rules(graph: &ReductionGraph) -> Vec<DominatedRule>` |
| 84 | + |
| 85 | +1. Iterate all edges in the variant-level graph |
| 86 | +2. For each edge (u, v), call `find_all_paths(u, v)` |
| 87 | +3. Filter to composite paths (len > 1) |
| 88 | +4. Compose overhead via `ReductionOverhead::compose` |
| 89 | +5. Compare with `compare_overhead` |
| 90 | +6. Collect dominated results |
| 91 | + |
| 92 | +## Integration Test |
| 93 | + |
| 94 | +In `src/unit_tests/rules/analysis.rs`: |
| 95 | +- Call `find_dominated_rules` on the global `ReductionGraph` |
| 96 | +- Maintain an allow-list of known dominated rules (currently 9: 6 genuine + 3 cast-composed) |
| 97 | +- **Fail if a new dominated rule appears** that is not in the allow-list |
| 98 | +- **Fail if an allow-listed rule is no longer dominated** (stale allow-list) |
| 99 | + |
| 100 | +## Existing Infrastructure Used |
| 101 | + |
| 102 | +- `ReductionGraph::find_all_paths()` — path enumeration |
| 103 | +- `ReductionOverhead::compose()` — chains overheads via `Expr::substitute` |
| 104 | +- `Expr::is_polynomial()` — partial classification (extended by `growth_rate`) |
| 105 | +- `Expr::substitute()` — variable substitution for composition |
| 106 | + |
| 107 | +## Known Dominated Rules (Allow-List) |
| 108 | + |
| 109 | +| Primitive | Best Composite | Category | |
| 110 | +|---|---|---| |
| 111 | +| KColoring → QUBO | KColoring → ILP → QUBO | genuine | |
| 112 | +| MIS → ILP | MIS → MVC → ILP | genuine | |
| 113 | +| MIS → QUBO | MIS → MVC → QUBO | genuine | |
| 114 | +| MaxSetPacking → ILP | SetPacking → MIS → ILP | genuine | |
| 115 | +| MVC → ILP | MVC → MinSetCovering → ILP | genuine | |
| 116 | +| MVC → QUBO | MVC → MIS → QUBO | genuine | |
| 117 | +| KSAT/K2 → SAT | K2 → KN → SAT | cast-composed | |
| 118 | +| KSAT/K3 → SAT | K3 → KN → SAT | cast-composed | |
| 119 | +| MIS/One → MIS/Kings/i32 | One→i32 → Kings/i32 | cast-composed | |
0 commit comments