summaryrefslogtreecommitdiff
path: root/src/eval_tune/dna_trend.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/eval_tune/dna_trend.py')
-rwxr-xr-xsrc/eval_tune/dna_trend.py154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/eval_tune/dna_trend.py b/src/eval_tune/dna_trend.py
new file mode 100755
index 0000000..15ecca8
--- /dev/null
+++ b/src/eval_tune/dna_trend.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+"""
+Track per-parameter direction across a sequence of .dna files (e.g.
+one KEPT candidate.dna per cycle.sh cycle, in chronological order) to
+tell apart two very different things that both show up as "the DNA
+changed again":
+
+ - a parameter trending: consecutive deltas keep the same sign, i.e.
+ the tuner keeps pushing it the same direction cycle over cycle --
+ this looks like real signal.
+ - a parameter flip-flopping: consecutive deltas alternate sign --
+ the tuner is chasing sampling noise in the position batch, not
+ converging on anything.
+
+For each raw DNA cell we compute:
+ net = final_value - first_value
+ churn = sum(|delta| for each consecutive step)
+ consistency = net / churn (in [-1, 1]; 0 churn -> consistency 1 if
+ net is also 0, else undefined/skipped)
+
+consistency near +-1 means every step moved the same direction (pure
+trend); consistency near 0 with nonzero churn means it moved a lot but
+ended up roughly where it started (pure flip-flop).
+
+Usage:
+ python3 dna_trend.py cycle1.dna cycle2.dna cycle3.dna ...
+ python3 dna_trend.py --cycles-dir eval_tune/cycles # auto-discover,
+ chronological by directory timestamp, only cycles with a KEPT
+ candidate.dna per their summary.txt
+
+Prints, per named array, a one-line summary, then the individual cells
+with the strongest trend and the worst flip-flop for a closer look.
+"""
+import argparse
+import sys
+from pathlib import Path
+
+from dna_diff import DNA_NAMES, read_dna_file
+
+
+def load_sequence(paths):
+ rows_by_file = [read_dna_file(p) for p in paths]
+ n_cells_per_file = [sum(len(row) for row in rows) for rows in rows_by_file]
+ if len(set(n_cells_per_file)) != 1:
+ sys.exit(f"cell-count mismatch across files: {dict(zip(paths, n_cells_per_file))} "
+ f"-- these .dna files don't all match the same eval.c revision.")
+ # Flatten each file to one list of (name, index_within_array, value)
+ flat_sequences = []
+ for rows in rows_by_file:
+ flat = []
+ for name, row in zip(DNA_NAMES, rows):
+ for i, v in enumerate(row):
+ flat.append((name, i, v))
+ flat_sequences.append(flat)
+ return flat_sequences
+
+
+def discover_kept_cycles(cycles_dir):
+ paths = []
+ for d in sorted(Path(cycles_dir).iterdir()):
+ summary = d / "summary.txt"
+ candidate = d / "candidate.dna"
+ if summary.exists() and candidate.exists():
+ text = summary.read_text()
+ if "RESULT: KEPT" in text:
+ paths.append(candidate)
+ return paths
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("dna_files", nargs="*")
+ ap.add_argument("--cycles-dir", default=None,
+ help="auto-discover KEPT candidate.dna files under this "
+ "cycle.sh cycles/ directory, chronologically")
+ ap.add_argument("--top", type=int, default=15,
+ help="how many strongest-trend / worst-flip-flop cells to list")
+ args = ap.parse_args()
+
+ if args.cycles_dir:
+ paths = discover_kept_cycles(args.cycles_dir)
+ else:
+ paths = [Path(p) for p in args.dna_files]
+
+ if len(paths) < 2:
+ sys.exit("need at least 2 .dna files (in chronological order) to "
+ "compute a trend -- got "
+ f"{len(paths)}: {[str(p) for p in paths]}")
+
+ print(f"Sequence ({len(paths)} points, chronological):")
+ for p in paths:
+ print(f" {p}")
+ print()
+
+ sequences = load_sequence(paths)
+ n_cells = len(sequences[0])
+
+ results = [] # (name, index, net, churn, consistency, first, last)
+ for cell_idx in range(n_cells):
+ name, arr_idx, _ = sequences[0][cell_idx]
+ values = [seq[cell_idx][2] for seq in sequences]
+ deltas = [b - a for a, b in zip(values, values[1:])]
+ net = values[-1] - values[0]
+ churn = sum(abs(d) for d in deltas)
+ if churn == 0:
+ continue # never moved -- not interesting either way
+ consistency = net / churn
+ results.append((name, arr_idx, net, churn, consistency, values[0], values[-1]))
+
+ if not results:
+ print("No cell changed at all across this sequence.")
+ return
+
+ # Per-array rollup: mean |consistency| weighted by churn, plus counts.
+ by_array = {}
+ for name, arr_idx, net, churn, consistency, first, last in results:
+ d = by_array.setdefault(name, {"churn": 0, "weighted": 0.0, "n": 0,
+ "trending": 0, "flipping": 0})
+ d["churn"] += churn
+ d["weighted"] += abs(consistency) * churn
+ d["n"] += 1
+ if abs(consistency) >= 0.6:
+ d["trending"] += 1
+ elif abs(consistency) <= 0.25:
+ d["flipping"] += 1
+
+ print(f"{'ARRAY':45s} {'cells':>6s} {'trend':>6s} {'flip':>5s} {'churn-wtd consistency':>22s}")
+ for name in DNA_NAMES:
+ if name not in by_array:
+ continue
+ d = by_array[name]
+ wavg = d["weighted"] / d["churn"] if d["churn"] else 0.0
+ print(f"{name:45s} {d['n']:6d} {d['trending']:6d} {d['flipping']:5d} {wavg:22.2f}")
+
+ results.sort(key=lambda r: -abs(r[4]) * r[3]) # weight by churn too
+ trending = [r for r in results if r[4] >= 0.6][:args.top]
+ flipping = sorted([r for r in results if abs(r[4]) <= 0.25],
+ key=lambda r: -r[3])[:args.top]
+
+ print(f"\nTop {len(trending)} most consistently-trending cells "
+ f"(same direction every step):")
+ for name, idx, net, churn, cons, first, last in trending:
+ print(f" {name}[{idx}]: {first} -> {last} "
+ f"(net {net:+d}, churn {churn}, consistency {cons:+.2f})")
+
+ print(f"\nTop {len(flipping)} worst flip-flopping cells "
+ f"(moved a lot, net ~0 -- likely noise):")
+ for name, idx, net, churn, cons, first, last in flipping:
+ print(f" {name}[{idx}]: {first} -> {last} "
+ f"(net {net:+d}, churn {churn}, consistency {cons:+.2f})")
+
+
+if __name__ == "__main__":
+ main()