use std::collections::HashMap;
use std::io::{BufRead, BufReader};
#[derive(Default, Clone)]
struct Fp {
mean: f32,
sumsq: f32,
l1: f32,
nnz: f32,
hist: Vec<f32>,
elemsig: Vec<f32>,
}
impl Fp {
fn vec(&self) -> Vec<f32> {
if !self.elemsig.is_empty() {
return self.elemsig.clone();
}
let mut v = vec![self.mean, self.sumsq, self.l1, self.nnz];
v.extend_from_slice(&self.hist);
v
}
}
fn dist(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return f32::INFINITY;
}
let num: f32 = a
.iter()
.zip(b)
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt();
let den: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt() + 1e-9;
num / den
}
#[derive(Clone, Debug)]
pub struct Opportunity {
pub site: String,
pub gflops: f64,
pub cost_share: f32,
pub recur: f32,
pub drift: f32,
pub recommendation: String,
pub savings: f32,
}
const MATCH_THRESH: f32 = 1e-3;
pub fn mine_opportunities(csv: &str, sidecar: &str) -> std::io::Result<Vec<Opportunity>> {
let mut fps: HashMap<(String, u64), Fp> = HashMap::new();
let f = std::fs::File::open(csv)?;
for (i, line) in BufReader::new(f).lines().enumerate() {
let line = line?;
if i == 0 || line.is_empty() {
continue;
}
let c: Vec<&str> = line.split(',').collect();
if c.len() != 13 || c[9] != "lhs" {
continue; }
let step: u64 = c[1].parse().unwrap_or(0);
let site = c[8].to_string();
let idx: usize = c[11].parse().unwrap_or(0);
let val: f32 = c[12].parse().unwrap_or(0.0);
let e = fps.entry((site, step)).or_default();
match c[10] {
"mean" => e.mean = val,
"sumsq" => e.sumsq = val,
"l1" => e.l1 = val,
"nnz" => e.nnz = val,
"hist" => {
if e.hist.len() <= idx {
e.hist.resize(idx + 1, 0.0);
}
e.hist[idx] = val;
}
"elemsig" => {
if e.elemsig.len() <= idx {
e.elemsig.resize(idx + 1, 0.0);
}
e.elemsig[idx] = val;
}
_ => {}
}
}
let mut flops: HashMap<String, u64> = HashMap::new();
if let Ok(sf) = std::fs::File::open(sidecar) {
for (i, line) in BufReader::new(sf).lines().enumerate() {
let line = line?;
if i == 0 || line.is_empty() {
continue;
}
let c: Vec<&str> = line.split(',').collect();
if c.len() == 2 {
flops.insert(c[0].to_string(), c[1].parse().unwrap_or(0));
}
}
}
let total_flops: u64 = flops.values().sum::<u64>().max(1);
let mut by_site: HashMap<String, Vec<(u64, Vec<f32>)>> = HashMap::new();
for ((site, step), fp) in &fps {
by_site
.entry(site.clone())
.or_default()
.push((*step, fp.vec()));
}
let mut out = Vec::new();
for (site, mut series) in by_site {
series.sort_by_key(|&(s, _)| s);
if series.len() < 2 {
continue;
}
let mut repeats = 0usize;
for i in 1..series.len() {
let hit = (0..i).any(|j| dist(&series[i].1, &series[j].1) < MATCH_THRESH);
if hit {
repeats += 1;
}
}
let recur = repeats as f32 / (series.len() - 1) as f32;
let drift = (1..series.len())
.map(|i| dist(&series[i].1, &series[i - 1].1))
.sum::<f32>()
/ (series.len() - 1) as f32;
let fl = *flops.get(&site).unwrap_or(&0);
let cost_share = fl as f32 / total_flops as f32;
let delta_headroom = (1.0 - drift).clamp(0.0, 1.0);
let (rec, best) = if recur > 0.2 {
(
format!("memoize — cache the {:.0}% repeated calls", recur * 100.0),
recur,
)
} else if drift < 0.3 {
(
format!("delta-compute — linear op, tiny drift {drift:.2}"),
delta_headroom,
)
} else {
("(input varies — no temporal reuse)".into(), 0.0)
};
out.push(Opportunity {
site,
gflops: fl as f64 / 1e9,
cost_share,
recur,
drift,
recommendation: rec,
savings: cost_share * best,
});
}
out.sort_by(|a, b| b.savings.partial_cmp(&a.savings).unwrap());
Ok(out)
}
pub fn report(opps: &[Opportunity]) {
println!(
"{:<14} {:>8} {:>7} {:>6} {:>6} {:<8} recommendation",
"site", "GFLOP", "%cost", "recur", "drift", "save"
);
println!("{}", "-".repeat(100));
for o in opps.iter().take(14) {
println!(
"{:<14} {:>8.3} {:>6.1}% {:>6.0}% {:>6.2} {:>6.1}% {}",
o.site.chars().take(14).collect::<String>(),
o.gflops,
o.cost_share * 100.0,
o.recur * 100.0,
o.drift,
o.savings * 100.0,
o.recommendation
);
}
let total: f32 = opps.iter().map(|o| o.savings).sum();
println!(
"\nAggregate recoverable compute (Σ cost-share × reuse): {:.1}%",
total * 100.0
);
}