use std::io;
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Instant;
pub use crate::classify::cascade::StageMode;
use crate::classify::cascade::{
AcceptBounds, Stage, StageOutcome, classify, fast_stages, run_stages,
};
use crate::classify::cert::{Classified, Verdict};
use crate::classify::reptile::{RepScreen, reptile_screen_one};
use crate::classify::store::{
CoveredRange, append_range, covered_union, deep_overlay_path, in_covered_union, read_ranges,
read_resume,
};
use crate::cyclotomic::IsRing;
use crate::dataset::LazyRatDafsa;
use crate::geom::rat::Rat;
pub const FREE_ZZ12: [u64; 17] = [
0, 0, 0, 1, 3, 4, 22, 69, 418, 2210, 14024, 89075, 597581, 4076855, 28499301, 202464580,
1460982297,
];
fn ro_crate_dataset(dir: &str) -> serde_json::Value {
let path = format!("{dir}/ro-crate-metadata.json");
let txt = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let v: serde_json::Value =
serde_json::from_str(&txt).unwrap_or_else(|e| panic!("parse {path}: {e}"));
let graph = v["@graph"]
.as_array()
.unwrap_or_else(|| panic!("{path}: no @graph array"));
for node in graph {
let is_dataset = match &node["@type"] {
serde_json::Value::String(s) => s == "Dataset",
serde_json::Value::Array(a) => a.iter().any(|t| t.as_str() == Some("Dataset")),
_ => false,
};
if is_dataset {
return node.clone();
}
}
panic!("{path}: no @type==Dataset node in @graph");
}
pub fn asset_ring(dir: &str) -> usize {
let node = ro_crate_dataset(dir);
let props = node["additionalProperty"].as_array().unwrap_or_else(|| {
panic!("{dir}/ro-crate-metadata.json: Dataset has no additionalProperty")
});
for p in props {
if p["name"].as_str() == Some("effectiveRing") {
return p["value"]
.as_u64()
.unwrap_or_else(|| panic!("{dir}: effectiveRing value is not an integer"))
as usize;
}
}
panic!("{dir}/ro-crate-metadata.json: no effectiveRing additionalProperty");
}
pub fn asset_free_counts(dir: &str) -> Vec<u64> {
let node = ro_crate_dataset(dir);
let vars = node["variableMeasured"]
.as_array()
.unwrap_or_else(|| panic!("{dir}/ro-crate-metadata.json: Dataset has no variableMeasured"));
for v in vars {
if v["name"].as_str() == Some("free") {
let s = v["value"]
.as_str()
.unwrap_or_else(|| panic!("{dir}: free variableMeasured value is not a string"));
let mut counts = vec![0u64];
for tok in s.split(',') {
counts.push(
tok.trim().parse::<u64>().unwrap_or_else(|e| {
panic!("{dir}: free value has non-integer {tok:?}: {e}")
}),
);
}
return counts;
}
}
panic!("{dir}/ro-crate-metadata.json: no free variableMeasured");
}
pub fn perimeter_range(counts: &[u64], n: usize) -> (u64, u64) {
let start: u64 = counts[..n].iter().sum();
(start, start + counts[n])
}
pub fn perimeter_of(counts: &[u64], idx: u64) -> Option<usize> {
let mut cum = 0u64;
for (p, &c) in counts.iter().enumerate() {
if idx >= cum && idx < cum + c {
return Some(p);
}
cum += c;
}
None
}
pub fn open_ratdb(dir: &str) -> LazyRatDafsa<impl Fn(u32) -> io::Result<Vec<u8>>> {
let manifest = std::fs::read_to_string(format!("{dir}/block_index.json"))
.unwrap_or_else(|e| panic!("read {dir}/block_index.json: {e}"));
let v: serde_json::Value = serde_json::from_str(&manifest).unwrap();
let shas: Vec<String> = v["blocks"]
.as_array()
.unwrap()
.iter()
.map(|b| b["sha256"].as_str().unwrap().to_string())
.collect();
let base = dir.to_string();
LazyRatDafsa::open(&manifest, move |idx| {
std::fs::read(format!("{base}/blocks/{}.bin", shas[idx as usize]))
})
.unwrap()
}
fn verdict_check<T: IsRing>(base: &Rat<T>, c: &Classified) -> (usize, bool) {
match c {
Classified::Decided(Verdict::Periodic(pc)) => {
debug_assert!(pc.verify(base), "minted cert must be pre-verified");
(0, true)
}
Classified::Decided(Verdict::CannotTile(hc)) => (1, hc.verify_lower_bound(base)),
Classified::Undecided { .. } => (2, true),
}
}
fn workers_or_all(workers: usize) -> usize {
if workers > 0 {
workers
} else {
crate::util::available_workers()
}
}
#[derive(Debug, Clone)]
pub struct FastConfig {
pub mode: StageMode,
pub deep_bound: usize,
pub deep_budget: usize,
pub deep_torus_kmax: usize,
pub bounds: AcceptBounds,
pub workers: usize,
pub chunk: usize,
}
impl Default for FastConfig {
fn default() -> Self {
Self {
mode: StageMode::Full,
deep_bound: 6,
deep_budget: 500_000,
deep_torus_kmax: 32,
bounds: AcceptBounds {
aniso_budget: 5_000,
..AcceptBounds::default()
},
workers: 0,
chunk: 0,
}
}
}
fn parallel_sweep(name: &str, total: usize, workers: usize, work: impl Fn(usize) + Sync) {
use crate::util::parallel::parallel_drain;
const PROGRESS_MIN: usize = 2_000;
if total < PROGRESS_MIN {
parallel_drain(total, workers, || (), |_, j| work(j), |_, _| ());
return;
}
let st = Instant::now();
let done_ct = AtomicUsize::new(0);
let stop = AtomicBool::new(false);
let (done_ct, stop) = (&done_ct, &stop);
std::thread::scope(|sc| {
sc.spawn(move || {
let mut printed_at = 0u64;
while !stop.load(Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_secs(1));
let secs = st.elapsed().as_secs();
if secs >= printed_at + 15 {
printed_at = secs;
let dn = done_ct.load(Ordering::Relaxed);
let rate = dn as f64 / st.elapsed().as_secs_f64().max(1e-3);
eprintln!(" {name}: {dn}/{total} ({rate:.0}/s)");
}
}
});
struct StopOnDrop<'a>(&'a AtomicBool);
impl Drop for StopOnDrop<'_> {
fn drop(&mut self) {
self.0.store(true, Ordering::Relaxed);
}
}
let _stop_guard = StopOnDrop(stop);
parallel_drain(
total,
workers,
|| (),
|_, j| {
work(j);
done_ct.fetch_add(1, Ordering::Relaxed);
},
|_, _| (),
);
});
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ClassifyStats {
pub periodic: usize,
pub cannot_tile: usize,
pub undecided: usize,
pub verify_fail: usize,
pub total: usize,
}
pub fn run_fast<T: IsRing, F>(
d: &LazyRatDafsa<F>,
counts: &[u64],
perim: usize,
out: &Path,
cfg: &FastConfig,
) -> ClassifyStats
where
F: Fn(u32) -> io::Result<Vec<u8>>,
{
let (start, end) = perimeter_range(counts, perim);
run_fast_range::<T, F>(d, start, end, out, cfg)
}
pub fn run_fast_range<T: IsRing, F>(
d: &LazyRatDafsa<F>,
start: u64,
end: u64,
out: &Path,
cfg: &FastConfig,
) -> ClassifyStats
where
F: Fn(u32) -> io::Result<Vec<u8>>,
{
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
assert!(start <= end, "empty or inverted range");
let t0 = Instant::now();
let n = (end - start) as usize;
let workers = workers_or_all(cfg.workers);
let b = cfg.bounds;
let (deep_bound, deep_budget, deep_torus_kmax) =
(cfg.deep_bound, cfg.deep_budget, cfg.deep_torus_kmax);
let ranges = read_ranges(out);
let union = covered_union(&ranges);
let mut rr = [0usize; 3];
for r in &ranges {
if r.start >= start && r.end <= end {
for (acc, t) in rr.iter_mut().zip(r.tally) {
*acc += t;
}
} else if r.start < end && r.end > start {
eprintln!(
"fast: sidecar range [{},{}) straddles the launch range -- its tallies are excluded from the reported stats",
r.start, r.end
);
}
}
let (done, [lp, ln, lu]) = read_resume(out, start, end, &union);
let (rp, rn, ru) = (rr[0] + lp, rr[1] + ln, rr[2] + lu);
let covered_ct: u64 = union
.iter()
.map(|&(s, e)| e.min(end).saturating_sub(s.max(start)))
.sum();
const FLUSH_LINES: usize = 256;
const FLUSH_SECS: u64 = 1;
let writer = Mutex::new((
std::io::BufWriter::new(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(out)
.unwrap(),
),
0usize,
Instant::now(),
));
let chunk = if cfg.chunk == 0 { n.max(1) } else { cfg.chunk };
eprintln!(
"fast [{start},{end}) = {n} rats (windows of {chunk}), {workers} workers -> {} (resuming {} line-done + {} covered ranges, setup {:?})",
out.display(),
done.len(),
ranges.len(),
t0.elapsed(),
);
let cnt = [
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
];
let emit = |seq: &[i8], abs_idx: u64, c: &Classified| {
let base = Rat::<T>::from_slice_trusted(seq);
let (slot, ok) = verdict_check(&base, c);
cnt[slot].fetch_add(1, Ordering::Relaxed);
if !ok {
cnt[3].fetch_add(1, Ordering::Relaxed);
eprintln!("VERIFY FAIL idx {abs_idx} seq {seq:?}");
}
let line = format!("{abs_idx}\t{}\n", serde_json::to_string(c).unwrap());
let mut w = writer.lock().unwrap();
w.0.write_all(line.as_bytes()).unwrap();
w.1 += 1;
if w.1 >= FLUSH_LINES || w.2.elapsed().as_secs() >= FLUSH_SECS {
w.0.flush().unwrap();
w.1 = 0;
w.2 = Instant::now();
}
};
struct Tally {
settled: AtomicUsize,
busy_ns: AtomicU64,
}
let mut tallies: Vec<(&'static str, Arc<Tally>)> = Vec::new();
let stages: Vec<Stage> = fast_stages::<T>(b, deep_torus_kmax, deep_bound, deep_budget)
.into_iter()
.filter(|st| st.kind.selected_by(cfg.mode))
.map(|st| {
let t = Arc::new(Tally {
settled: AtomicUsize::new(0),
busy_ns: AtomicU64::new(0),
});
tallies.push((st.name, t.clone()));
let inner = st.check;
let check = move |s: &[i8]| {
let t0 = Instant::now();
let out = inner(s);
t.busy_ns
.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
if matches!(out, StageOutcome::Settled(_)) {
t.settled.fetch_add(1, Ordering::Relaxed);
}
out
};
Stage {
name: st.name,
kind: st.kind,
check: Box::new(check),
}
})
.collect();
let mut wstart = start;
while wstart < end {
let wend = (wstart + chunk as u64).min(end);
if chunk < n {
eprintln!(" window [{wstart},{wend})");
}
let open: Vec<u64> = (wstart..wend)
.filter(|&i| !in_covered_union(&union, i))
.collect();
if open.is_empty() {
wstart = wend;
continue;
}
let rats: Vec<Vec<i8>> = open
.iter()
.map(|&i| d.get(i).expect("index in range"))
.collect();
let todo: Vec<usize> = (0..open.len())
.filter(|&j| !done.contains(&open[j]))
.collect();
let line_resumed = open.len() - todo.len();
let before = [
cnt[0].load(Ordering::Relaxed),
cnt[1].load(Ordering::Relaxed),
cnt[2].load(Ordering::Relaxed),
];
let st = Instant::now();
parallel_sweep("classify", todo.len(), workers, |j| {
let t = todo[j];
emit(
&rats[t],
open[t],
&run_stages::<T>(&rats[t], &stages, cfg.mode),
);
});
for (name, t) in &tallies {
let settled = t.settled.swap(0, Ordering::Relaxed);
let busy = t.busy_ns.swap(0, Ordering::Relaxed);
eprintln!(
" stage {name}: settled {settled}, busy {:.1}s",
busy as f64 / 1e9
);
}
eprintln!(
" window: {} tiles classified ({:?})",
todo.len(),
st.elapsed()
);
if line_resumed == 0 && !todo.is_empty() {
writer.lock().unwrap().0.flush().unwrap(); let after = [
cnt[0].load(Ordering::Relaxed),
cnt[1].load(Ordering::Relaxed),
cnt[2].load(Ordering::Relaxed),
];
append_range(
out,
CoveredRange {
start: wstart,
end: wend,
tally: [
after[0] - before[0],
after[1] - before[1],
after[2] - before[2],
],
},
);
}
wstart = wend;
}
writer.lock().unwrap().0.flush().unwrap();
let load = |i: usize| cnt[i].load(Ordering::Relaxed);
let (p, nn, u, vf) = (load(0), load(1), load(2), load(3));
let stats = ClassifyStats {
periodic: rp + p,
cannot_tile: rn + nn,
undecided: ru + u,
verify_fail: vf,
total: covered_ct as usize + done.len() + p + nn + u,
};
eprintln!(
"fast [{start},{end}) done in {:?}: Periodic={} CannotTile={} Undecided={} (resumed {}, new {}); verify_fail={vf}; total={}",
t0.elapsed(),
stats.periodic,
stats.cannot_tile,
stats.undecided,
covered_ct as usize + done.len(),
p + nn + u,
stats.total,
);
assert_eq!(stats.total, n, "every rat accounted for (store + this run)");
assert_eq!(vf, 0, "every minted cert must self-verify");
stats
}
#[derive(Debug, Clone)]
pub struct DeepConfig {
pub mode: StageMode,
pub res_bound: usize,
pub res_budget: usize,
pub bounds: AcceptBounds,
pub workers: usize,
}
impl Default for DeepConfig {
fn default() -> Self {
let bounds = AcceptBounds {
torus_kmax: 40,
torus_coronas: 5,
aniso_kmax: 6,
aniso_budget: 30_000,
aniso_restarts: 12,
..AcceptBounds::default()
};
Self {
mode: StageMode::Full,
res_bound: 6,
res_budget: 100_000_000,
bounds,
workers: 0,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DeepStats {
pub to_periodic: usize,
pub to_cannot_tile: usize,
pub still_undecided: usize,
}
pub fn run_deep<T: IsRing, F>(d: &LazyRatDafsa<F>, store: &Path, cfg: &DeepConfig) -> DeepStats
where
F: Fn(u32) -> io::Result<Vec<u8>>,
{
use std::io::Write;
let workers = workers_or_all(cfg.workers);
let (bounds, res_bound, res_budget, mode) =
(cfg.bounds, cfg.res_bound, cfg.res_budget, cfg.mode);
let overlay = deep_overlay_path(store);
let (done, [rp, rn, ru]) = read_resume(&overlay, 0, u64::MAX, &[]);
let txt = std::fs::read_to_string(store).expect("store readable");
let mut work: Vec<(u64, Vec<i8>, Vec<crate::geom::matches::PatchMatch>)> = Vec::new();
for line in txt.lines() {
let Some((idx_s, json)) = line.split_once('\t') else {
continue;
};
let Ok(idx) = idx_s.parse::<u64>() else {
continue;
};
if done.contains(&idx) {
continue;
}
let Ok(c) = serde_json::from_str::<Classified>(json) else {
continue;
};
if let Classified::Undecided { corona, .. } = c {
work.push((idx, d.get(idx).expect("idx in range"), corona));
}
}
let total = work.len();
eprintln!(
"deep: {total} Undecided tiles to re-classify ({} already in {}), {workers} workers",
done.len(),
overlay.display()
);
let writer = Mutex::new(std::io::BufWriter::new(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&overlay)
.unwrap(),
));
let cnt = [
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
];
let emit = |idx: u64, seq: &[i8], nc: &Classified, secs: f64, tag: &str| {
let base = Rat::<T>::from_slice_trusted(seq);
let (slot, ok) = verdict_check(&base, nc);
cnt[slot].fetch_add(1, Ordering::Relaxed);
if !ok {
cnt[3].fetch_add(1, Ordering::Relaxed);
eprintln!("CERT-FAIL idx {idx}");
}
let new = ["Periodic", "CannotTile", "Undecided"][slot];
eprintln!("idx {idx} Undecided->{new} ({secs:.2}s{tag}) seq {seq:?}");
let mut wtr = writer.lock().unwrap();
writeln!(wtr, "{idx}\t{}", serde_json::to_string(nc).unwrap()).unwrap();
wtr.flush().unwrap();
};
let survivors: Vec<usize> = if mode.runs_accepts() {
let kept: Mutex<Vec<usize>> = Mutex::new(Vec::new());
parallel_sweep("deep-cheap", total, workers, |w| {
let (idx, seq, witness) = &work[w];
let t = Instant::now();
let base = Rat::<T>::from_slice_trusted(seq);
let pc =
crate::classify::cascade::certify_periodic_cheap::<T>(seq, &bounds).or_else(|| {
crate::classify::mint::witness_torus_cert(&base, witness, bounds.torus_kmax)
});
match pc {
Some(pc) => {
let nc = Classified::Decided(Verdict::Periodic(pc));
emit(*idx, seq, &nc, t.elapsed().as_secs_f64(), ", cheap");
}
None => kept.lock().unwrap().push(w),
}
});
let mut kept = kept.into_inner().unwrap();
kept.sort_unstable();
eprintln!(
"deep: cheap accepts settled {}, {} remain for the full funnel",
total - kept.len(),
kept.len()
);
kept
} else {
(0..total).collect()
};
parallel_sweep("deep", survivors.len(), workers, |s| {
let (idx, seq, _) = &work[survivors[s]];
let t = Instant::now();
let nc = classify::<T>(seq, &bounds, res_bound, res_budget, mode);
emit(*idx, seq, &nc, t.elapsed().as_secs_f64(), "");
});
assert_eq!(
cnt[3].load(Ordering::Relaxed),
0,
"every newly-minted cert must verify"
);
let stats = DeepStats {
to_periodic: rp + cnt[0].load(Ordering::Relaxed),
to_cannot_tile: rn + cnt[1].load(Ordering::Relaxed),
still_undecided: ru + cnt[2].load(Ordering::Relaxed),
};
eprintln!(
"deep: Undecided -> Periodic {} / CannotTile {} / still-Undecided {} (incl. resumed); apply with --merge {}",
stats.to_periodic,
stats.to_cannot_tile,
stats.still_undecided,
overlay.display()
);
stats
}
#[derive(Debug, Clone, Default)]
pub struct VerifyReport {
pub perimeters: usize,
pub bad_lines: usize,
pub reverify_fail: usize,
pub incomplete: usize,
pub dups: usize,
}
impl VerifyReport {
pub fn is_clean(&self) -> bool {
self.perimeters > 0
&& self.bad_lines == 0
&& self.reverify_fail == 0
&& self.incomplete == 0
&& self.dups == 0
}
}
pub fn run_reptile_screen<T: IsRing, F: Fn(u32) -> io::Result<Vec<u8>>>(
d: &LazyRatDafsa<F>,
store: &Path,
out: &Path,
kmax: usize,
budget: usize,
) {
use std::collections::BTreeMap;
use std::io::{BufRead, Write};
let rdr = std::io::BufReader::new(std::fs::File::open(store).expect("open cert store"));
let mut periodics: Vec<u64> = Vec::new();
for line in rdr.lines() {
let line = line.expect("read cert store line");
let Some((idx_s, json)) = line.split_once('\t') else {
continue;
};
if !json.contains("\"Periodic\"") {
continue;
}
if let Ok(idx) = idx_s.parse::<u64>() {
periodics.push(idx);
}
}
let cands: Vec<(u64, Vec<i8>)> = periodics
.iter()
.filter_map(|&idx| d.get(idx).map(|s| (idx, s)))
.collect();
let n = cands.len();
eprintln!(
"reptile-screen: {} periodic tiles ({n} resolved), kmax={kmax}, budget={budget}",
periodics.len()
);
type Acc = (Vec<(u64, String)>, BTreeMap<usize, usize>, Vec<u64>);
let workers = crate::util::available_workers();
let done = AtomicUsize::new(0);
let (mut hit_lines, counts, mut inconclusive): Acc = crate::util::parallel::parallel_drain(
cands.len(),
workers,
|| (Vec::new(), BTreeMap::new(), Vec::new()),
|acc, i| {
let (idx, seq) = &cands[i];
let base = Rat::<T>::from_slice_trusted(seq);
match reptile_screen_one(&base, kmax, budget) {
RepScreen::RepTile(cert) => {
let json = serde_json::to_string(&cert).unwrap();
acc.0.push((*idx, json));
*acc.1.entry(cert.k * cert.k).or_insert(0) += 1;
}
RepScreen::Inconclusive => acc.2.push(*idx),
RepScreen::Anomaly(k) => {
eprintln!(
" ANOMALY idx {idx}: scale-{k} tiling found but cert re-verify failed (build-reconstruction bug)"
);
acc.2.push(*idx);
}
RepScreen::No => {}
}
let c = done.fetch_add(1, Ordering::Relaxed) + 1;
if c.is_multiple_of(50_000) {
eprintln!(" {c}/{} screened", cands.len());
}
},
|mut a, b| {
a.0.extend(b.0);
for (order, c) in b.1 {
*a.1.entry(order).or_insert(0) += c;
}
a.2.extend(b.2);
a
},
);
hit_lines.sort_unstable_by_key(|(idx, _)| *idx);
let mut w = std::io::BufWriter::new(std::fs::File::create(out).expect("create reptile out"));
for (idx, json) in &hit_lines {
writeln!(w, "{idx}\t{json}").unwrap();
}
w.flush().unwrap();
let hits: usize = counts.values().sum();
eprintln!(
"reptile-screen: {hits} rep-tiles among {n} periodics ({} inconclusive/budget-capped)",
inconclusive.len()
);
for (order, c) in &counts {
eprintln!(" order {order}: {c}");
}
if !inconclusive.is_empty() {
inconclusive.sort_unstable();
let side = out.with_extension("inconclusive");
let text: String = inconclusive.iter().map(|i| format!("{i}\n")).collect();
if std::fs::write(&side, text).is_ok() {
eprintln!(
" wrote {} budget-capped indices to {} (re-run deeper if wanted)",
inconclusive.len(),
side.display()
);
}
}
}
pub fn run_verify<T: IsRing, F>(
d: &LazyRatDafsa<F>,
counts: &[u64],
store: &Path,
only_perim: Option<usize>,
) -> VerifyReport
where
F: Fn(u32) -> io::Result<Vec<u8>>,
{
use std::collections::{BTreeMap, HashSet};
#[derive(Default)]
struct Stats {
lines: usize,
seen: HashSet<u64>,
p: usize,
nn: usize,
u: usize,
vf: usize,
rp: usize,
rn: usize,
ru: usize,
covered_ct: u64,
}
let t0 = Instant::now();
let ranges = read_ranges(store);
let union = covered_union(&ranges);
let txt = std::fs::read_to_string(store).expect("store readable");
let mut per: BTreeMap<usize, Stats> = BTreeMap::new();
let mut bad = 0usize;
for r in &ranges {
let Some(pm) = perimeter_of(counts, r.start) else {
bad += 1;
continue;
};
if only_perim.is_some_and(|op| op != pm) {
continue;
}
let e = per.entry(pm).or_default();
e.rp += r.tally[0];
e.rn += r.tally[1];
e.ru += r.tally[2];
}
for &(s0, e0) in &union {
let mut pos = s0;
while pos < e0 {
let Some(pm) = perimeter_of(counts, pos) else {
break;
};
let (_, block_end) = perimeter_range(counts, pm);
let seg_end = e0.min(block_end);
if only_perim.is_none_or(|op| op == pm) {
per.entry(pm).or_default().covered_ct += seg_end - pos;
}
pos = seg_end;
}
}
for line in txt.lines() {
let Some((idx_s, json)) = line.split_once('\t') else {
bad += 1;
continue;
};
let Ok(idx) = idx_s.parse::<u64>() else {
bad += 1;
continue;
};
let Some(pm) = perimeter_of(counts, idx) else {
bad += 1;
continue;
};
if only_perim.is_some_and(|op| op != pm) {
continue;
}
let Ok(c) = serde_json::from_str::<Classified>(json) else {
bad += 1;
continue;
};
let in_union = in_covered_union(&union, idx);
let e = per.entry(pm).or_default();
e.lines += 1;
e.seen.insert(idx);
let base = Rat::<T>::from_slice_trusted(&d.get(idx).expect("index in range"));
let ok = match &c {
Classified::Decided(Verdict::Periodic(pc)) => {
if !in_union {
e.p += 1;
}
pc.verify(&base)
}
Classified::Decided(Verdict::CannotTile(hc)) => {
if !in_union {
e.nn += 1;
}
hc.verify_lower_bound(&base)
}
Classified::Undecided { .. } => {
if !in_union {
e.u += 1;
}
true
}
};
if !ok {
e.vf += 1;
eprintln!("RE-VERIFY FAIL idx {idx} (perimeter {pm})");
}
}
let mut report = VerifyReport {
perimeters: per.len(),
bad_lines: bad,
..Default::default()
};
for (&pm, e) in &per {
let expect = counts[pm] as usize;
let outside = e
.seen
.iter()
.filter(|&&i| !in_covered_union(&union, i))
.count();
let accounted = e.covered_ct as usize + outside;
let complete = accounted == expect;
let nodup = e.lines == e.seen.len();
if !complete {
report.incomplete += 1;
}
if !nodup {
report.dups += 1;
}
report.reverify_fail += e.vf;
eprintln!(
" perimeter {pm}: {accounted}/{expect} {}{}{} | P={} N={} U={} | reverify_fail={}",
if complete { "COMPLETE" } else { "INCOMPLETE" },
if nodup { "" } else { " +DUPLICATES" },
if e.covered_ct > 0 {
format!(" ({} by coverage, {} kept lines)", e.covered_ct, e.lines)
} else {
String::new()
},
e.p + e.rp,
e.nn + e.rn,
e.u + e.ru,
e.vf,
);
}
eprintln!(
"verify: {} perimeter(s); bad_lines {}; reverify_fail {}; incomplete {}; with_dups {}; {:?}",
report.perimeters,
report.bad_lines,
report.reverify_fail,
report.incomplete,
report.dups,
t0.elapsed()
);
report
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::store::*;
#[test]
fn parallel_sweep_runs_every_item_with_heartbeat() {
use std::sync::atomic::{AtomicUsize, Ordering};
let total = 3_000usize;
let count = AtomicUsize::new(0);
let sum = AtomicUsize::new(0);
parallel_sweep("test", total, 4, |j| {
count.fetch_add(1, Ordering::Relaxed);
sum.fetch_add(j, Ordering::Relaxed);
});
assert_eq!(count.load(Ordering::Relaxed), total);
assert_eq!(sum.load(Ordering::Relaxed), total * (total - 1) / 2);
}
#[cfg(feature = "cli")]
#[test]
#[ignore = "positive control (~min): known aperiodic + Heesch-3 tiles must be kept"]
fn fast_catches_known_interesting_tiles() {
use crate::classify::cert::{Classified, HeeschStatus, Verdict};
use crate::classify::heesch::{Heesch, heesch_number};
use crate::cyclotomic::ZZ12;
use crate::geom::rat::Rat;
use crate::geom::tiles;
use crate::geom::tileset::TileSet;
let cfg = FastConfig::default();
let stages = fast_stages::<ZZ12>(
cfg.bounds,
cfg.deep_torus_kmax,
cfg.deep_bound,
cfg.deep_budget,
);
let classify_one = |seq: &[i8]| run_stages::<ZZ12>(seq, &stages, StageMode::Full);
let interesting = |c: &Classified| match c {
Classified::Undecided { .. } => true,
Classified::Decided(Verdict::CannotTile(hc)) => {
hc.heesch >= 2 || matches!(hc.status, HeeschStatus::Unknown)
}
Classified::Decided(Verdict::Periodic(_)) => false,
};
let spectre_seq = Rat::<ZZ12>::from_snake_trusted(&tiles::spectre::<ZZ12>())
.seq()
.to_vec();
let sc = classify_one(&spectre_seq);
eprintln!("SPECTRE -> {sc:?}");
assert!(
matches!(sc, Classified::Undecided { .. }),
"SPECTRE must classify Undecided, got {sc:?}"
);
let h3: &[i8] = &[-4, 2, 4, -4, 2, 4, -2, 4, -2, 2, 2, 4];
let ts = TileSet::single(Rat::<ZZ12>::from_slice_trusted(h3));
let hn = heesch_number(ts, 0, 4, 20_000_000);
eprintln!("Heesch-3 tile heesch_number(bound=4) -> {hn:?}");
assert!(
matches!(hn, Heesch::Finite(3)),
"known Heesch-3 tile must report Finite(3), got {hn:?}"
);
let hc = classify_one(h3);
eprintln!("Heesch-3 tile --fast -> {hc:?}");
assert!(
interesting(&hc),
"known Heesch-3 tile must be kept (heesch>=2/Undecided), got {hc:?}"
);
}
#[test]
fn perimeter_ranges_partition_the_index_space() {
let mut prev_end = 0u64;
for (n, &count) in FREE_ZZ12.iter().enumerate().skip(1) {
let (s, e) = perimeter_range(&FREE_ZZ12, n);
assert_eq!(s, prev_end, "perimeter {n} starts where {} ended", n - 1);
assert_eq!(e - s, count, "perimeter {n} block size");
prev_end = e;
}
for n in 3..=13 {
let (s, e) = perimeter_range(&FREE_ZZ12, n);
assert_eq!(perimeter_of(&FREE_ZZ12, s), Some(n));
assert_eq!(perimeter_of(&FREE_ZZ12, e - 1), Some(n));
}
}
#[test]
#[ignore = "metadata readers: needs the ratdb assets (ro-crate-metadata.json)"]
fn asset_metadata_readers_match_conventions() {
assert_eq!(asset_ring("web/ratdb/data/zz12_n16_free"), 12);
assert_eq!(
asset_free_counts("web/ratdb/data/zz12_n16_free"),
FREE_ZZ12.to_vec(),
"zz12 asset counts must equal the FREE_ZZ12 const"
);
assert_eq!(asset_ring("web/ratdb/data/zz10_n18_free"), 10);
let z10 = asset_free_counts("web/ratdb/data/zz10_n18_free");
assert_eq!(z10[0], 0, "leading 0 prepended (index == perimeter)");
assert_eq!(z10[16], 66_850_339, "zz10 perimeter-16 free count");
assert_eq!(z10.iter().sum::<u64>(), 2_875_831_850, "zz10 total");
}
#[test]
fn read_resume_drops_bad_and_duplicate_lines() {
let undecided = r#"{"Undecided":{"depth":0,"corona":[]}}"#;
let out = std::env::temp_dir().join("read_resume_test.jsonl");
let txt =
format!("5\t{undecided}\n6\tGARBAGE\n7\t{undecided}\n5\t{undecided}\n8\t{undecided}");
std::fs::write(&out, &txt).unwrap();
let (done, tally) = read_resume(&out, 0, 100, &[]);
assert_eq!(tally, [0, 0, 2], "idx 5 and 7 tallied once each");
assert!(done.contains(&5) && done.contains(&7) && !done.contains(&6) && !done.contains(&8));
let back = std::fs::read_to_string(&out).unwrap();
assert_eq!(back, format!("5\t{undecided}\n7\t{undecided}\n"));
let (done2, tally2) = read_resume(&out, 0, 100, &[]);
assert_eq!((done2.len(), tally2), (2, [0, 0, 2]));
assert_eq!(std::fs::read_to_string(&out).unwrap(), back);
let _ = std::fs::remove_file(&out);
}
#[test]
fn coverage_sidecar_primitives() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("cov_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let store = dir.join("s.jsonl");
let mut f = std::fs::File::create(ranges_path(&store)).unwrap();
writeln!(f, "10\t20\t1\t8\t1").unwrap();
writeln!(f, "20\t30\t2\t8\t0").unwrap();
writeln!(f, "25\t35\t0\t5\t0").unwrap();
writeln!(f, "50\t60\t3\t7\t0").unwrap();
write!(f, "70\t80").unwrap(); drop(f);
let ranges = read_ranges(&store);
assert_eq!(ranges.len(), 4, "torn tail skipped");
let union = covered_union(&ranges);
assert_eq!(union, vec![(10, 35), (50, 60)]);
for (idx, want) in [
(9, false),
(10, true),
(34, true),
(35, false),
(49, false),
(50, true),
(59, true),
(60, false),
] {
assert_eq!(in_covered_union(&union, idx), want, "idx {idx}");
}
let undec = serde_json::to_string(&Classified::Undecided {
depth: 0,
corona: vec![],
})
.unwrap();
let mut f = std::fs::File::create(&store).unwrap();
writeln!(f, "12\t{undec}").unwrap(); writeln!(f, "40\t{undec}").unwrap(); drop(f);
let (done, tally) = read_resume(&store, 0, 100, &union);
assert!(!done.contains(&12) && done.contains(&40));
assert_eq!(tally, [0, 0, 1], "only the uncovered line tallied");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
#[ignore = "coverage roundtrip: range launches + filter + verify (needs the ratdb asset)"]
fn range_launch_filter_verify_roundtrip() {
use crate::cyclotomic::ZZ12;
use std::io::Write;
let asset = std::env::var("ASSET").unwrap_or("web/ratdb/data/zz12_n16_free".into());
let d = open_ratdb(&asset);
let dir = std::env::temp_dir().join(format!("covrt_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let store = dir.join("s.jsonl");
let (s8, e8) = perimeter_range(&FREE_ZZ12, 8);
let mid = (s8 + e8) / 2;
let cfg = FastConfig {
chunk: 100,
..FastConfig::default()
};
let st1 = run_fast_range::<ZZ12, _>(&d, s8, mid, &store, &cfg);
let st2 = run_fast_range::<ZZ12, _>(&d, mid, e8, &store, &cfg);
assert_eq!(st1.total as u64, mid - s8);
assert_eq!(st2.total as u64, e8 - mid);
let ranges = read_ranges(&store);
assert!(!ranges.is_empty(), "windows recorded");
let union = covered_union(&ranges);
assert!(in_covered_union(&union, s8) && in_covered_union(&union, e8 - 1));
let total_tallied: [usize; 3] = ranges.iter().fold([0; 3], |mut a, r| {
for (acc, t) in a.iter_mut().zip(r.tally) {
*acc += t;
}
a
});
assert_eq!(
total_tallied.iter().sum::<usize>() as u64,
e8 - s8,
"tallies cover the block"
);
let txt = std::fs::read_to_string(&store).unwrap();
let mut f = std::fs::File::create(&store).unwrap();
let mut dropped = 0;
for line in txt.lines() {
let (idx_s, json) = line.split_once('\t').unwrap();
let idx: u64 = idx_s.parse().unwrap();
let c: Classified = serde_json::from_str(json).unwrap();
if in_covered_union(&union, idx)
&& matches!(c, Classified::Decided(Verdict::CannotTile(_)))
{
dropped += 1;
continue;
}
writeln!(f, "{line}").unwrap();
}
drop(f);
assert!(dropped > 0, "the boring bulk existed and was dropped");
let st3 = run_fast::<ZZ12, _>(&d, &FREE_ZZ12, 8, &store, &cfg);
assert_eq!(st3.total as u64, e8 - s8, "all accounted");
assert_eq!(
(st3.periodic + st3.cannot_tile + st3.undecided) as u64,
e8 - s8,
"tallies intact after filtering (ranges carry the dropped lines)"
);
assert_eq!(
read_ranges(&store).len(),
ranges.len(),
"no duplicate coverage appended"
);
let report = run_verify::<ZZ12, _>(&d, &FREE_ZZ12, &store, Some(8));
assert!(
report.is_clean(),
"filtered store verifies clean via coverage: {report:?}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
#[ignore = "pipeline smoke: windowed fast pass + verify on small perimeters (needs the ratdb asset)"]
#[allow(clippy::needless_range_loop)] fn fast_then_verify_small_perimeters() {
use crate::cyclotomic::ZZ12;
let asset =
std::env::var("ASSET").unwrap_or_else(|_| "web/ratdb/data/zz12_n16_free".into());
let d = open_ratdb(&asset);
let out = std::env::temp_dir().join("classify_smoke.jsonl");
let _ = std::fs::remove_file(&out);
let cfg = FastConfig {
chunk: 50,
..FastConfig::default()
};
for perim in 3..=8 {
let s = run_fast::<ZZ12, _>(&d, &FREE_ZZ12, perim, &out, &cfg);
assert_eq!(s.verify_fail, 0);
assert_eq!(s.total as u64, FREE_ZZ12[perim]);
}
let report = run_verify::<ZZ12, _>(&d, &FREE_ZZ12, &out, None);
assert!(report.is_clean(), "smoke store must be clean: {report:?}");
}
#[test]
fn pack_then_lookup_roundtrip() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("pack_lookup_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let store = dir.join("s.jsonl");
let idxs: Vec<u64> = (0..500u64).map(|i| (i * 7919) % 100_000).collect();
{
let mut f = std::io::BufWriter::new(std::fs::File::create(&store).unwrap());
for (n, &idx) in idxs.iter().enumerate() {
let c = Classified::Undecided {
depth: (n % 7),
corona: vec![
crate::geom::matches::PatchMatch::new(
crate::geom::matches::EdgeRange::new(n % 13, 1),
crate::geom::matches::Segment::new(
0,
crate::geom::matches::EdgeRange::new(n % 5, 1)
),
);
n % 40
],
};
writeln!(f, "{idx}\t{}", serde_json::to_string(&c).unwrap()).unwrap();
}
}
let n = run_pack(&store);
assert_eq!(n, idxs.len());
for (i, &idx) in idxs.iter().enumerate() {
let c = store_lookup(&store, idx)
.unwrap()
.unwrap_or_else(|| panic!("idx {idx} found"));
match c {
Classified::Undecided { depth, corona } => {
assert_eq!(depth, i % 7, "idx {idx} verdict intact");
assert_eq!(corona.len(), i % 40);
}
other => panic!("unexpected verdict {other:?}"),
}
}
let present: std::collections::HashSet<u64> = idxs.iter().copied().collect();
for probe in [1u64, 3, 99_999, 50_001] {
if !present.contains(&probe) {
assert!(
store_lookup(&store, probe).unwrap().is_none(),
"idx {probe} absent"
);
}
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn merge_overrides_undecided_only() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("merge_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (store, overlay) = (dir.join("s.jsonl"), dir.join("s.deep.jsonl"));
let undec = |d: usize| {
serde_json::to_string(&Classified::Undecided {
depth: d,
corona: vec![],
})
.unwrap()
};
let decided = serde_json::to_string(&Classified::Decided(Verdict::CannotTile(
crate::classify::cert::HeeschCert {
heesch: 0,
status: crate::classify::cert::HeeschStatus::Finite,
build: vec![],
bound: 1,
budget: 1,
},
)))
.unwrap();
let mut f = std::fs::File::create(&store).unwrap();
writeln!(f, "10\t{}", undec(1)).unwrap();
writeln!(f, "20\t{decided}").unwrap();
writeln!(f, "30\t{}", undec(2)).unwrap();
writeln!(f, "40\t{}", undec(3)).unwrap();
let mut f = std::fs::File::create(&overlay).unwrap();
writeln!(f, "10\t{decided}").unwrap();
writeln!(f, "20\t{decided}").unwrap();
writeln!(f, "30\t{}", undec(6)).unwrap();
writeln!(f, "99\t{decided}").unwrap();
let stats = run_merge(&store, &overlay);
assert_eq!(
stats,
MergeStats {
overridden: 1,
kept_decided: 1,
still_undecided: 1,
appended: 1
}
);
let txt = std::fs::read_to_string(&store).unwrap();
let lines: Vec<&str> = txt.lines().collect();
assert_eq!(lines.len(), 5);
assert!(
lines[0].starts_with("10\t") && lines[0].contains("CannotTile"),
"10 overridden"
);
assert!(lines[1].starts_with("20\t"), "20 kept in place");
assert!(
lines[2].starts_with("30\t") && lines[2].contains(r#""depth":2"#),
"30 untouched"
);
assert!(
lines[3].starts_with("40\t"),
"40 untouched (no overlay entry)"
);
assert!(lines[4].starts_with("99\t"), "99 appended");
std::fs::remove_dir_all(&dir).ok();
}
}