summaryrefslogtreecommitdiff
path: root/annotate_pgns/aggregate_marginsafety.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-23 13:19:17 -0700
committerScott Gasch <[email protected]>2026-10-23 13:19:17 -0700
commit69525eb2f42ea9cca8255f6cc3b85e6b1312fd05 (patch)
treed78420b6c45d584a3f09b4dcbaf4f1edb36e8d5d /annotate_pgns/aggregate_marginsafety.py
parent6b883e83935f5e8110788565cfcb77cd6fbf8950 (diff)
NNUE search integration: FH% tables, NODE_TYPE refinement, TREEDUMP rewrite
- data.c/chess.h: widen the joint FH% table's late axis 16->17, dedicating the new bucket 0 to a whole-node ("will something here fail high at all") sample rather than sharing it with the first-leftover-move curve. Add an NNUE-eval counterpart table (g_uJointFHThousandths_NNUE) and an NNUE counterpart of the beta-side table, selected at runtime based on whether NNUE eval is active. - search.c/searchsup.c: add _RefineNodeTypeGuess, which uses the FH% tables (via g_NodeTypeGuessTable) to refine a node's NODE_TYPE guess instead of the old heuristic; thread uParentMoveFHEstimate through PLY_INFO so a node can see its parent's prediction for the move that led to it. - generate.c: initialize sKillers[2]/[3].uBonus unconditionally (an uninitialized-stack read caught by valgrind under LTO; harmless today but a real bug). - dumptree.c/chess.h/GNUmakefile: replace the long-dead DUMP_TREE XML tree dump (written for a GUI this project no longer has) with TREEDUMP, a flat, path-keyed move/result trace meant to diff against an identically-instrumented Stockfish build -- see docs/TREEDUMP_FORMAT.md. - docs/DEEPER_PRUNING.md: notes on closing the search-efficiency gap vs Stockfish. - crafty_egtb/stockfish_xboard/typhoon_nnue: wrapper scripts so match_play.py's Engine class can drive Crafty, Stockfish (via PolyGlot), and an NNUE-enabled typhoon the same way it drives HCE typhoon. - tests/lop: rename lop.txt -> lop.ep_; update compare_lop.cpp. - annotate_pgns: add aggregate_marginsafety.py, dedup_pgn.py, fit_lazy_probe.py; update annotate.py/run_nnue_pipeline.py for the NNUE training pipeline. - .gitignore: ignore compile_commands.json (clangd build DB) and eval.nnue (a symlink into the untracked head_reference/ comparison baseline), neither of which belongs in the repo. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'annotate_pgns/aggregate_marginsafety.py')
-rw-r--r--annotate_pgns/aggregate_marginsafety.py123
1 files changed, 123 insertions, 0 deletions
diff --git a/annotate_pgns/aggregate_marginsafety.py b/annotate_pgns/aggregate_marginsafety.py
new file mode 100644
index 0000000..32d5808
--- /dev/null
+++ b/annotate_pgns/aggregate_marginsafety.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+"""Sums the raw swing histograms eval.c's PersistMarginSafetyHistogramsToFile
+writes (one file per shard of a sharded CALIBRATE_MARGIN_SAFETY run)
+bucket-by-bucket and bin-by-bin, then computes EXACT combined mean/
+percentiles from the summed bins -- percentiles don't combine validly
+shard-by-shard (each shard's own in-process percentile is over a
+different, non-representative sub-sample), only the raw counts do.
+
+File format (one line per tier x material-bucket):
+ <tier> <bucket> <count> <exceeded> <sum> <sumSq> <max> <bin0> ... <bin299>
+SWING_HIST_BIN_WIDTH=10, SWING_HIST_BINS=300 (covers [0, 3000) cp) --
+read from each file's own header comment rather than hardcoded, in
+case a future build changes them.
+
+Usage:
+ python3 aggregate_marginsafety.py /path/to/calib_out/hist_*.txt
+"""
+import argparse
+import glob
+import sys
+from collections import defaultdict
+
+
+def parse_header(path):
+ with open(path) as f:
+ for line in f:
+ if line.startswith("# SWING_HIST_BINS="):
+ parts = dict(kv.split("=") for kv in line[2:].split())
+ return int(parts["SWING_HIST_BINS"]), int(parts["SWING_HIST_BIN_WIDTH"])
+ sys.exit(f"{path}: no SWING_HIST_BINS header found")
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("files", nargs="+", help="hist_*.txt files (glob already expanded by shell, "
+ "or pass a glob pattern quoted)")
+ args = ap.parse_args()
+
+ files = []
+ for pat in args.files:
+ matched = glob.glob(pat)
+ files.extend(matched if matched else [pat])
+ if not files:
+ sys.exit("no input files")
+
+ n_bins, bin_width = parse_header(files[0])
+
+ # (tier, bucket) -> dict(count, exceeded, sum, sumSq, max, bins=[n_bins])
+ agg = defaultdict(lambda: {"count": 0, "exceeded": 0, "sum": 0, "sumSq": 0,
+ "max": 0, "bins": [0] * n_bins})
+
+ for path in files:
+ with open(path) as f:
+ for line in f:
+ if line.startswith("#") or not line.strip():
+ continue
+ parts = line.split()
+ tier, bucket = parts[0], int(parts[1])
+ count, exceeded, s, sumsq, mx = (int(x) for x in parts[2:7])
+ bins = [int(x) for x in parts[7:7 + n_bins]]
+ a = agg[(tier, bucket)]
+ a["count"] += count
+ a["exceeded"] += exceeded
+ a["sum"] += s
+ a["sumSq"] += sumsq
+ a["max"] = max(a["max"], mx)
+ for i, v in enumerate(bins):
+ a["bins"][i] += v
+
+ def percentile(bins, count, p):
+ """Exact percentile from binned counts -- rank = p/100 * count,
+ walk cumulative counts to find which bin holds that rank, report
+ the bin's lower edge (a slight underestimate, bin_width=10cp
+ granularity -- good enough for choosing a margin, not claimed
+ exact to the cp)."""
+ if count == 0:
+ return None
+ rank = (p / 100.0) * count
+ cum = 0
+ for i, c in enumerate(bins):
+ cum += c
+ if cum >= rank:
+ return i * bin_width
+ return (len(bins) - 1) * bin_width # fell off the end -- all mass in last bin
+
+ tiers = sorted(set(t for t, b in agg.keys()))
+ print(f"Aggregated {len(files)} shard files, {n_bins} bins x {bin_width}cp "
+ f"(covers [0, {n_bins * bin_width}) cp)\n")
+
+ results = {}
+ for tier in tiers:
+ print(f"=== {tier} ===")
+ print(f"{'bucket':>6} {'n':>10} {'mean':>8} {'p50':>6} {'p98':>6} "
+ f"{'p99.9':>7} {'max':>7} {'exceeded':>9}")
+ for bucket in range(8):
+ a = agg.get((tier, bucket))
+ if a is None or a["count"] == 0:
+ print(f"{bucket:>6} (no data)")
+ continue
+ mean = a["sum"] / a["count"]
+ p50 = percentile(a["bins"], a["count"], 50)
+ p98 = percentile(a["bins"], a["count"], 98)
+ p999 = percentile(a["bins"], a["count"], 99.9)
+ results[(tier, bucket)] = {"p98": p98, "p999": p999, "mean": mean,
+ "max": a["max"], "n": a["count"]}
+ print(f"{bucket:>6} {a['count']:>10} {mean:>8.1f} {p50:>6} {p98:>6} "
+ f"{p999:>7} {a['max']:>7} {a['exceeded']:>9}")
+ print()
+
+ if "nnue_lazy" in tiers:
+ p98s = [results.get(("nnue_lazy", b), {}).get("p98", "?") for b in range(8)]
+ print(f"Suggested iNnueLazyMarginByMatBucket (p98 per bucket, matching "
+ f"the existing table's own documented choice of statistic):")
+ print(f" {{{', '.join(str(x) for x in p98s)}}}")
+ if "nnue_superlazy" in tiers:
+ p98s = [results.get(("nnue_superlazy", b), {}).get("p98", "?") for b in range(8)]
+ print(f"\nFor comparison, NNUE super-lazy (material-only) p98 per bucket:")
+ print(f" {{{', '.join(str(x) for x in p98s)}}}")
+
+
+if __name__ == "__main__":
+ main()