|
| 1 | +//! Audit registered variant complexities against the reduction graph. |
| 2 | +//! |
| 3 | +//! For every simple path A -> ... -> B of 1-3 witness hops, substitute the |
| 4 | +//! path's composed parameter transform into B's registered complexity. The |
| 5 | +//! resulting expression is a candidate bound on solving A. Substitution of |
| 6 | +//! upper-bound parameters requires the target complexity to be monotone in those |
| 7 | +//! parameters; a known growth form alone does not prove that property. |
| 8 | +//! The audit compares represented growth bounds and reports the shortest route |
| 9 | +//! for each candidate improvement. Widening can make a registered bound loose, |
| 10 | +//! and the reduction's own running time is not modeled. Every finding therefore |
| 11 | +//! requires manual verification of monotonicity, tightness, and reduction cost. |
| 12 | +//! |
| 13 | +//! cargo run --release --example complexity_consistency_audit |
| 14 | +use problemreductions::Growth; |
| 15 | +use problemreductions::expr::Expr; |
| 16 | +use problemreductions::rules::{ReductionGraph, ReductionPath, ReductionStep}; |
| 17 | +use std::collections::{BTreeMap, HashMap}; |
| 18 | + |
| 19 | +const MAX_HOPS: usize = 3; |
| 20 | + |
| 21 | +type Node = (String, BTreeMap<String, String>); |
| 22 | + |
| 23 | +struct Finding { |
| 24 | + path: ReductionPath, |
| 25 | + registered: String, |
| 26 | + derived: String, |
| 27 | + target_complexity: String, |
| 28 | +} |
| 29 | + |
| 30 | +#[derive(Default)] |
| 31 | +struct Totals { |
| 32 | + paths: usize, |
| 33 | + no_complexity: usize, |
| 34 | + uncomposable: usize, |
| 35 | + unknown_growth: usize, |
| 36 | + incomparable: usize, |
| 37 | + consistent: usize, |
| 38 | + violations: usize, |
| 39 | + unknown_reasons: BTreeMap<String, usize>, |
| 40 | +} |
| 41 | + |
| 42 | +fn extend(graph: &ReductionGraph, path: &mut Vec<Node>, found: &mut Vec<ReductionPath>) { |
| 43 | + if path.len() > MAX_HOPS { |
| 44 | + return; |
| 45 | + } |
| 46 | + let (name, variant) = path.last().expect("non-empty path").clone(); |
| 47 | + for edge in graph.outgoing_reductions(&name) { |
| 48 | + // Aggregate-only edges bound a value, not a witness search. |
| 49 | + if edge.source_variant != variant || edge.capabilities.turing || !edge.capabilities.witness |
| 50 | + { |
| 51 | + continue; |
| 52 | + } |
| 53 | + let next = (edge.target_name.to_string(), edge.target_variant); |
| 54 | + if path.contains(&next) { |
| 55 | + continue; |
| 56 | + } |
| 57 | + path.push(next); |
| 58 | + found.push(ReductionPath { |
| 59 | + steps: path |
| 60 | + .iter() |
| 61 | + .map(|(name, variant)| ReductionStep { |
| 62 | + name: name.clone(), |
| 63 | + variant: variant.clone(), |
| 64 | + }) |
| 65 | + .collect(), |
| 66 | + }); |
| 67 | + extend(graph, path, found); |
| 68 | + path.pop(); |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +/// The target complexity rewritten in source parameters, if every step composes. |
| 73 | +/// |
| 74 | +/// Substituting upper-bound fields is sound only for a complexity that is |
| 75 | +/// monotone in the substituted parameters. The caller skips unsupported target |
| 76 | +/// growth forms, but support is not a monotonicity proof. Findings must be |
| 77 | +/// checked against the target formula and each transform's accuracy. |
| 78 | +fn derived_complexity(graph: &ReductionGraph, path: &ReductionPath, target: &str) -> Option<Expr> { |
| 79 | + let composed = graph.compose_path_parameter_transform(path).ok()??; |
| 80 | + let replacements: HashMap<&str, &Expr> = composed.expressions().collect(); |
| 81 | + Expr::parse(target).substitute_complete(&replacements).ok() |
| 82 | +} |
| 83 | + |
| 84 | +fn label(step: &ReductionStep) -> String { |
| 85 | + let variant: Vec<&str> = step.variant.values().map(String::as_str).collect(); |
| 86 | + if variant.is_empty() { |
| 87 | + step.name.clone() |
| 88 | + } else { |
| 89 | + format!("{}/{}", step.name, variant.join("/")) |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +fn main() { |
| 94 | + let graph = ReductionGraph::new(); |
| 95 | + let mut paths = Vec::new(); |
| 96 | + for name in graph.problem_types() { |
| 97 | + for variant in graph.variants_for(name) { |
| 98 | + extend(&graph, &mut vec![(name.to_string(), variant)], &mut paths); |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + let mut totals = Totals::default(); |
| 103 | + let mut findings: BTreeMap<String, Vec<Finding>> = BTreeMap::new(); |
| 104 | + for path in paths { |
| 105 | + totals.paths += 1; |
| 106 | + let (first, last) = ( |
| 107 | + &path.steps[0], |
| 108 | + path.steps.last().expect("path has a target"), |
| 109 | + ); |
| 110 | + let (Some(registered), Some(target)) = ( |
| 111 | + graph.variant_complexity(&first.name, &first.variant), |
| 112 | + graph.variant_complexity(&last.name, &last.variant), |
| 113 | + ) else { |
| 114 | + totals.no_complexity += 1; |
| 115 | + continue; |
| 116 | + }; |
| 117 | + if Growth::from_expr(&Expr::parse(target)).failures().is_some() { |
| 118 | + totals.unknown_growth += 1; |
| 119 | + *totals |
| 120 | + .unknown_reasons |
| 121 | + .entry("target complexity unsupported".into()) |
| 122 | + .or_default() += 1; |
| 123 | + continue; |
| 124 | + } |
| 125 | + let Some(derived) = derived_complexity(&graph, &path, target) else { |
| 126 | + totals.uncomposable += 1; |
| 127 | + continue; |
| 128 | + }; |
| 129 | + let derived_growth = Growth::from_expr(&derived); |
| 130 | + let registered_growth = Growth::from_expr(&Expr::parse(registered)); |
| 131 | + let failure = derived_growth |
| 132 | + .failures() |
| 133 | + .map(|failures| ("derived", failures)) |
| 134 | + .or_else(|| { |
| 135 | + registered_growth |
| 136 | + .failures() |
| 137 | + .map(|failures| ("registered", failures)) |
| 138 | + }); |
| 139 | + if let Some((side, failures)) = failure { |
| 140 | + totals.unknown_growth += 1; |
| 141 | + for failure in failures { |
| 142 | + let kind = failure.to_string(); |
| 143 | + let kind = kind.split(':').next().unwrap_or_default(); |
| 144 | + *totals |
| 145 | + .unknown_reasons |
| 146 | + .entry(format!("{side}: {kind}")) |
| 147 | + .or_default() += 1; |
| 148 | + } |
| 149 | + continue; |
| 150 | + } |
| 151 | + let derived_dominates = derived_growth.dominates(®istered_growth); |
| 152 | + let registered_dominates = registered_growth.dominates(&derived_growth); |
| 153 | + match (registered_dominates, derived_dominates) { |
| 154 | + (true, false) => { |
| 155 | + totals.violations += 1; |
| 156 | + let derived_big_o = derived_growth.to_big_o(); |
| 157 | + findings.entry(label(first)).or_default().push(Finding { |
| 158 | + path, |
| 159 | + registered: registered.to_string(), |
| 160 | + derived: derived_big_o, |
| 161 | + target_complexity: target.to_string(), |
| 162 | + }); |
| 163 | + } |
| 164 | + (false, false) => totals.incomparable += 1, |
| 165 | + _ => totals.consistent += 1, |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + println!("Complexity consistency audit (paths of 1-{MAX_HOPS} witness hops)"); |
| 170 | + println!("paths examined: {}", totals.paths); |
| 171 | + println!(" consistent: {}", totals.consistent); |
| 172 | + println!(" candidate improvements: {}", totals.violations); |
| 173 | + println!(" incomparable Big-O: {}", totals.incomparable); |
| 174 | + println!(" unknown growth: {}", totals.unknown_growth); |
| 175 | + println!(" uncomposable: {}", totals.uncomposable); |
| 176 | + println!(" no complexity: {}", totals.no_complexity); |
| 177 | + for (reason, count) in &totals.unknown_reasons { |
| 178 | + println!(" unknown growth, {reason}: {count}"); |
| 179 | + } |
| 180 | + |
| 181 | + println!( |
| 182 | + "\n## Candidate improvements requiring manual review ({} variants)", |
| 183 | + findings.len() |
| 184 | + ); |
| 185 | + for (source, list) in &findings { |
| 186 | + let best = list |
| 187 | + .iter() |
| 188 | + .min_by_key(|f| f.path.steps.len()) |
| 189 | + .expect("non-empty finding list"); |
| 190 | + let route: Vec<String> = best.path.steps.iter().map(label).collect(); |
| 191 | + println!("- {source}"); |
| 192 | + println!(" registered: {}", best.registered); |
| 193 | + println!( |
| 194 | + " derived: {} ({} witness paths)", |
| 195 | + best.derived, |
| 196 | + list.len() |
| 197 | + ); |
| 198 | + println!(" via: {}", route.join(" -> ")); |
| 199 | + println!(" target: {}", best.target_complexity); |
| 200 | + } |
| 201 | +} |
0 commit comments