use rustc_hash::FxHashSet as HashSet;
use std::sync::Arc;
use crate::classify::grow;
use crate::combinatorics::junctiontypes::OpenJunctionTypeIndex;
use crate::cyclotomic::IsRing;
use crate::geom::matches::PatchMatch;
use crate::geom::patch::{EPatch, Patch};
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::geom::vertices::OpenJunctionType;
pub(crate) fn cursed_junction_types<T: IsRing>(
tileset: &Arc<TileSet<T>>,
) -> HashSet<OpenJunctionType> {
OpenJunctionTypeIndex::new(tileset.clone())
.entries()
.iter()
.filter(|e| e.is_cursed())
.map(|e| e.jtype().clone())
.collect()
}
fn has_cursed_frozen<T: IsRing, P: Patch<T>, J: Fn(&P, usize) -> Option<OpenJunctionType>>(
patch: &P,
frozen: &HashSet<T>,
cursed: &HashSet<OpenJunctionType>,
junction_at: &J,
) -> bool {
if cursed.is_empty() {
return false;
}
let pos = patch.boundary_positions();
for (i, p) in pos.iter().enumerate().take(patch.angles().len()) {
if frozen.contains(p)
&& let Some(jt) = junction_at(patch, i)
&& cursed.contains(&jt)
{
return true;
}
}
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Heesch {
Finite(usize),
AtLeast(usize),
Unknown(usize),
}
impl Heesch {
pub fn cannot_tile(self) -> bool {
matches!(self, Heesch::Finite(_))
}
}
fn frozen_open<T: IsRing, P: Patch<T>>(patch: &P, frozen: &HashSet<T>) -> i64 {
let pos = patch.boundary_positions();
let ang = patch.angles();
let half = T::turn() as i64 / 2;
(0..ang.len())
.filter(|&i| frozen.contains(&pos[i]))
.map(|i| half + ang[i] as i64)
.sum()
}
fn frozen_coords<T: IsRing, P: Patch<T>>(patch: &P, threshold: usize) -> HashSet<T> {
let pos = patch.boundary_positions();
let pt = patch.patch_tile_ids();
let mut s = HashSet::default();
for i in 0..pt.len() {
if pt[i] < threshold {
s.insert(pos[i]);
s.insert(pos[i + 1]); }
}
s
}
fn exposed<T: IsRing, P: Patch<T>>(patch: &P, threshold: usize) -> usize {
patch
.patch_tile_ids()
.iter()
.filter(|&&id| id < threshold)
.count()
}
pub(crate) trait TileSink<T: IsRing, P: Patch<T>> {
fn push(&mut self, pm: &PatchMatch, patch: &P, new_id: usize) -> bool;
fn pop(&mut self);
fn mark(&mut self, _depth: usize, _patch: &P) {}
}
pub(crate) struct PathSink<'a> {
pub path: &'a mut Vec<PatchMatch>,
}
impl<T: IsRing, P: Patch<T>> TileSink<T, P> for PathSink<'_> {
fn push(&mut self, pm: &PatchMatch, _: &P, _: usize) -> bool {
self.path.push(*pm);
true
}
fn pop(&mut self) {
self.path.pop();
}
}
pub(crate) struct BestPathSink {
current: Vec<PatchMatch>,
best: Vec<PatchMatch>,
best_depth: usize,
}
impl BestPathSink {
fn new() -> Self {
Self {
current: Vec::new(),
best: Vec::new(),
best_depth: 0,
}
}
fn restart(&mut self, first: &PatchMatch) {
self.current.clear();
self.current.push(*first);
}
}
impl<T: IsRing, P: Patch<T>> TileSink<T, P> for BestPathSink {
fn push(&mut self, pm: &PatchMatch, _: &P, _: usize) -> bool {
self.current.push(*pm);
true
}
fn pop(&mut self) {
self.current.pop();
}
fn mark(&mut self, depth: usize, _patch: &P) {
if depth > self.best_depth {
self.best_depth = depth;
self.best.clone_from(&self.current);
}
}
}
pub(crate) struct BurySearch<'a> {
pub bound: usize,
pub budget: usize,
pub spent: usize,
pub budget_hit: bool,
pub cursed: &'a HashSet<OpenJunctionType>,
pub edge_only: bool,
pub enumerate: bool,
}
impl<'a> BurySearch<'a> {
pub fn new(
bound: usize,
budget: usize,
cursed: &'a HashSet<OpenJunctionType>,
edge_only: bool,
) -> Self {
BurySearch {
bound,
budget,
spent: 0,
budget_hit: false,
cursed,
edge_only,
enumerate: false,
}
}
}
pub(crate) fn bury<
T: IsRing,
P: Patch<T>,
S: TileSink<T, P>,
J: Fn(&P, usize) -> Option<OpenJunctionType>,
>(
patch: &P,
threshold: usize,
frozen: &HashSet<T>,
completed: usize,
ctx: &mut BurySearch<'_>,
sink: &mut S,
junction_at: &J,
) -> usize {
let (bound, edge_only) = (ctx.bound, ctx.edge_only);
let exp = exposed(patch, threshold);
if exp > 0 {
let ids = patch.patch_tile_ids();
let angles = patch.angles();
let edge = (0..ids.len())
.filter(|&i| ids[i] < threshold)
.min_by_key(|&i| angles[i])
.expect("exp > 0");
return branch(
patch,
threshold,
frozen,
completed,
ctx,
sink,
junction_at,
patch.get_matches_in_edge_range(edge, edge),
|next| exposed(next, threshold) < exp,
);
}
let m = if edge_only {
0
} else {
frozen_open(patch, frozen)
};
if m == 0 {
let done = completed + 1;
sink.mark(done, patch); if done >= bound {
return bound;
}
let next_threshold = patch.next_tile_id();
let next_frozen = frozen_coords(patch, next_threshold);
return bury(
patch,
next_threshold,
&next_frozen,
done,
ctx,
sink,
junction_at,
);
}
let pos = patch.boundary_positions();
let angles = patch.angles();
let vi = (0..angles.len())
.filter(|&i| frozen.contains(&pos[i]))
.min_by_key(|&i| angles[i])
.expect("frozen_open > 0 means a frozen vertex is on the boundary");
branch(
patch,
threshold,
frozen,
completed,
ctx,
sink,
junction_at,
patch.get_matches_touching_vertex(vi),
|next| frozen_open(next, frozen) < m,
)
}
#[allow(clippy::too_many_arguments)] fn branch<
T: IsRing,
P: Patch<T>,
S: TileSink<T, P>,
J: Fn(&P, usize) -> Option<OpenJunctionType>,
>(
patch: &P,
threshold: usize,
frozen: &HashSet<T>,
completed: usize,
ctx: &mut BurySearch<'_>,
sink: &mut S,
junction_at: &J,
candidates: Vec<PatchMatch>,
progress: impl Fn(&P) -> bool,
) -> usize {
let (bound, budget) = (ctx.bound, ctx.budget);
let mut best = completed;
for pm in candidates {
if ctx.spent >= budget {
ctx.budget_hit = true;
break;
}
ctx.spent += 1;
let new_id = patch.next_tile_id();
let mut next = patch.clone();
if next.add_tile(&pm).is_none() {
continue;
}
if !progress(&next) {
continue;
}
if has_cursed_frozen(&next, frozen, ctx.cursed, junction_at) {
continue; }
if !sink.push(&pm, &next, new_id) {
continue; }
let d = bury(&next, threshold, frozen, completed, ctx, sink, junction_at);
if d > best {
best = d;
}
if best >= bound && !ctx.enumerate {
return bound; }
sink.pop();
}
best
}
pub(crate) fn corona_witness<T: IsRing>(
tileset: &Arc<TileSet<T>>,
tile_id: usize,
target: usize,
budget: usize,
) -> Option<Vec<PatchMatch>> {
if target == 0 {
return Some(Vec::new());
}
let seed = EPatch::single_tile(tileset.clone(), tile_id);
let empty = HashSet::default();
let mut ctx = BurySearch::new(target, budget, &empty, false);
for first in seed.get_all_matches() {
let Some(gp) = seed.with_tile(&first) else {
continue;
};
let frozen = frozen_coords(&gp, 1);
let mut path = vec![first];
let reached = {
let mut sink = PathSink { path: &mut path };
bury(&gp, 1, &frozen, 0, &mut ctx, &mut sink, &|p, i| {
p.junction_type_at(i)
}) >= target
};
if reached {
return Some(path);
}
if ctx.spent >= budget {
break;
}
}
None
}
struct CollectSink {
k: usize,
cap: usize,
current: Vec<PatchMatch>,
all: Vec<Vec<PatchMatch>>,
}
impl CollectSink {
fn new(k: usize, cap: usize) -> Self {
Self {
k,
cap,
current: Vec::new(),
all: Vec::new(),
}
}
fn restart(&mut self, first: &PatchMatch) {
self.current.clear();
self.current.push(*first);
}
}
impl<T: IsRing, P: Patch<T>> TileSink<T, P> for CollectSink {
fn push(&mut self, pm: &PatchMatch, _: &P, _: usize) -> bool {
self.current.push(*pm);
true
}
fn pop(&mut self) {
self.current.pop();
}
fn mark(&mut self, depth: usize, _patch: &P) {
if depth == self.k && self.all.len() < self.cap {
self.all.push(self.current.clone());
}
}
}
fn corona_key<T: IsRing>(base: &Rat<T>, build: &[PatchMatch]) -> Option<Vec<i8>> {
if build.is_empty() {
return Some(Vec::new());
}
let gp = grow::replay_recipe(base, build, |_, _| true)?;
Some(crate::stringmatch::canonical_rotation(gp.to_rat().seq()))
}
pub fn enumerate_coronas<T: IsRing>(
tileset: &Arc<TileSet<T>>,
tile_id: usize,
k: usize,
edge_only: bool,
budget: usize,
cap: usize,
) -> (Vec<Vec<PatchMatch>>, bool) {
if k == 0 {
return (vec![Vec::new()], false);
}
let seed = EPatch::single_tile(tileset.clone(), tile_id);
let empty = HashSet::default();
let mut ctx = BurySearch::new(k, budget, &empty, edge_only);
ctx.enumerate = true;
let mut sink = CollectSink::new(k, cap.saturating_mul(4));
for first in seed.get_all_matches() {
if ctx.spent >= budget || sink.all.len() >= sink.cap {
break;
}
let Some(gp) = seed.with_tile(&first) else {
continue;
};
let frozen = frozen_coords(&gp, 1);
sink.restart(&first);
bury(&gp, 1, &frozen, 0, &mut ctx, &mut sink, &|p, i| {
p.junction_type_at(i)
});
}
let base = tileset.rat(tile_id);
let mut seen: HashSet<Vec<i8>> = HashSet::default();
let mut out: Vec<Vec<PatchMatch>> = Vec::new();
for build in sink.all {
if out.len() >= cap {
break;
}
if let Some(key) = corona_key::<T>(base, &build)
&& seen.insert(key)
{
out.push(build);
}
}
let hit_limit = ctx.budget_hit || seen.len() >= cap;
(out, hit_limit)
}
fn corona_closed<T: IsRing, P: Patch<T>>(gp: &P, threshold: usize, frozen: &HashSet<T>) -> bool {
exposed(gp, threshold) == 0 && frozen_open(gp, frozen) == 0
}
fn advance_closed_coronas<T: IsRing, P: Patch<T>>(
gp: &P,
threshold: &mut usize,
frozen: &mut HashSet<T>,
completed: &mut usize,
) {
while corona_closed(gp, *threshold, frozen) {
*completed += 1;
*threshold = gp.next_tile_id();
*frozen = frozen_coords(gp, *threshold);
}
}
pub(crate) fn count_coronas<T: IsRing>(base: &Rat<T>, build: &[PatchMatch]) -> usize {
if build.is_empty() {
return 0;
}
let mut threshold = 1usize;
let mut frozen: HashSet<T> = HashSet::default();
let mut completed = 0usize;
let mut initialized = false;
grow::replay_recipe(base, build, |gp, _new_id| {
if !initialized {
frozen = frozen_coords(gp, threshold);
initialized = true;
}
advance_closed_coronas(gp, &mut threshold, &mut frozen, &mut completed);
true
});
completed
}
fn heesch_search<T: IsRing>(
tileset: &Arc<TileSet<T>>,
tile_id: usize,
bound: usize,
budget: usize,
cursed: &HashSet<OpenJunctionType>,
edge_only: bool,
) -> (Heesch, Vec<PatchMatch>) {
if bound == 0 {
return (Heesch::AtLeast(0), Vec::new());
}
let seed = EPatch::single_tile(tileset.clone(), tile_id);
let candidates: Vec<_> = seed.get_all_matches();
let mut ctx = BurySearch::new(bound, budget, cursed, edge_only);
let mut best = 0usize;
let mut sink = BestPathSink::new();
for pm in &candidates {
if ctx.spent >= budget {
ctx.budget_hit = true;
break;
}
ctx.spent += 1;
let Some(gp) = seed.with_tile(pm) else {
continue;
};
let frozen = frozen_coords(&gp, 1);
sink.restart(pm);
let d = bury(&gp, 1, &frozen, 0, &mut ctx, &mut sink, &|p, i| {
p.junction_type_at(i)
});
if d > best {
best = d;
}
if best >= bound {
return (Heesch::AtLeast(bound), std::mem::take(&mut sink.best));
}
}
let result = if ctx.budget_hit {
Heesch::Unknown(best)
} else {
Heesch::Finite(best)
};
if crate::classify::trace::heesch_spent() {
eprintln!(
"HEESCH_SPENT bound={bound} budget={budget} spent={} -> {result:?}",
ctx.spent
);
}
(result, std::mem::take(&mut sink.best))
}
const ESCALATE_NODES: usize = 20_000;
pub fn heesch_number<T: IsRing>(
tileset: Arc<TileSet<T>>,
tile_id: usize,
bound: usize,
budget: usize,
) -> Heesch {
heesch_number_witnessed(tileset, tile_id, bound, budget).0
}
pub fn heesch_number_witnessed<T: IsRing>(
tileset: Arc<TileSet<T>>,
tile_id: usize,
bound: usize,
budget: usize,
) -> (Heesch, Vec<PatchMatch>) {
let empty = HashSet::default();
if bound < 2 {
return heesch_search(&tileset, tile_id, bound, budget, &empty, false);
}
let probe = heesch_search(
&tileset,
tile_id,
bound,
budget.min(ESCALATE_NODES),
&empty,
false,
);
if !matches!(probe.0, Heesch::Unknown(_)) {
return probe;
}
let cursed = cursed_junction_types(&tileset);
heesch_search(&tileset, tile_id, bound, budget, &cursed, false)
}
#[cfg(all(test, feature = "cli"))]
pub(crate) fn heesch_search_cursed<T: IsRing>(
tileset: &Arc<TileSet<T>>,
tile_id: usize,
bound: usize,
budget: usize,
cursed: &HashSet<OpenJunctionType>,
) -> Heesch {
heesch_search(tileset, tile_id, bound, budget, cursed, false).0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::ZZ12;
use crate::geom::rat::Rat;
use crate::geom::tiles;
const HEESCH_BUDGET: usize = 200_000;
fn ts_of(rat: Rat<ZZ12>) -> Arc<TileSet<ZZ12>> {
TileSet::single(rat)
}
#[test]
#[ignore = "corona enumeration counts + validity (~seconds-minutes)"]
fn enumerate_coronas_valid_and_deduped() {
let seq: &[i8] = &[-4, 0, 2, 2, 2, 2, -2, 0, 2, 0, 2, 4, -2, 0, 4];
let ts = ts_of(Rat::<ZZ12>::from_slice_trusted(seq));
let base = ts.rat(0);
for (k, edge) in [(1, false), (2, false), (1, true), (2, true)] {
let (builds, capped) = enumerate_coronas(&ts, 0, k, edge, 5_000_000, 500);
let kind = if edge { "edge" } else { "true" };
eprintln!(
"{kind} {k}-coronas: {} distinct{}",
builds.len(),
if capped { " (CAPPED)" } else { "" }
);
assert!(
!builds.is_empty(),
"{kind} {k}-corona set empty for a Heesch-2 tile"
);
let mut keys = std::collections::HashSet::new();
for b in &builds {
assert!(!b.is_empty(), "empty corona build");
if !edge {
assert_eq!(count_coronas(base, b), k, "true {k}-corona has wrong depth");
}
assert!(
keys.insert(corona_key::<ZZ12>(base, b)),
"duplicate corona survived dedup"
);
}
}
}
#[test]
fn first_corona_separates_tilers_from_dodecagon() {
for (name, rat) in [
(
"triangle",
Rat::from_snake_trusted(&tiles::triangle::<ZZ12>()),
),
("square", Rat::from_snake_trusted(&tiles::square::<ZZ12>())),
(
"hexagon",
Rat::from_snake_trusted(&tiles::hexagon::<ZZ12>()),
),
] {
let v = heesch_number(ts_of(rat), 0, 1, HEESCH_BUDGET);
eprintln!("{name}: {v:?}");
assert_eq!(v, Heesch::AtLeast(1), "{name} must surround");
}
let dodec = heesch_number(
ts_of(Rat::from_snake_trusted(&tiles::dodecagon::<ZZ12>())),
0,
1,
HEESCH_BUDGET,
);
eprintln!("dodecagon: {dodec:?}");
assert_eq!(dodec, Heesch::Finite(0), "dodecagon cannot be surrounded");
}
#[cfg(feature = "cli")]
#[test]
#[ignore = "regression: true-Heesch screening at bound 2 (the filter default)"]
fn true_heesch_n10_regression() {
const BOUND: usize = 2;
const BUDGET: usize = 20_000_000; let ts = |s: &[i8]| ts_of(Rat::<ZZ12>::from_slice_trusted(s));
let tri = Rat::from_snake_trusted(&tiles::triangle::<ZZ12>());
let periodic: [&[i8]; 4] = [
tri.seq(), &[-4, 3, 4, 1, 3, 5], &[-2, -1, 2, 5, -2, 1, 2, 1, 2, 4], &[-4, 3, 4, 0, 4, -3, 5, 3], ];
for seq in periodic {
let t = std::time::Instant::now();
let h = heesch_number(ts(seq), 0, BOUND, BUDGET);
eprintln!("periodic {seq:?}: {h:?} [{:?}]", t.elapsed());
assert!(
!h.cannot_tile(),
"{seq:?} tiles; must not be Finite, got {h:?}"
);
assert!(
!matches!(h, Heesch::Unknown(_)),
"{seq:?}: bound-2 screen should not be Unknown, got {h:?}"
);
}
let finite: [(&[i8], usize); 5] = [
(&[-2, 1, 4, -1, 4, -1, 2, -1, 5, 1], 3),
(&[-3, 2, 4, -2, 5, -2, 3, -2, 5, 2], 3),
(&[-1, 0, -1, 4, 1, 2, 1, 0, 1, 5], 3),
(&[-3, 2, -3, 4, 3, 0, 3, -2, 3, 5], 3),
(&[-2, 1, -2, 4, 2, 1, 2, -1, 2, 5], 4), ];
let (mut rejected, mut candidates) = (0usize, 0usize);
for (seq, edge) in finite {
let t = std::time::Instant::now();
let h = heesch_number(ts(seq), 0, BOUND, BUDGET);
let el = t.elapsed();
match h {
Heesch::Finite(k) => {
assert!(
k < BOUND && k <= edge,
"{seq:?}: Finite({k}) out of range (edge {edge})"
);
eprintln!("finite {seq:?}: REJECTED at Heesch {k} (edge {edge}) [{el:?}]");
rejected += 1;
}
Heesch::AtLeast(b) => {
assert_eq!(b, BOUND, "{seq:?}: AtLeast({b}) but bound is {BOUND}");
eprintln!(
"finite {seq:?}: CANDIDATE (reaches corona {b}, edge {edge}) [{el:?}]"
);
candidates += 1;
}
Heesch::Unknown(k) => {
panic!(
"{seq:?}: Unknown({k}) at bound {BOUND} after {el:?} -- screen should exhaust or short-circuit"
);
}
}
}
eprintln!(
"bound-{BOUND} screen: {rejected} rejected outright, {candidates} candidates for a deeper pass"
);
assert_eq!(rejected + candidates, 5);
}
#[test]
fn dodecagon_heesch_zero_at_higher_bound() {
let v = heesch_number(
ts_of(Rat::from_snake_trusted(&tiles::dodecagon::<ZZ12>())),
0,
3,
HEESCH_BUDGET,
);
eprintln!("dodecagon bound 3: {v:?}");
assert_eq!(v, Heesch::Finite(0));
}
#[cfg(feature = "cli")]
#[test]
#[ignore = "regression: faithful cursed prune must not false-reject n=12 tiler 700511 (bound-2, ~30s)"]
fn cursed_prune_sound_on_700511() {
let ts = ts_of(Rat::<ZZ12>::from_slice_trusted(&[
-1, 0, 1, 1, 1, 4, -1, 1, -1, 2, 1, 4,
]));
let cursed = cursed_junction_types(&ts);
let pruned = heesch_search_cursed(&ts, 0, 2, 20_000_000, &cursed);
let pruneless = heesch_number(ts.clone(), 0, 2, 20_000_000);
assert_eq!(pruneless, Heesch::AtLeast(2), "pruneless: 700511 tiles");
assert_eq!(
pruned,
Heesch::AtLeast(2),
"faithful prune must NOT reject the tiler 700511"
);
}
#[test]
#[ignore = "exploratory: second corona (heavier)"]
fn tilers_reach_second_corona() {
for (name, rat) in [
(
"triangle",
Rat::from_snake_trusted(&tiles::triangle::<ZZ12>()),
),
(
"hexagon",
Rat::from_snake_trusted(&tiles::hexagon::<ZZ12>()),
),
] {
let v = heesch_number(ts_of(rat), 0, 2, HEESCH_BUDGET);
eprintln!("{name} bound 2: {v:?}");
assert!(!v.cannot_tile(), "{name} tiles, must not be rejected");
}
}
#[test]
#[ignore = "measurement: serial bound-2 exhaustion wall time on hard n=10 tiles"]
fn bench_heesch_bound2_hard_tiles() {
let tiles: [&[i8]; 5] = [
&[-2, 1, 4, -1, 4, -1, 2, -1, 5, 1],
&[-3, 2, 4, -2, 5, -2, 3, -2, 5, 2],
&[-1, 0, -1, 4, 1, 2, 1, 0, 1, 5],
&[-3, 2, -3, 4, 3, 0, 3, -2, 3, 5],
&[-2, 1, -2, 4, 2, 1, 2, -1, 2, 5],
];
for rep in 0..3 {
let t0 = std::time::Instant::now();
for seq in tiles {
let t = std::time::Instant::now();
let h = heesch_number(
ts_of(Rat::<ZZ12>::from_slice_trusted(seq)),
0,
2,
20_000_000,
);
eprintln!(" rep {rep} {seq:?}: {h:?} in {:?}", t.elapsed());
}
eprintln!("rep {rep} TOTAL: {:?}", t0.elapsed());
}
}
#[cfg(feature = "cli")]
#[test]
#[ignore = "screening run + dump: parallel Heesch over ZZ12 free rats"]
fn screen_zz12_heesch_dump() {
use crate::enumerate::enumerate_dispatch;
use crate::util::parallel::parallel_drain;
use std::io::Write;
use std::sync::Mutex;
use std::time::Instant;
let envn = |k: &str, d: usize| {
std::env::var(k)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(d)
};
let n_max = envn("HEESCH_N", 10);
let bound = envn("HEESCH_BOUND", 3);
let budget = envn("HEESCH_BUDGET", 1_000_000);
let seqs = enumerate_dispatch::<ZZ12>(n_max, 1, 1, true, false, false).0;
let n = seqs.len();
let workers = crate::util::available_workers();
let out = std::env::temp_dir().join(format!("heesch_zz12_n{n_max}_b{bound}.tsv"));
eprintln!(
"Heesch bound {bound} budget {budget} on {n} ZZ12 rats (n<={n_max}), {workers} workers -> {}",
out.display()
);
let file = Mutex::new(std::io::BufWriter::new(
std::fs::File::create(&out).unwrap(),
));
let t = Instant::now();
let s = parallel_drain(
n,
workers,
|| [0usize; 5],
|tally, i| {
let ts = ts_of(Rat::<ZZ12>::from_slice_trusted(&seqs[i]));
let code = match heesch_number(ts, 0, bound, budget) {
Heesch::Finite(0) => {
tally[0] += 1;
"F0".to_string()
}
Heesch::Finite(1) => {
tally[1] += 1;
"F1".to_string()
}
Heesch::Finite(k) => {
tally[2] += 1;
format!("F{k}")
}
Heesch::AtLeast(b) => {
tally[3] += 1;
format!("A{b}")
}
Heesch::Unknown(k) => {
tally[4] += 1;
format!("U{k}")
}
};
let mut f = file.lock().unwrap();
let _ = writeln!(f, "{code}\t{:?}", seqs[i]);
let _ = f.flush();
},
|mut a, b| {
for j in 0..5 {
a[j] += b[j];
}
a
},
);
eprintln!(
"done in {:?}: F0={} F1={} F(>=2,high-Heesch)={} A(>={bound})={} Unknown={} dump={}",
t.elapsed(),
s[0],
s[1],
s[2],
s[3],
s[4],
out.display()
);
}
}