use anyhow::{Context, Result, anyhow, ensure};
use cortiq_core::CmfModel;
use cortiq_engine::dflash::{ROUTE_KEY, Route, RouteFit, sidecar_path};
use cortiq_engine::pipeline::{
DFLASH_LEAN_BAND, DFLASH_LEAN_STEP, DflashStart, dflash_round_values, dflash_round_values_at, tail_repeat,
};
use cortiq_engine::router;
use std::path::Path;
struct Round {
row: Vec<f32>,
value: ((f32, f32), (f32, f32)),
block: bool,
repeat: usize,
}
type Prices = ([f32; 7], f32, f32);
fn rounds(path: &Path, hidden: usize, window: usize, edge: f32, prices: Option<&Prices>) -> Result<Vec<Round>> {
let bytes = std::fs::read(path).with_context(|| path.display().to_string())?;
let rec = 24 * 4 + hidden * 4;
ensure!(!bytes.is_empty() && bytes.len() % rec == 0, "{}: not records of {rec} bytes", path.display());
let recs: Vec<(Vec<u32>, Vec<f32>)> = bytes
.chunks_exact(rec)
.map(|r| {
let head = r[..96].chunks_exact(4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
let row = r[96..].chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
(head, row)
})
.collect();
let ids_path = format!("{}.ids", path.display());
let ids: Vec<u32> = std::fs::read(&ids_path)
.with_context(|| format!("{ids_path} (rows are logged with it; `q27_dflash_rows.sh` collects both)"))?
.chunks_exact(4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect();
ensure!(ids.last() == Some(&recs[0].0[1]), "{ids_path}: does not end at the first round's first token");
let first = recs[0].0[0] as usize;
let mut full: Vec<u32> = ids[..ids.len() - 1].to_vec();
let base = full.len();
for (h, _) in &recs {
let (pos, kept) = (h[0] as usize, h[3] as usize);
ensure!(pos == first + full.len() - base, "{}: a round at {pos} after {} tokens from {first}", path.display(), full.len() - base);
full.push(h[1]);
full.extend_from_slice(&h[4..4 + kept]);
}
let (mut starts, mut rows, mut repeats) = (Vec::new(), Vec::new(), Vec::new());
for (h, row) in &recs {
let at = base + h[0] as usize - first;
if h[2] != 7 || h[11..18].contains(&u32::MAX) || full.len() < at + 8 {
continue;
}
starts.push(DflashStart { seq: &full[..=at], after: &full[at + 1..], block: &h[4..11], chain: &h[11..18] });
rows.push(row.clone());
repeats.push(tail_repeat(&full[..=at]));
}
let values = match prices {
Some((cost, block, step)) => dflash_round_values_at(&starts, cost, *block, *step),
None => dflash_round_values(&starts),
};
let n = values.len();
Ok((0..n)
.map(|i| {
let (lo, hi) = (i.saturating_sub(window), (i + window + 1).min(n));
let sum = |f: fn(&((f32, f32), (f32, f32))) -> (f32, f32)| {
let (t, c) = values[lo..hi].iter().map(f).fold((0f32, 0f32), |s, v| (s.0 + v.0, s.1 + v.1));
t / c
};
Round { row: rows[i].clone(), value: values[i], block: sum(|v| v.0) > sum(|v| v.1) * edge, repeat: repeats[i] }
})
.collect())
}
type Seq = (String, Vec<Round>);
fn rate<'a>(seqs: impl IntoIterator<Item = &'a Seq>, mut pick: impl FnMut(&Round, bool) -> bool) -> (f32, f32) {
let (mut tokens, mut cost, mut rounds, mut to_block) = (0f32, 0f32, 0f32, 0usize);
for (_, s) in seqs {
let mut last = false;
for r in s {
last = pick(r, last);
let v = if last { r.value.0 } else { r.value.1 };
tokens += v.0;
cost += v.1;
to_block += last as usize;
rounds += 1.0;
}
}
(tokens / cost.max(1e-6), to_block as f32 / rounds.max(1.0))
}
#[allow(clippy::too_many_arguments)]
pub fn run(
model: &str,
rows_dir: &str,
rank: usize,
window: usize,
edge: f32,
hold_every: usize,
prices: Option<&str>,
write: bool,
) -> Result<()> {
let prices: Option<Prices> = match prices {
Some(list) => {
let v: Vec<f32> = list.split(',').map(|x| x.trim().parse::<f32>()).collect::<Result<_, _>>().context("--prices")?;
ensure!(v.len() == 9, "--prices: nine numbers (seven rounds, the block, a chain draft), {} given", v.len());
ensure!(!write, "--prices measures another device: add --dry-run, the router is fitted at this one's prices");
Some((v[..7].try_into().unwrap(), v[7], v[8]))
}
None => None,
};
let side = sidecar_path(Path::new(model));
let hidden = {
let m = CmfModel::open(&side).with_context(|| side.display().to_string())?;
cortiq_engine::dflash::Geometry::of(&m).map_err(|e| anyhow!(e))?.hidden
};
let mut files: Vec<_> = std::fs::read_dir(rows_dir)
.with_context(|| rows_dir.to_string())?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "rows") && std::fs::metadata(p).is_ok_and(|m| m.len() > 0))
.filter(|p| !p.file_name().is_some_and(|n| n.to_string_lossy().starts_with('.')))
.collect();
files.sort();
ensure!(files.len() >= 4, "{rows_dir}: {} files of rows, at least four are needed", files.len());
let mut all = Vec::new();
for f in &files {
let r = rounds(f, hidden, window, edge, prices.as_ref())?;
let name = f.file_stem().unwrap().to_string_lossy().to_string();
let one = [(name.clone(), r)];
let (b, c) = (rate(&one, |_, _| true).0, rate(&one, |_, _| false).0);
let [(_, r)] = one;
println!("{name}: {} rounds, the block {b:.2} and the chain {c:.2} tokens in a plain token's time, {} of them the block's", r.len(), r.iter().filter(|r| r.block).count());
all.push((name, r));
}
let (mut fit, mut held): (Vec<Seq>, Vec<Seq>) = (Vec::new(), Vec::new());
for (i, r) in all.into_iter().enumerate() {
if hold_every > 0 && i % hold_every == hold_every - 1 { held.push(r) } else { fit.push(r) }
}
let class = |seqs: &[Seq], block: bool| -> Vec<Vec<f32>> { seqs.iter().flat_map(|s| &s.1).filter(|r| r.block == block).map(|r| r.row.clone()).collect() };
let build = |seqs: &[Seq]| -> Result<RouteFit> {
let (b, c) = (class(seqs, true), class(seqs, false));
ensure!(b.len() >= 8 && c.len() >= 8, "{} rounds are the block's and {} the chain's: too few of one to fit", b.len(), c.len());
Ok(RouteFit {
block: router::fit_descriptor(&b, 0, rank).ok_or_else(|| anyhow!("the block's rounds do not fit"))?,
chain: router::fit_descriptor(&c, 0, rank).ok_or_else(|| anyhow!("the chain's rounds do not fit"))?,
block_rows: b.len(),
chain_rows: c.len(),
})
};
let report = |name: &str, route: &Route, seqs: &[Seq]| {
let line = |name: &str, seqs: &[&Seq]| {
let n = seqs.iter().map(|s| s.1.len()).sum::<usize>().max(1);
let right = seqs.iter().flat_map(|s| &s.1).filter(|r| (route.margin(&r.row) > 0.0) == r.block).count();
let of = || seqs.iter().copied();
let chain = rate(of(), |_, _| false).0;
let block = rate(of(), |_, _| true).0;
let best = rate(of(), |r, _| r.value.0.0 / r.value.0.1 > r.value.1.0 / r.value.1.1).0;
let labels = rate(of(), |r, _| r.block).0;
let raw = rate(of(), |r, _| route.margin(&r.row) > 0.0);
let mut lean = 0f32;
let mut at = 0usize;
let starts: Vec<usize> = seqs.iter().scan(0usize, |a, s| { let v = *a; *a += s.1.len(); Some(v) }).collect();
let run = rate(of(), |r, last| {
if starts.contains(&at) {
lean = 0.0;
}
at += 1;
lean += DFLASH_LEAN_STEP * (route.margin(&r.row) - lean);
if last { lean > -DFLASH_LEAN_BAND } else { lean > DFLASH_LEAN_BAND }
});
let copy: Vec<String> = [3usize, 4, 6, 8, 12, 16]
.iter()
.map(|&m| {
let r = rate(of(), |r, _| r.repeat < m && route.margin(&r.row) > 0.0);
format!("{m}: {:.3} ({:.0} %)", r.0, 100.0 * r.1)
})
.collect();
println!(
"{name}: {n} rounds, the router agrees with {:.1} % of them; tokens in a plain token's time: the chain alone {chain:.3}, the block alone {block:.3}, the router a round at a time {:.3} ({:.0} % to the block), averaged {:.3} ({:.0} %), the fit's own split {labels:.3}, the better of the two every round {best:.3}; the router with the chain where the tail repeats N tokens or more: {}",
100.0 * right as f32 / n as f32,
raw.0,
100.0 * raw.1,
run.0,
100.0 * run.1,
copy.join(", ")
);
};
line(name, &seqs.iter().collect::<Vec<_>>());
let kind = |s: &Seq| s.0.rsplit('_').next().unwrap_or("").to_string();
let mut kinds: Vec<String> = seqs.iter().map(kind).collect();
kinds.sort();
kinds.dedup();
if kinds.len() > 1 {
for k in kinds {
line(&format!(" {k}"), &seqs.iter().filter(|s| kind(s) == k).collect::<Vec<_>>());
}
}
};
if !held.is_empty() {
let part = build(&fit)?;
report("sequences held out of the fit", &Route::of(&part, hidden).map_err(|e| anyhow!(e))?, &held);
}
fit.extend(held);
let whole = build(&fit)?;
report("all sequences, fitted on them", &Route::of(&whole, hidden).map_err(|e| anyhow!(e))?, &fit);
println!("rank {}, {} rounds of the block and {} of the chain", whole.block.rank, whole.block_rows, whole.chain_rows);
if write {
let value = serde_json::to_value(&whole)?;
CmfModel::update_header_append(&side, move |h| {
let p = h.provenance.get_or_insert_with(|| serde_json::json!({}));
p[ROUTE_KEY] = value;
})
.map_err(|e| anyhow!("{}: {e}", side.display()))?;
println!("written to {}", side.display());
}
Ok(())
}