use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, ValueEnum};
use tilezz::classify::cert::{Classified, Verdict};
use tilezz::classify::classify_tiles::{
DeepConfig, FastConfig, StageMode, asset_free_counts, asset_ring, open_ratdb, run_deep,
run_fast, run_fast_range, run_reptile_screen, run_verify,
};
use tilezz::classify::heesch::{enumerate_coronas, heesch_number_witnessed};
use tilezz::classify::render::{TileColoring, witness_svg};
use tilezz::classify::reptile::reptile_cert;
use tilezz::classify::store::{run_merge, run_pack, store_lookup};
use tilezz::enumerate::canonical::ccw_free_canonical;
use tilezz::geom::rat::Rat;
use tilezz::geom::tileset::TileSet;
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
enum RenderView {
Corona,
Rotation,
Both,
}
#[derive(Parser)]
#[command(
name = "classify_tiles",
// Embeds the source commit (via TILEZZ_GIT_COMMIT), matching rat_enum,
// so a cert store's verify harness can provenance-guard the binary it
// built before trusting its --verify verdict.
version = tilezz::VERSION,
about = "Classify a ratdb DAFSA dataset into a certified verdict store (default fast \
classification; --deep crunches the Undecided residue; --verify checks a store)."
)]
struct Cli {
#[arg(long, default_value = "web/ratdb/data/zz12_n16_free")]
asset: String,
#[arg(long)]
workers: Option<usize>,
#[arg(long, required_unless_present_any = ["render", "coronas", "reptile"])]
store: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["deep", "verify"])]
fast: bool,
#[arg(long, conflicts_with_all = ["fast", "verify"])]
deep: bool,
#[arg(long, value_name = "OVERLAY", conflicts_with_all = ["fast", "deep", "verify", "pack", "lookup"])]
merge: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["fast", "deep"])]
verify: bool,
#[arg(long, conflicts_with_all = ["fast", "deep", "verify", "lookup"])]
pack: bool,
#[arg(long, conflicts_with_all = ["fast", "deep", "verify", "pack"])]
lookup: Option<String>,
#[arg(long, conflicts_with_all = ["fast", "deep", "verify", "pack", "lookup", "merge"])]
render: Option<String>,
#[arg(long, value_enum, default_value_t = RenderView::Both, requires = "render")]
render_mode: RenderView,
#[arg(long)]
render_out: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["fast", "deep", "verify", "pack", "lookup", "merge", "render"])]
coronas: Option<String>,
#[arg(long, default_value_t = 2, requires = "coronas")]
coronas_k: usize,
#[arg(long, requires = "coronas")]
coronas_edge: bool,
#[arg(long, default_value_t = 64, requires = "coronas")]
coronas_cap: usize,
#[arg(long, conflicts_with_all = ["fast", "deep", "verify", "pack", "lookup", "merge", "render", "coronas"])]
reptile: Option<String>,
#[arg(long, default_value_t = 6)]
reptile_kmax: usize,
#[arg(long)]
reptile_out: Option<PathBuf>,
#[arg(long, requires = "reptile_out", conflicts_with_all = ["fast", "deep", "verify", "pack", "lookup", "merge", "render", "coronas", "reptile"])]
reptile_screen: bool,
#[arg(long, default_value_t = 300_000)]
reptile_budget: usize,
#[arg(long)]
perim: Option<usize>,
#[arg(long, requires = "to", conflicts_with_all = ["perim", "deep", "verify", "pack", "lookup", "merge"])]
from: Option<u64>,
#[arg(long, requires = "from")]
to: Option<u64>,
#[arg(long, conflicts_with_all = ["deep", "verify"], default_value_t = 0)]
chunk: usize,
#[arg(long, conflicts_with_all = ["heesch_only", "verify"])]
periodic_only: bool,
#[arg(long, conflicts_with_all = ["periodic_only", "verify"])]
heesch_only: bool,
#[arg(long, conflicts_with_all = ["deep", "verify"])]
deep_budget: Option<usize>,
#[arg(long, conflicts_with_all = ["fast", "verify"])]
res_budget: Option<usize>,
#[arg(long, conflicts_with_all = ["fast", "verify"])]
aniso_restarts: Option<usize>,
}
fn parse_lookup(s: &str) -> Result<Result<u64, Vec<i8>>, String> {
if let Ok(idx) = s.trim().parse::<u64>() {
return Ok(Ok(idx));
}
let cleaned = s.replace(['[', ']'], " ");
let word: Result<Vec<i8>, _> = cleaned
.split([',', ' '])
.filter(|t| !t.is_empty())
.map(|t| t.parse::<i8>())
.collect();
match word {
Ok(w) if !w.is_empty() => Ok(Err(w)),
_ => Err(format!("--lookup: neither an index nor a turn word: {s}")),
}
}
fn verdict_summary(c: &Classified) -> String {
match c {
Classified::Decided(Verdict::Periodic(pc)) => format!(
"PERIODIC via {:?}: meta-tile of {} base copies, {} glue pairs (fundamental domain replayable from `build`)",
pc.via,
pc.build.len() + 1,
pc.glue.len()
),
Classified::Decided(Verdict::CannotTile(hc)) => format!(
"CANNOT TILE: Heesch {} ({:?}, bound {}, budget {}), corona witness {} glues",
hc.heesch,
hc.status,
hc.bound,
hc.budget,
hc.build.len()
),
Classified::Undecided { depth, corona } => format!(
"UNDECIDED (aperiodic candidate): witnessed corona depth {depth}, witness {} glues",
corona.len()
),
}
}
impl Cli {
fn store(&self) -> &PathBuf {
self.store
.as_ref()
.expect("--store is required for this mode")
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let d = open_ratdb(&cli.asset);
if cli.pack {
let n = run_pack(cli.store());
eprintln!(
"pack: {n} lines sorted by index in {}",
cli.store().display()
);
return ExitCode::SUCCESS;
}
if let Some(overlay) = &cli.merge {
run_merge(cli.store(), overlay);
return ExitCode::SUCCESS;
}
let counts = asset_free_counts(&cli.asset);
let ring = asset_ring(&cli.asset);
tilezz::dispatch_ring!(
ring,
run_ring::<ZZ, _>(&cli, &d, &counts),
else {
eprintln!("unsupported ring: {ring} (asset effectiveRing not in the dispatch table)");
ExitCode::FAILURE
}
)
}
fn run_ring<T: tilezz::cyclotomic::IsRing, F: Fn(u32) -> std::io::Result<Vec<u8>>>(
cli: &Cli,
d: &tilezz::dataset::LazyRatDafsa<F>,
counts: &[u64],
) -> ExitCode {
let workers = cli.workers.unwrap_or(0);
let resolve = |q: &str| -> Result<(Vec<i8>, Option<u64>), String> {
match parse_lookup(q)? {
Ok(idx) => match d.get(idx) {
Some(s) => Ok((s, Some(idx))),
None => Err(format!("index {idx} is outside the dataset range")),
},
Err(word) => {
let canon = ccw_free_canonical(&word);
let idx = d.index_of(&canon);
Ok((canon, idx))
}
}
};
if let Some(q) = &cli.lookup {
let idx = match parse_lookup(q) {
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
Ok(Ok(idx)) => idx,
Ok(Err(word)) => {
let canon = ccw_free_canonical(&word);
match d.index_of(&canon) {
Some(idx) => {
eprintln!("word canonicalized to {canon:?} = dataset index {idx}");
idx
}
None => {
eprintln!("word {word:?} (canonical {canon:?}) is not in the dataset");
return ExitCode::FAILURE;
}
}
}
};
return match store_lookup(cli.store(), idx) {
Err(e) => {
eprintln!("lookup failed: {e} (is the store packed? run --pack)");
ExitCode::FAILURE
}
Ok(None) => {
eprintln!(
"idx {idx}: no verdict in this store (not yet classified, or store not packed -- run --pack)"
);
ExitCode::FAILURE
}
Ok(Some(c)) => {
let seq = d.get(idx).expect("index in dataset range");
println!("idx {idx} seq {seq:?}");
println!("{}", verdict_summary(&c));
println!("{}", serde_json::to_string(&c).unwrap());
ExitCode::SUCCESS
}
};
}
if let Some(q) = &cli.render {
let (seq, idx) = match resolve(q) {
Ok(x) => x,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let base = Rat::<T>::from_slice_trusted(&seq);
let (h, build) = heesch_number_witnessed(TileSet::single(base.clone()), 0, 3, 20_000_000);
eprintln!(
"render {seq:?}: {h:?}, {} tiles (center + {} glues)",
build.len() + 1,
build.len()
);
let label = idx.map_or_else(|| "tile".to_string(), |i| i.to_string());
let stem = cli
.render_out
.clone()
.unwrap_or_else(|| std::path::PathBuf::from(format!("corona_{label}")));
let stem = stem.to_string_lossy();
let stem = stem.strip_suffix(".svg").unwrap_or(&stem);
let both = cli.render_mode == RenderView::Both;
let modes: &[(TileColoring, &str)] = match cli.render_mode {
RenderView::Corona => &[(TileColoring::Corona, "corona")],
RenderView::Rotation => &[(TileColoring::Rotation, "rotation")],
RenderView::Both => &[
(TileColoring::Corona, "corona"),
(TileColoring::Rotation, "rotation"),
],
};
for &(mode, tag) in modes {
let Some(svg) = witness_svg(&base, &build, mode, 720) else {
eprintln!("render failed: could not replay the corona witness");
return ExitCode::FAILURE;
};
let path = if both {
PathBuf::from(format!("{stem}_{tag}.svg"))
} else {
PathBuf::from(format!("{stem}.svg"))
};
if let Err(e) = std::fs::write(&path, svg) {
eprintln!("write {}: {e}", path.display());
return ExitCode::FAILURE;
}
eprintln!("wrote {} ({tag})", path.display());
}
return ExitCode::SUCCESS;
}
if let Some(q) = &cli.coronas {
let (seq, idx) = match resolve(q) {
Ok(x) => x,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let base = Rat::<T>::from_slice_trusted(&seq);
let ts = TileSet::single(base.clone());
let (builds, capped) = enumerate_coronas(
&ts,
0,
cli.coronas_k,
cli.coronas_edge,
20_000_000,
cli.coronas_cap,
);
let kind = if cli.coronas_edge { "edge" } else { "true" };
eprintln!(
"{} distinct {kind} {}-coronas{}",
builds.len(),
cli.coronas_k,
if capped { " (cap/budget hit)" } else { "" }
);
let label = idx.map_or_else(|| "tile".to_string(), |i| i.to_string());
let stem = cli.render_out.clone().unwrap_or_else(|| {
std::path::PathBuf::from(format!("coronas_{label}_{kind}{}", cli.coronas_k))
});
let stem = stem.to_string_lossy();
let stem = stem.strip_suffix(".svg").unwrap_or(&stem);
for (n, build) in builds.iter().enumerate() {
let Some(svg) = witness_svg(&base, build, TileColoring::Corona, 480) else {
continue;
};
let path = PathBuf::from(format!("{stem}_{n:02}.svg"));
if let Err(e) = std::fs::write(&path, svg) {
eprintln!("write {}: {e}", path.display());
return ExitCode::FAILURE;
}
}
eprintln!("wrote {} svgs as {stem}_NN.svg", builds.len());
return ExitCode::SUCCESS;
}
if let Some(q) = &cli.reptile {
let (seq, idx) = match resolve(q) {
Ok(x) => x,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let base = Rat::<T>::from_slice_trusted(&seq);
let label = idx.map_or_else(|| "tile".to_string(), |i| i.to_string());
match reptile_cert(&base, cli.reptile_kmax) {
None => eprintln!(
"{label} {seq:?}: NOT a rep-tile through scale k={} (a proof -- exhaustive over \
orientation-preserving copies)",
cli.reptile_kmax
),
Some(cert) => {
let order = cert.k * cert.k;
debug_assert!(cert.verify(&base), "minted cert must self-verify");
eprintln!(
"{label} {seq:?}: REP-TILE of order {order} -- {order} copies tile the \
{}x-scaled tile ({} glues in `build`)",
cert.k,
cert.build.len()
);
println!("{}", serde_json::to_string(&cert).unwrap());
if let Some(out) = &cli.reptile_out {
match witness_svg(&base, &cert.build, TileColoring::Rotation, 720) {
Some(svg) => {
let stem = out.to_string_lossy();
let path = PathBuf::from(format!(
"{}.svg",
stem.strip_suffix(".svg").unwrap_or(&stem)
));
if let Err(e) = std::fs::write(&path, svg) {
eprintln!("write {}: {e}", path.display());
return ExitCode::FAILURE;
}
eprintln!("wrote {}", path.display());
}
None => eprintln!("render failed: could not replay the witness"),
}
}
}
}
return ExitCode::SUCCESS;
}
if cli.reptile_screen {
let out = cli
.reptile_out
.as_ref()
.expect("--reptile-out (clap requires it)");
run_reptile_screen::<T, _>(d, cli.store(), out, cli.reptile_kmax, cli.reptile_budget);
return ExitCode::SUCCESS;
}
if cli.verify {
let report = run_verify::<T, _>(d, counts, cli.store(), cli.perim);
return if report.is_clean() {
eprintln!("verify: store is clean.");
ExitCode::SUCCESS
} else {
eprintln!("verify: store is DIRTY: {report:?}");
ExitCode::FAILURE
};
}
let mode = if cli.periodic_only {
StageMode::PeriodicOnly
} else if cli.heesch_only {
StageMode::HeeschOnly
} else {
StageMode::Full
};
if cli.deep {
let mut cfg = DeepConfig {
mode,
workers,
..DeepConfig::default()
};
if let Some(b) = cli.res_budget {
cfg.res_budget = b;
}
if let Some(r) = cli.aniso_restarts {
cfg.bounds.aniso_restarts = r;
}
run_deep::<T, _>(d, cli.store(), &cfg);
return ExitCode::SUCCESS;
}
let mut cfg = FastConfig {
mode,
workers,
chunk: cli.chunk,
..FastConfig::default()
};
if let Some(b) = cli.deep_budget {
cfg.deep_budget = b;
}
match (cli.from, cli.to, cli.perim) {
(Some(from), Some(to), _) => {
run_fast_range::<T, _>(d, from, to, cli.store(), &cfg);
}
(_, _, Some(perim)) => {
run_fast::<T, _>(d, counts, perim, cli.store(), &cfg);
}
_ => {
eprintln!("error: the fast classification needs --perim <N> or --from/--to");
return ExitCode::FAILURE;
}
}
ExitCode::SUCCESS
}