summaryrefslogtreecommitdiff
path: root/src/split_ep.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-20 09:26:44 -0700
committerScott Gasch <[email protected]>2026-10-20 09:26:44 -0700
commitc8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (patch)
treeb6c2137c9ac577fa83b1e948fa0311d53dfe445d /src/split_ep.py
parent3b5d3677bb14ae14728a1c4b221176bb83667d92 (diff)
Wire NNUE eval into search, build the training pipeline, and add a fast
move/eval-agreement benchmark against Stockfish (tests/lop) Implements the architecture decisions in src/docs/NNUE.md: HalfKAv2_hm feature set, int16/int8 quantization, per-ply accumulator backup for king moves, and a lazy-eval-ready integration into search.c/eval.c. Adds the full annotate_pgns/ training pipeline (extraction, K-fit/ labeling, quiet filtering, tensor building, training, export) plus an end-to-end orchestrator (run_nnue_pipeline.py) and validation tooling (sanity_check_net.py, compare_vs_stockfish_net.py). Adds tests/lop: a 10k-position list-of-positions benchmark comparing engine eval rank and move choice against pre-computed Stockfish/lc0 references, much cheaper than a full self-play match for day-to-day progress checks. First sd10-matched self-play result against classic HCE: LLR +2.28 (H1=+2.94), -72=119+169 over 360 games, confirming the net is "smarter per node" at matched depth. A real-time (--st 4) match is currently weaker (candidate loses ~3 plies of depth to a ~3.3-3.7x nps gap against an unrecalibrated search), consistent with NNUE.md's adoption-criterion section -- pruning/reduction constants still need retuning for the new eval's score scale before a fair real-time comparison is possible. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/split_ep.py')
-rwxr-xr-xsrc/split_ep.py69
1 files changed, 69 insertions, 0 deletions
diff --git a/src/split_ep.py b/src/split_ep.py
new file mode 100755
index 0000000..557dbec
--- /dev/null
+++ b/src/split_ep.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""Split a script-format .ep_ file (setboard/solution/id/go blocks, as
+used by typhoon's `script` command) into two disjoint, shuffled halves
+for a train/holdout threshold sweep.
+
+Usage: split_ep.py <input.ep_> <out_a.ep_> <out_b.ep_> [--seed N]
+
+Splits by whole position blocks (never mid-position), shuffled with a
+fixed seed so the split is reproducible but not biased by the file's
+original ordering (ECM.ep_ is not randomly ordered).
+"""
+import argparse
+import random
+import sys
+
+
+def read_blocks(path):
+ """Each block is the lines from one 'setboard' up to and including
+ the following 'go' line."""
+ blocks = []
+ current = []
+ with open(path) as f:
+ for line in f:
+ stripped = line.rstrip("\n")
+ if stripped.startswith("setboard") and current:
+ blocks.append(current)
+ current = []
+ current.append(stripped)
+ if current:
+ blocks.append(current)
+ return blocks
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__)
+ ap.add_argument("input")
+ ap.add_argument("out_a")
+ ap.add_argument("out_b")
+ ap.add_argument("--seed", type=int, default=0)
+ args = ap.parse_args()
+
+ blocks = read_blocks(args.input)
+ if not blocks:
+ print(f"ERROR: no position blocks found in {args.input}", file=sys.stderr)
+ sys.exit(1)
+
+ rng = random.Random(args.seed)
+ order = list(range(len(blocks)))
+ rng.shuffle(order)
+
+ mid = len(order) // 2
+ half_a = sorted(order[:mid])
+ half_b = sorted(order[mid:])
+
+ with open(args.out_a, "w") as f:
+ for i in half_a:
+ f.write("\n".join(blocks[i]) + "\n")
+
+ with open(args.out_b, "w") as f:
+ for i in half_b:
+ f.write("\n".join(blocks[i]) + "\n")
+
+ print(f"{len(blocks)} positions in {args.input} -> "
+ f"{len(half_a)} in {args.out_a}, {len(half_b)} in {args.out_b} "
+ f"(seed={args.seed})")
+
+
+if __name__ == "__main__":
+ main()