diff --git a/docs/research/precision_gate_2026_09_14.json b/docs/research/precision_gate_2026_09_14.json new file mode 100644 index 00000000..8481c1bc --- /dev/null +++ b/docs/research/precision_gate_2026_09_14.json @@ -0,0 +1,2170 @@ +{ + "generated_at": "2026-09-14", + "ayanamsa": "raman", + "node_mode": "mean", + "holdout": "references/real_case_calibration/minute_rectification_holdout_v4.json", + "case_count": 20, + "radii": [ + 10, + 30, + 60 + ], + "minute_step": 2, + "precision_counts": { + "day": 59, + "month": 0, + "year": 84, + "other": 0 + }, + "headline": "**\u65e0\u6536\u76ca\uff0c\u4e0d\u7acb\u5b9e\u73b0\u5355\u3002** \u7ebf\u4e0a `MIN_BOUNDARY_DAYS=45` / \u5237\u65b0 30 \u4e0e\u7b49\u6743\u5206\u76d8\u4fdd\u6301\u4e0d\u52a8\u3002\u771f\u673a\u5361\u4f4f\u7684 \u00b110 \u7a97\u4e0a\uff0c\u653e\u5bbd\u95f8\u95e8\u80fd\u628a\u5bbd\u5ea6\u4ece 15 \u6536\u5230 11\uff0c\u4f46\u5934\u540d\u4ece 0.80 \u6389\u5230 0.75\uff1b\u5bbd\u7a97\u4e0a\u5bbd\u5ea6\u53cd\u800c\u53d8\u5927\u3002V1/V2 \u4e0e\u57fa\u7ebf\u76f8\u540c\uff0cV3\uff08D60\uff09\u5728 \u00b110 \u628a\u5934\u540d\u964d\u5230 0.70\u3002\u552f\u4e00\u770b\u8d77\u6765\u50cf\u8fc7\u95e8\u7684\u683c\u5b50\u662f G1+V3 \u5728 \u00b110\uff08\u547d\u4e2d\u4ecd 0.80\u3001\u5bbd\u5ea6 15\u219211\uff09\uff0c\u5bbd\u7a97\u4e0a\u4e0d\u6210\u7acb\uff0c\u4e5f\u6ca1\u6709\u5bf9\u8be5\u7ec4\u5408\u505a \u00b17 \u8bb0\u9519\uff0c\u4e0d\u591f\u7acb\u5b9e\u73b0\u5355\u3002", + "decision": "no_benefit", + "m0": { + "10": { + "A": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0346, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.5, + "alive": 20 + }, + "B": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.561, + "entropy6": 2.0341, + "probes_mean": 5.1, + "refresh_mean": 3.35, + "discriminative_mean": 3.35, + "unique_mean": 2.65, + "alive": 20 + }, + "C": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5607, + "entropy6": 2.0286, + "probes_mean": 5.1, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.55, + "alive": 20 + } + }, + "30": { + "A": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + }, + "B": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.041, + "entropy6": 3.164, + "probes_mean": 7.1, + "refresh_mean": 5.2, + "discriminative_mean": 5.2, + "unique_mean": 3.85, + "alive": 20 + }, + "C": { + "n": 20, + "top1": 0.6, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0422, + "entropy6": 3.1585, + "probes_mean": 7.1, + "refresh_mean": 5.2, + "discriminative_mean": 5.2, + "unique_mean": 3.8, + "alive": 20 + } + }, + "60": { + "A": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "B": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9966, + "entropy6": 3.9526, + "probes_mean": 7.2, + "refresh_mean": 6.05, + "discriminative_mean": 5.95, + "unique_mean": 4.5, + "alive": 20 + }, + "C": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 61.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9986, + "entropy6": 3.9471, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 5.95, + "unique_mean": 4.3, + "alive": 20 + } + } + }, + "m1": { + "10": { + "G0": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0346, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.5, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.8932, + "probes_mean": 5.9, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.45, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7948, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7948, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7948, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + } + }, + "30": { + "G0": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.185, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.9, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1564, + "probes_mean": 7.05, + "refresh_mean": 5.6, + "discriminative_mean": 5.6, + "unique_mean": 3.95, + "alive": 20 + } + }, + "60": { + "G0": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.35, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9614, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 6.1, + "unique_mean": 4.4, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9787, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.4, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9775, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.4, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9819, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.45, + "alive": 20 + } + } + }, + "m1b": { + "10": { + "V0": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0346, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.5, + "alive": 20 + }, + "V1": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0346, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.5, + "alive": 20 + }, + "V2": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0346, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.5, + "alive": 20 + }, + "V3": { + "n": 20, + "top1": 0.7, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5605, + "entropy6": 2.0344, + "probes_mean": 5.1, + "refresh_mean": 3.15, + "discriminative_mean": 3.15, + "unique_mean": 2.45, + "alive": 20 + } + }, + "30": { + "V0": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + }, + "V1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + }, + "V2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + }, + "V3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 33.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1646, + "probes_mean": 7.1, + "refresh_mean": 5.45, + "discriminative_mean": 5.45, + "unique_mean": 3.9, + "alive": 20 + } + }, + "60": { + "V0": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "V1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "V2": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "V3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9537, + "probes_mean": 7.2, + "refresh_mean": 5.75, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + } + } + }, + "combos": { + "10": { + "G1+V1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.8931, + "probes_mean": 5.9, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.45, + "alive": 20 + }, + "G2+V1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G3+V1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G4+V1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G1+V2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.8931, + "probes_mean": 5.9, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.45, + "alive": 20 + }, + "G2+V2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G3+V2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G4+V2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7947, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G1+V3": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5605, + "entropy6": 1.8936, + "probes_mean": 5.9, + "refresh_mean": 3.2, + "discriminative_mean": 3.2, + "unique_mean": 2.5, + "alive": 20 + }, + "G2+V3": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5605, + "entropy6": 1.7951, + "probes_mean": 5.9, + "refresh_mean": 2.85, + "discriminative_mean": 2.85, + "unique_mean": 2.15, + "alive": 20 + }, + "G3+V3": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5605, + "entropy6": 1.7951, + "probes_mean": 5.9, + "refresh_mean": 2.85, + "discriminative_mean": 2.85, + "unique_mean": 2.15, + "alive": 20 + }, + "G4+V3": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5605, + "entropy6": 1.7951, + "probes_mean": 5.9, + "refresh_mean": 2.85, + "discriminative_mean": 2.85, + "unique_mean": 2.15, + "alive": 20 + } + }, + "30": { + "G1+V1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.185, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.9, + "alive": 20 + }, + "G2+V1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G3+V1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G4+V1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1564, + "probes_mean": 7.05, + "refresh_mean": 5.6, + "discriminative_mean": 5.6, + "unique_mean": 3.95, + "alive": 20 + }, + "G1+V2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.185, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.9, + "alive": 20 + }, + "G2+V2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G3+V2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G4+V2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1564, + "probes_mean": 7.05, + "refresh_mean": 5.6, + "discriminative_mean": 5.6, + "unique_mean": 3.95, + "alive": 20 + }, + "G1+V3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.185, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.9, + "alive": 20 + }, + "G2+V3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G3+V3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G4+V3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0386, + "entropy6": 3.1564, + "probes_mean": 7.05, + "refresh_mean": 5.6, + "discriminative_mean": 5.6, + "unique_mean": 3.95, + "alive": 20 + } + }, + "60": { + "G1+V1": { + "n": 20, + "top1": 0.35, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9614, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 6.1, + "unique_mean": 4.4, + "alive": 20 + }, + "G2+V1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9787, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.4, + "alive": 20 + }, + "G3+V1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9775, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.4, + "alive": 20 + }, + "G4+V1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9819, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.45, + "alive": 20 + }, + "G1+V2": { + "n": 20, + "top1": 0.35, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9614, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 6.1, + "unique_mean": 4.4, + "alive": 20 + }, + "G2+V2": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9787, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.4, + "alive": 20 + }, + "G3+V2": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9775, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.4, + "alive": 20 + }, + "G4+V2": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9819, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.45, + "alive": 20 + }, + "G1+V3": { + "n": 20, + "top1": 0.35, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9614, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 6.1, + "unique_mean": 4.4, + "alive": 20 + }, + "G2+V3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9787, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.4, + "alive": 20 + }, + "G3+V3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9775, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.4, + "alive": 20 + }, + "G4+V3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9819, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.25, + "unique_mean": 4.45, + "alive": 20 + } + } + }, + "m2": { + "3": { + "10": { + "G0": { + "n": 20, + "top1": 0.8, + "coverage": 1.0, + "width_median": 14.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5596, + "entropy6": 2.0347, + "probes_mean": 5.1, + "refresh_mean": 3.25, + "discriminative_mean": 3.25, + "unique_mean": 2.55, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5596, + "entropy6": 1.893, + "probes_mean": 5.9, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.55, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5596, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.25, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5596, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.25, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5596, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.25, + "alive": 20 + } + }, + "30": { + "G0": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 34.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0387, + "entropy6": 3.1645, + "probes_mean": 7.1, + "refresh_mean": 5.55, + "discriminative_mean": 5.55, + "unique_mean": 3.9, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0387, + "entropy6": 3.1854, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.9, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0387, + "entropy6": 3.1509, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0387, + "entropy6": 3.1509, + "probes_mean": 7.05, + "refresh_mean": 5.5, + "discriminative_mean": 5.5, + "unique_mean": 3.95, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.55, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0387, + "entropy6": 3.1568, + "probes_mean": 7.05, + "refresh_mean": 5.6, + "discriminative_mean": 5.6, + "unique_mean": 3.95, + "alive": 20 + } + }, + "60": { + "G0": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9538, + "probes_mean": 7.2, + "refresh_mean": 5.85, + "discriminative_mean": 5.4, + "unique_mean": 4.0, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 55.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9618, + "probes_mean": 7.2, + "refresh_mean": 6.15, + "discriminative_mean": 6.0, + "unique_mean": 4.35, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.979, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.05, + "unique_mean": 4.3, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9778, + "probes_mean": 7.2, + "refresh_mean": 6.15, + "discriminative_mean": 6.0, + "unique_mean": 4.3, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 56.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9942, + "entropy6": 3.9822, + "probes_mean": 7.2, + "refresh_mean": 6.25, + "discriminative_mean": 6.1, + "unique_mean": 4.35, + "alive": 20 + } + } + }, + "7": { + "10": { + "G0": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 14.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0343, + "probes_mean": 5.1, + "refresh_mean": 3.2, + "discriminative_mean": 3.2, + "unique_mean": 2.5, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.8928, + "probes_mean": 5.9, + "refresh_mean": 3.3, + "discriminative_mean": 3.3, + "unique_mean": 2.5, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.7945, + "probes_mean": 5.9, + "refresh_mean": 2.95, + "discriminative_mean": 2.95, + "unique_mean": 2.2, + "alive": 20 + } + }, + "30": { + "G0": { + "n": 20, + "top1": 0.6, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.1651, + "probes_mean": 7.1, + "refresh_mean": 5.55, + "discriminative_mean": 5.55, + "unique_mean": 3.9, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.185, + "probes_mean": 7.05, + "refresh_mean": 5.55, + "discriminative_mean": 5.55, + "unique_mean": 3.9, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.65, + "discriminative_mean": 5.65, + "unique_mean": 3.95, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.1505, + "probes_mean": 7.05, + "refresh_mean": 5.65, + "discriminative_mean": 5.65, + "unique_mean": 3.95, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.1565, + "probes_mean": 7.05, + "refresh_mean": 5.7, + "discriminative_mean": 5.7, + "unique_mean": 4.0, + "alive": 20 + } + }, + "60": { + "G0": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 83.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9535, + "probes_mean": 7.2, + "refresh_mean": 5.9, + "discriminative_mean": 5.45, + "unique_mean": 4.2, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.35, + "coverage": 1.0, + "width_median": 61.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9616, + "probes_mean": 7.2, + "refresh_mean": 6.05, + "discriminative_mean": 6.05, + "unique_mean": 4.3, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9787, + "probes_mean": 7.2, + "refresh_mean": 6.05, + "discriminative_mean": 6.05, + "unique_mean": 4.25, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 59.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9774, + "probes_mean": 7.2, + "refresh_mean": 6.0, + "discriminative_mean": 6.0, + "unique_mean": 4.25, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 59.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9818, + "probes_mean": 7.2, + "refresh_mean": 6.0, + "discriminative_mean": 6.0, + "unique_mean": 4.25, + "alive": 20 + } + } + }, + "14": { + "10": { + "G0": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 15.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 2.0353, + "probes_mean": 5.1, + "refresh_mean": 3.2, + "discriminative_mean": 3.2, + "unique_mean": 2.6, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.8933, + "probes_mean": 5.9, + "refresh_mean": 3.1, + "discriminative_mean": 3.1, + "unique_mean": 2.4, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.795, + "probes_mean": 5.9, + "refresh_mean": 2.75, + "discriminative_mean": 2.75, + "unique_mean": 2.0, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.795, + "probes_mean": 5.9, + "refresh_mean": 2.75, + "discriminative_mean": 2.75, + "unique_mean": 2.0, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.75, + "coverage": 1.0, + "width_median": 11.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5595, + "entropy6": 1.795, + "probes_mean": 5.9, + "refresh_mean": 2.75, + "discriminative_mean": 2.75, + "unique_mean": 2.0, + "alive": 20 + } + }, + "30": { + "G0": { + "n": 20, + "top1": 0.65, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0384, + "entropy6": 3.1653, + "probes_mean": 7.1, + "refresh_mean": 5.25, + "discriminative_mean": 5.25, + "unique_mean": 3.9, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0384, + "entropy6": 3.1849, + "probes_mean": 7.05, + "refresh_mean": 5.4, + "discriminative_mean": 5.4, + "unique_mean": 3.95, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0384, + "entropy6": 3.1507, + "probes_mean": 7.05, + "refresh_mean": 5.75, + "discriminative_mean": 5.75, + "unique_mean": 4.05, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0384, + "entropy6": 3.1507, + "probes_mean": 7.05, + "refresh_mean": 5.75, + "discriminative_mean": 5.75, + "unique_mean": 4.05, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 37.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0384, + "entropy6": 3.1567, + "probes_mean": 7.05, + "refresh_mean": 5.85, + "discriminative_mean": 5.85, + "unique_mean": 4.05, + "alive": 20 + } + }, + "60": { + "G0": { + "n": 20, + "top1": 0.45, + "coverage": 1.0, + "width_median": 58.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.994, + "entropy6": 3.9543, + "probes_mean": 7.2, + "refresh_mean": 5.95, + "discriminative_mean": 5.95, + "unique_mean": 4.45, + "alive": 20 + }, + "G1": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 76.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.994, + "entropy6": 3.9624, + "probes_mean": 7.2, + "refresh_mean": 6.1, + "discriminative_mean": 6.1, + "unique_mean": 4.65, + "alive": 20 + }, + "G2": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.994, + "entropy6": 3.9793, + "probes_mean": 7.2, + "refresh_mean": 6.3, + "discriminative_mean": 6.3, + "unique_mean": 4.6, + "alive": 20 + }, + "G3": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.994, + "entropy6": 3.9781, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.55, + "alive": 20 + }, + "G4": { + "n": 20, + "top1": 0.5, + "coverage": 1.0, + "width_median": 64.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.994, + "entropy6": 3.9825, + "probes_mean": 7.2, + "refresh_mean": 6.2, + "discriminative_mean": 6.2, + "unique_mean": 4.55, + "alive": 20 + } + } + } + }, + "v3_jitter7": { + "10": { + "n": 20, + "top1": 0.7, + "coverage": 1.0, + "width_median": 14.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 2.5602, + "entropy6": 2.0338, + "probes_mean": 5.1, + "refresh_mean": 3.15, + "discriminative_mean": 3.15, + "unique_mean": 2.55, + "alive": 20 + }, + "30": { + "n": 20, + "top1": 0.6, + "coverage": 1.0, + "width_median": 35.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.0385, + "entropy6": 3.1651, + "probes_mean": 7.1, + "refresh_mean": 5.55, + "discriminative_mean": 5.55, + "unique_mean": 3.9, + "alive": 20 + }, + "60": { + "n": 20, + "top1": 0.4, + "coverage": 1.0, + "width_median": 83.0, + "tie": 0.0, + "squeezed": 0, + "entropy0": 4.9929, + "entropy6": 3.9535, + "probes_mean": 7.2, + "refresh_mean": 5.9, + "discriminative_mean": 5.45, + "unique_mean": 4.2, + "alive": 20 + } + }, + "m3": { + "rows": [ + [ + "\u00b110", + "G0", + 3.25, + 3.25, + 15.0 + ], + [ + "\u00b110", + "G1", + 3.3, + 3.3, + 11.0 + ], + [ + "\u00b110", + "G2", + 2.95, + 2.95, + 11.0 + ], + [ + "\u00b110", + "G3", + 2.95, + 2.95, + 11.0 + ], + [ + "\u00b110", + "G4", + 2.95, + 2.95, + 11.0 + ], + [ + "\u00b130", + "G0", + 5.45, + 5.45, + 33.0 + ], + [ + "\u00b130", + "G1", + 5.4, + 5.4, + 35.0 + ], + [ + "\u00b130", + "G2", + 5.5, + 5.5, + 37.0 + ], + [ + "\u00b130", + "G3", + 5.5, + 5.5, + 37.0 + ], + [ + "\u00b130", + "G4", + 5.6, + 5.6, + 37.0 + ], + [ + "\u00b160", + "G0", + 5.75, + 5.4, + 56.0 + ], + [ + "\u00b160", + "G1", + 6.1, + 6.1, + 58.0 + ], + [ + "\u00b160", + "G2", + 6.25, + 6.25, + 64.0 + ], + [ + "\u00b160", + "G3", + 6.2, + 6.2, + 64.0 + ], + [ + "\u00b160", + "G4", + 6.25, + 6.25, + 64.0 + ] + ] + }, + "varga_contribution": {}, + "verdicts": { + "G1": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit squeezed7=0\uff1b\u00b130:no_benefit squeezed7=0\uff1b\u00b160:no_benefit squeezed7=0" + }, + "G2": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit squeezed7=0\uff1b\u00b130:no_benefit squeezed7=0\uff1b\u00b160:no_benefit squeezed7=0" + }, + "G3": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit squeezed7=0\uff1b\u00b130:no_benefit squeezed7=0\uff1b\u00b160:no_benefit squeezed7=0" + }, + "G4": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit squeezed7=0\uff1b\u00b130:no_benefit squeezed7=0\uff1b\u00b160:no_benefit squeezed7=0" + }, + "V1": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit\uff1b\u00b130:no_benefit\uff1b\u00b160:no_benefit" + }, + "V2": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit\uff1b\u00b130:no_benefit\uff1b\u00b160:no_benefit" + }, + "V3": { + "verdict": "no_benefit", + "note": "\u00b110:no_benefit\uff1b\u00b130:no_benefit\uff1b\u00b160:no_benefit" + }, + "G1+V1@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G2+V1@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G3+V1@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G4+V1@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G1+V2@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G2+V2@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G3+V2@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G4+V2@\u00b110": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G1+V3@\u00b110": { + "verdict": "benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G2+V3@\u00b110": { + "verdict": "benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G3+V3@\u00b110": { + "verdict": "benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G4+V3@\u00b110": { + "verdict": "benefit", + "note": "combo vs G0 V0 at \u00b110" + }, + "G1+V1@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G2+V1@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G3+V1@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G4+V1@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G1+V2@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G2+V2@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G3+V2@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G4+V2@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G1+V3@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G2+V3@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G3+V3@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G4+V3@\u00b130": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b130" + }, + "G1+V1@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G2+V1@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G3+V1@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G4+V1@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G1+V2@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G2+V2@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G3+V2@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G4+V2@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G1+V3@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G2+V3@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G3+V3@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + }, + "G4+V3@\u00b160": { + "verdict": "no_benefit", + "note": "combo vs G0 V0 at \u00b160" + } + }, + "errors": [], + "production_gate": { + "MIN_BOUNDARY_DAYS": 45, + "REFRESH_MIN_BOUNDARY_DAYS": 30 + } +} diff --git a/docs/research/precision_gate_2026_09_14.md b/docs/research/precision_gate_2026_09_14.md new file mode 100644 index 00000000..78f1c1f4 --- /dev/null +++ b/docs/research/precision_gate_2026_09_14.md @@ -0,0 +1,277 @@ +# 出题闸门按证据精度分档测量(2026-09-14) + +- 口径:ayanamsa `raman`,node mode `mean`。不得与上游 true-node 数字直接对比。 +- 数据:`references/real_case_calibration/minute_rectification_holdout_v4.json`,20 例公开 Rodden-AA。精度 日 59 / 月 0 / 年 84。 +- 半径:±10, ±30, ±60,步长 2 分钟。 +- 性质:离线测量。生产 `event_probes.py` 的 `MIN_BOUNDARY_DAYS=45` / `REFRESH_MIN_BOUNDARY_DAYS=30` 未改;计分引擎等权分盘未改。 +- 线上对照:`unionStillValidRange` = 未淘汰且落后头名不足 8 分的簇覆盖并集。 + +## 结论 + +**无收益,不立实现单。** 线上 `MIN_BOUNDARY_DAYS=45` / 刷新 30 与等权分盘保持不动。真机卡住的 ±10 窗上,放宽闸门能把宽度从 15 收到 11,但头名从 0.80 掉到 0.75;宽窗上宽度反而变大。V1/V2 与基线相同,V3(D60)在 ±10 把头名降到 0.70。唯一看起来像过门的格子是 G1+V3 在 ±10(命中仍 0.80、宽度 15→11),宽窗上不成立,也没有对该组合做 ±7 记错,不够立实现单。 + +判定口径:命中不降 **且** 宽度下降才算有收益;只多出题而宽度变宽不算。±7 天记错挤出真值的档位一律不推荐。本网格覆盖率始终 20/20,M2 挤出全是 0。 + +| 方案 | 判定 | 说明 | +| --- | ---: | ---: | +| G1 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 | +| G2 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 | +| G3 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 | +| G4 | no_benefit | ±10:no_benefit squeezed7=0;±30:no_benefit squeezed7=0;±60:no_benefit squeezed7=0 | +| V1 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit | +| V2 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit | +| V3 | no_benefit | ±10:no_benefit;±30:no_benefit;±60:no_benefit | + +过运与三大外部引擎未参与本单(纯出题闸门 / 分盘配权,不解释运势)。 + +## M0 · 精度处理三组(生产闸门) + +同一批例子、生产 45/30 闸。A 原样,B 日→月,C 全部→年。用来隔离精度本身,而不是闸门。 + +### ±10 + +| 组 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| A | 0.8 | 1 | 15 | 0 | 0 | 5.1 | 3.25 | +| B | 0.8 | 1 | 15 | 0 | 0 | 5.1 | 3.35 | +| C | 0.75 | 1 | 15 | 0 | 0 | 5.1 | 3.3 | + +### ±30 + +| 组 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| A | 0.55 | 1 | 33 | 0 | 0 | 7.1 | 5.45 | +| B | 0.55 | 1 | 35 | 0 | 0 | 7.1 | 5.2 | +| C | 0.6 | 1 | 35 | 0 | 0 | 7.1 | 5.2 | + +### ±60 + +| 组 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| A | 0.4 | 1 | 56 | 0 | 0 | 7.2 | 5.75 | +| B | 0.4 | 1 | 58 | 0 | 0 | 7.2 | 6.05 | +| C | 0.45 | 1 | 61 | 0 | 0 | 7.2 | 6.1 | + +对 BUG-689 邀请文案:组 A 与把日精度降成月的组 B 在 ±10 上命中/宽度完全一样(0.80 / 15)。全部降成年精度的组 C 命中只掉到 0.75。v4 没有原生月精度事件,**不能**把「记得到天比记得到月有用得多」写成已证实,更不能承诺能定到分钟。 + +## M1 · 闸门分档(组 A) + +### ±10 + +| 档 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | 有分辨力 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| G0 | 0.8 | 1 | 15 | 0 | 0 | 5.1 | 3.25 | 3.25 | +| G1 | 0.75 | 1 | 11 | 0 | 0 | 5.9 | 3.3 | 3.3 | +| G2 | 0.75 | 1 | 11 | 0 | 0 | 5.9 | 2.95 | 2.95 | +| G3 | 0.75 | 1 | 11 | 0 | 0 | 5.9 | 2.95 | 2.95 | +| G4 | 0.75 | 1 | 11 | 0 | 0 | 5.9 | 2.95 | 2.95 | + +### ±30 + +| 档 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | 有分辨力 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| G0 | 0.55 | 1 | 33 | 0 | 0 | 7.1 | 5.45 | 5.45 | +| G1 | 0.55 | 1 | 35 | 0 | 0 | 7.05 | 5.4 | 5.4 | +| G2 | 0.55 | 1 | 37 | 0 | 0 | 7.05 | 5.5 | 5.5 | +| G3 | 0.55 | 1 | 37 | 0 | 0 | 7.05 | 5.5 | 5.5 | +| G4 | 0.55 | 1 | 37 | 0 | 0 | 7.05 | 5.6 | 5.6 | + +### ±60 + +| 档 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | 首轮出题均 | 六题后再出 | 有分辨力 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| G0 | 0.4 | 1 | 56 | 0 | 0 | 7.2 | 5.75 | 5.4 | +| G1 | 0.35 | 1 | 58 | 0 | 0 | 7.2 | 6.1 | 6.1 | +| G2 | 0.4 | 1 | 64 | 0 | 0 | 7.2 | 6.25 | 6.25 | +| G3 | 0.4 | 1 | 64 | 0 | 0 | 7.2 | 6.2 | 6.2 | +| G4 | 0.4 | 1 | 64 | 0 | 0 | 7.2 | 6.25 | 6.25 | + +## M1b · 分盘按分钟敏感度配权 + +V0 生产等权;V1 `min(window/varga_minutes, 1)` 再除以 `2n`;V2 只计窗内至少变一次的盘;V3 = V2 + D60(仅 ±10)。 +未出现在上游表里的 D2/D3/D5/D7/D11 用 `120/n` 分钟。按盘贡献表本轮是空的:V0 采集误走了生产计分、没有打上分盘标签;但 V1/V2 三档半径都与 V0 数字相同,V3 只在 ±10 把命中从 0.80 降到 0.70,已经够判断这三档不能上线。 + +### ±10 + +| 改法 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| V0 | 0.8 | 1 | 15 | 0 | 0 | +| V1 | 0.8 | 1 | 15 | 0 | 0 | +| V2 | 0.8 | 1 | 15 | 0 | 0 | +| V3 | 0.7 | 1 | 15 | 0 | 0 | + +### ±30 + +| 改法 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| V0 | 0.55 | 1 | 33 | 0 | 0 | +| V1 | 0.55 | 1 | 33 | 0 | 0 | +| V2 | 0.55 | 1 | 33 | 0 | 0 | +| V3 | 0.55 | 1 | 33 | 0 | 0 | + +### ±60 + +| 改法 | 头名命中 | 区间覆盖 | 宽度中位 | 并列率 | 挤出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| V0 | 0.4 | 1 | 56 | 0 | 0 | +| V1 | 0.4 | 1 | 56 | 0 | 0 | +| V2 | 0.4 | 1 | 56 | 0 | 0 | +| V3 | 0.4 | 1 | 56 | 0 | 0 | + +### 闸门 × 分盘组合(组 A) + +±10 + +| 组合 | 头名命中 | 区间覆盖 | 宽度中位 | 挤出 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| G1+V1 | 0.75 | 1 | 11 | 0 | 3.3 | +| G2+V1 | 0.75 | 1 | 11 | 0 | 2.95 | +| G3+V1 | 0.75 | 1 | 11 | 0 | 2.95 | +| G4+V1 | 0.75 | 1 | 11 | 0 | 2.95 | +| G1+V2 | 0.75 | 1 | 11 | 0 | 3.3 | +| G2+V2 | 0.75 | 1 | 11 | 0 | 2.95 | +| G3+V2 | 0.75 | 1 | 11 | 0 | 2.95 | +| G4+V2 | 0.75 | 1 | 11 | 0 | 2.95 | +| G1+V3 | 0.8 | 1 | 11 | 0 | 3.2 | +| G2+V3 | 0.8 | 1 | 11 | 0 | 2.85 | +| G3+V3 | 0.8 | 1 | 11 | 0 | 2.85 | +| G4+V3 | 0.8 | 1 | 11 | 0 | 2.85 | + +±30 + +| 组合 | 头名命中 | 区间覆盖 | 宽度中位 | 挤出 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| G1+V1 | 0.55 | 1 | 35 | 0 | 5.4 | +| G2+V1 | 0.55 | 1 | 37 | 0 | 5.5 | +| G3+V1 | 0.55 | 1 | 37 | 0 | 5.5 | +| G4+V1 | 0.55 | 1 | 37 | 0 | 5.6 | +| G1+V2 | 0.55 | 1 | 35 | 0 | 5.4 | +| G2+V2 | 0.55 | 1 | 37 | 0 | 5.5 | +| G3+V2 | 0.55 | 1 | 37 | 0 | 5.5 | +| G4+V2 | 0.55 | 1 | 37 | 0 | 5.6 | +| G1+V3 | 0.55 | 1 | 35 | 0 | 5.4 | +| G2+V3 | 0.55 | 1 | 37 | 0 | 5.5 | +| G3+V3 | 0.55 | 1 | 37 | 0 | 5.5 | +| G4+V3 | 0.55 | 1 | 37 | 0 | 5.6 | + +±60 + +| 组合 | 头名命中 | 区间覆盖 | 宽度中位 | 挤出 | 六题后再出 | +| --- | ---: | ---: | ---: | ---: | ---: | +| G1+V1 | 0.35 | 1 | 58 | 0 | 6.1 | +| G2+V1 | 0.4 | 1 | 64 | 0 | 6.25 | +| G3+V1 | 0.4 | 1 | 64 | 0 | 6.2 | +| G4+V1 | 0.4 | 1 | 64 | 0 | 6.25 | +| G1+V2 | 0.35 | 1 | 58 | 0 | 6.1 | +| G2+V2 | 0.4 | 1 | 64 | 0 | 6.25 | +| G3+V2 | 0.4 | 1 | 64 | 0 | 6.2 | +| G4+V2 | 0.4 | 1 | 64 | 0 | 6.25 | +| G1+V3 | 0.35 | 1 | 58 | 0 | 6.1 | +| G2+V3 | 0.4 | 1 | 64 | 0 | 6.25 | +| G3+V3 | 0.4 | 1 | 64 | 0 | 6.2 | +| G4+V3 | 0.4 | 1 | 64 | 0 | 6.25 | + +## M2 · 答错容忍度(日精度事件日期偏移) + +组 A 的日精度事件按例、按事件独立随机偏移(种子 20260914,排除 0)。±7 天出现真值挤出的档位不得推荐。 + +### ±3 天 + +| 格子 | 头名命中 | 区间覆盖 | 挤出 | 宽度中位 | +| --- | ---: | ---: | ---: | ---: | +| ±10 G0 | 0.8 | 1 | 0 | 14 | +| ±10 G1 | 0.75 | 1 | 0 | 11 | +| ±10 G2 | 0.75 | 1 | 0 | 11 | +| ±10 G3 | 0.75 | 1 | 0 | 11 | +| ±10 G4 | 0.75 | 1 | 0 | 11 | +| ±30 G0 | 0.55 | 1 | 0 | 34 | +| ±30 G1 | 0.55 | 1 | 0 | 35 | +| ±30 G2 | 0.55 | 1 | 0 | 35 | +| ±30 G3 | 0.55 | 1 | 0 | 35 | +| ±30 G4 | 0.55 | 1 | 0 | 35 | +| ±60 G0 | 0.4 | 1 | 0 | 56 | +| ±60 G1 | 0.4 | 1 | 0 | 55 | +| ±60 G2 | 0.45 | 1 | 0 | 56 | +| ±60 G3 | 0.45 | 1 | 0 | 56 | +| ±60 G4 | 0.45 | 1 | 0 | 56 | + +### ±7 天 + +| 格子 | 头名命中 | 区间覆盖 | 挤出 | 宽度中位 | +| --- | ---: | ---: | ---: | ---: | +| ±10 G0 | 0.75 | 1 | 0 | 14 | +| ±10 G1 | 0.75 | 1 | 0 | 11 | +| ±10 G2 | 0.75 | 1 | 0 | 11 | +| ±10 G3 | 0.75 | 1 | 0 | 11 | +| ±10 G4 | 0.75 | 1 | 0 | 11 | +| ±30 G0 | 0.6 | 1 | 0 | 35 | +| ±30 G1 | 0.5 | 1 | 0 | 35 | +| ±30 G2 | 0.5 | 1 | 0 | 35 | +| ±30 G3 | 0.5 | 1 | 0 | 35 | +| ±30 G4 | 0.5 | 1 | 0 | 35 | +| ±60 G0 | 0.4 | 1 | 0 | 83 | +| ±60 G1 | 0.35 | 1 | 0 | 61 | +| ±60 G2 | 0.45 | 1 | 0 | 64 | +| ±60 G3 | 0.4 | 1 | 0 | 59 | +| ±60 G4 | 0.4 | 1 | 0 | 59 | + +### ±14 天 + +| 格子 | 头名命中 | 区间覆盖 | 挤出 | 宽度中位 | +| --- | ---: | ---: | ---: | ---: | +| ±10 G0 | 0.75 | 1 | 0 | 15 | +| ±10 G1 | 0.75 | 1 | 0 | 11 | +| ±10 G2 | 0.75 | 1 | 0 | 11 | +| ±10 G3 | 0.75 | 1 | 0 | 11 | +| ±10 G4 | 0.75 | 1 | 0 | 11 | +| ±30 G0 | 0.65 | 1 | 0 | 35 | +| ±30 G1 | 0.5 | 1 | 0 | 37 | +| ±30 G2 | 0.5 | 1 | 0 | 37 | +| ±30 G3 | 0.5 | 1 | 0 | 37 | +| ±30 G4 | 0.5 | 1 | 0 | 37 | +| ±60 G0 | 0.45 | 1 | 0 | 58 | +| ±60 G1 | 0.4 | 1 | 0 | 76 | +| ±60 G2 | 0.5 | 1 | 0 | 64 | +| ±60 G3 | 0.5 | 1 | 0 | 64 | +| ±60 G4 | 0.5 | 1 | 0 | 64 | + +V3 在 ±7 天偏移下: + +| 半径 | 头名命中 | 区间覆盖 | 挤出 | +| --- | ---: | ---: | ---: | +| ±10 | 0.7 | 1 | 0 | +| ±30 | 0.6 | 1 | 0 | +| ±60 | 0.4 | 1 | 0 | + +## M3 · 六题之后还能再出几道 + +数字见 M1 表的「六题后再出」与「有分辨力」。若新增 ≥8 道而宽度只收 1–2 分钟,判定里记为问答成本高于收益。 + +| 半径 | 档 | 再出均 | 有分辨力均 | 宽度中位 | +| --- | ---: | ---: | ---: | ---: | +| ±10 | G0 | 3.25 | 3.25 | 15 | +| ±10 | G1 | 3.3 | 3.3 | 11 | +| ±10 | G2 | 2.95 | 2.95 | 11 | +| ±10 | G3 | 2.95 | 2.95 | 11 | +| ±10 | G4 | 2.95 | 2.95 | 11 | +| ±30 | G0 | 5.45 | 5.45 | 33 | +| ±30 | G1 | 5.4 | 5.4 | 35 | +| ±30 | G2 | 5.5 | 5.5 | 37 | +| ±30 | G3 | 5.5 | 5.5 | 37 | +| ±30 | G4 | 5.6 | 5.6 | 37 | +| ±60 | G0 | 5.75 | 5.4 | 56 | +| ±60 | G1 | 6.1 | 6.1 | 58 | +| ±60 | G2 | 6.25 | 6.25 | 64 | +| ±60 | G3 | 6.2 | 6.2 | 64 | +| ±60 | G4 | 6.25 | 6.25 | 64 | + +## 方法 + +1. 每例先算一次静态盘(与事件无关),再按精度处理 / 偏移 / 分盘政策套 `build_event_contribution_matrix`。 +2. 闸门只通过研究脚本临时改 `MIN_BOUNDARY_DAYS` / `REFRESH_MIN_BOUNDARY_DAYS`,函数返回后立刻恢复。 +3. 参与该题的证据精度取该例事件包里最细的一档(有日用日,否则月,否则年)。 +4. 六题回放用真值簇最优 yes/no(上界,不是真人会怎么答)。交付口径与簇宽度研究相同:lead 8 未淘汰并集。 +5. 刷新出题走生产 `refresh_probes=true`(含已落地的 R3/R4),只换边界天数闸。 + +错误 0 例。20 例跑完。 + diff --git a/docs/tasks/PROGRESS-rectification-precision-gate-research-20260914.md b/docs/tasks/PROGRESS-rectification-precision-gate-research-20260914.md new file mode 100644 index 00000000..bc92f2ab --- /dev/null +++ b/docs/tasks/PROGRESS-rectification-precision-gate-research-20260914.md @@ -0,0 +1,49 @@ +# 进度 · 出题闸门按证据精度分档研究(2026-09-14) + +## 范围 + +- 分支:`codex/rectification-precision-gate-research-20260914`(基于当时 `origin/staging`;任务书含 M1b 的 `b56147e5`) +- 任务单:`docs/tasks/TASK-rectification-precision-adaptive-boundary-research-20260914.md` +- 结果:`docs/research/precision_gate_2026_09_14.md` + 同名 `.json` +- 脚本:`scripts/research/precision_gate_lib.py`、`scripts/research/precision_gate_sweep.py`(离线,不进 API) +- 未改 `scripts/rectification/event_probes.py` 线上默认值(仍是 45 / 30),未改计分引擎等权分盘 +- 本单不占 BUG 号 + +## 完成 + +- 开工读了结论页、`probe_supply_after_six`、`_boundary_windows` / `_union_boundary_dates`,以及更新后的 M1b。 +- 离线测量:M0 精度三组、M1 G0–G4、M1b V1/V2/V3(±10 才加 D60)、M2 ±3/±7/±14、M3 六题后再出题。闸门只在脚本里临时 patch。 +- 全量 20 例 × ±10/±30/±60,错误 0,约 2.5 小时。 +- 判定函数初版把「多出分辨题」也算收益,宽窗宽度其实变大;已改成任务书口径(命中不降 **且** 宽度下降),用 `--from-json` 重写结论,**没有重跑引擎**。 + +## 验收数字 + +口径 ayanamsa=`raman`、node mode=`mean`,步长 2 分钟。覆盖率三档都是 20/20,M2 挤出全是 0。 + +六题后线上口径(组 A,生产闸 G0): + +| 半径 | 头名命中 | 宽度中位 | 六题后再出 | +| --- | ---: | ---: | ---: | +| ±10 | 0.80 | 15 | 3.25 | +| ±30 | 0.55 | 33 | 5.45 | +| ±60 | 0.40 | 56 | 5.75 | + +放宽闸门(G1–G4)在 ±10:宽度 15→11,头名 0.80→0.75。±30/±60 宽度变宽(33→35–37,56→58–64)。 + +M1b:V1/V2 与 V0 相同;V3 在 ±10 命中 0.80→0.70。 + +M0:日→月(B)与原样(A)在 ±10 相同;全部降成年(C)命中 0.80→0.75。 + +**判定:无收益,不立实现单。** + +- `python -m pytest tests/test_precision_gate_research.py tests/test_rectification_refresh_r3_r4.py`:16 passed +- 按盘贡献表空(V0 采集误走生产计分);不改变 V1=V2=V0、V3 降命中的结论 +- 本机无项目 `.venv`,用 Anaconda Python 3.11.7 + swisseph。`run_quality_gate.py --profile quick` 若因 `mcp` 包缺口退出,与基线环境相同,不计入 + +## 未做 + +- 未改线上闸门或分盘权重 +- 未把研究脚本接到 API +- 未立实现单 +- 未做 PyJHora / VedAstro / jyotishganit 对照(本单不解释运势) +- 未 push diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f3724576..de6a7f5f 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -190,7 +190,7 @@ | `TASK-rectification-birth-time-provenance-20260914.md` | `PROGRESS-rectification-birth-time-provenance-20260914.md` | 与上游流程对照发现的缺口(上游 R2):录入时已问「医院记录 / 家人记得大概 / 只知道时段」并落库 `birth_time_source`,但整条校正链只在放宽窗口时用过一次,决策与文案一律不看它——家人推算的时间和出生证在系统里等同对待,输出还直接称「你的出生时间」。本单把来源标签接进投影并分档措辞(BUG-690) | 待验收 | `codex/rectification-birth-time-provenance-20260914` | -| `TASK-rectification-precision-adaptive-boundary-research-20260914.md` | `PROGRESS-rectification-precision-gate-research-20260914.md` | **研究单**:出题闸门 `MIN_BOUNDARY_DAYS=45`(刷新 30)不看证据精度,而出生时间每差 1 分钟大运边界只平移约 1.1 天——20 分钟窗内的候选边界全挤在三周内被整批丢掉,这就是「六题后出不来题」的算术原因。量闸门按年/月/日分档(G0–G4)的收益,并必测答错容忍度(±3/±7/±14 天偏移下真值是否被挤出) | 待执行 | `codex/rectification-precision-gate-research-20260914` | +| `TASK-rectification-precision-adaptive-boundary-research-20260914.md` | `PROGRESS-rectification-precision-gate-research-20260914.md` | **研究单**:出题闸门 `MIN_BOUNDARY_DAYS=45`(刷新 30)不看证据精度;另加 M1b 按分盘分钟敏感度配权(V1/V2/V3,含 D60)。量 G0–G4 与 V1–V3,并必测 ±3/±7/±14 天记错是否挤出真值 | 待验收 | `codex/rectification-precision-gate-research-20260914` | | `TASK-rectification-open-collect-invite-20260914.md` | `PROGRESS-rectification-open-collect-invite-20260914.md` | **P0**:固定七条采集线问完后只说「能问的都问完了」,用户不知道还能补经历、也不知道补了有用;而两轮研究证明补带年月经历是唯一有效手段。产品拍板:交付卡照出 + 卡上给不限领域的补充邀请(先要确切日期,再退年月;举七条线之外的例子),补完必须可见生效(BUG-689) | 待验收 | `codex/rectification-open-collect-invite-20260914` | diff --git a/scripts/research/precision_gate_lib.py b/scripts/research/precision_gate_lib.py new file mode 100644 index 00000000..86bb1c80 --- /dev/null +++ b/scripts/research/precision_gate_lib.py @@ -0,0 +1,532 @@ +"""Pure helpers for the precision-adaptive probe-gate research sweep. + +Does not change production defaults in event_probes.py or the scoring engine. +""" + +from __future__ import annotations + +from contextlib import contextmanager +from copy import deepcopy +from dataclasses import dataclass, field +from datetime import date, timedelta +from random import Random +from typing import Any, Iterator, Sequence + +from scripts.active_rectification_event_engine import ( + AUXILIARY_DOMAINS, + AUXILIARY_SCORE_FACTOR, + DOMAIN_CONFIG, + OBSERVATION_ONLY_LAYERS, + _active_narayana, + _active_vimshottari, + _ashtakavarga_auxiliary, + _controlled_transit_rules, + _event_datetime, + _house_lords, + _planet_house, + _relative_house, + _shadbala_verified_components_auxiliary, + _varga_chart, + _varga_house, +) +from scripts.active_rectification_events import precision_weight +from scripts.rectification.event_probes import ( + REFRESH_MIN_BOUNDARY_DAYS, + _boundary_windows, +) +import functional_benefics +import varga + +GATES: dict[str, dict[str, int]] = { + "G0": {"year": 45, "month": 45, "day": 45}, + "G1": {"year": 45, "month": 30, "day": 10}, + "G2": {"year": 45, "month": 30, "day": 7}, + "G3": {"year": 45, "month": 21, "day": 5}, + "G4": {"year": 60, "month": 30, "day": 3}, +} + +TREATMENTS = ("A", "B", "C") +JITTER_SPANS = (3, 7, 14) +VARGA_CAP = 1.0 +UPSTREAM_VARGA_MINUTES: dict[str, float] = { + "D1": 120.0, + "D9": 13.3, + "D10": 12.0, + "D12": 10.0, + "D4": 7.5, + "D24": 5.0, + "D30": 4.0, + "D60": 2.0, +} +PRODUCTION_VARGA_PREFIXES = ( + "D2", "D3", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30", +) +JITTER_SEED = 20260914 + + +@dataclass +class VargaPolicy: + name: str + window_minutes: float + changing: frozenset[str] = field(default_factory=frozenset) + use_d60: bool = False + cap: float = VARGA_CAP + hits: dict[str, int] = field(default_factory=dict) + points: dict[str, float] = field(default_factory=dict) + points_by_time: dict[str, dict[str, float]] = field(default_factory=dict) + + @property + def is_baseline(self) -> bool: + return self.name in {"V0", "baseline", ""} + + +def window_minutes_for_radius(radius: int) -> float: + return float(2 * int(radius)) + + +def varga_minutes(prefix: str) -> float: + if prefix in UPSTREAM_VARGA_MINUTES: + return UPSTREAM_VARGA_MINUTES[prefix] + number = int(str(prefix)[1:]) + return 120.0 / max(number, 1) + + +def varga_factor(prefix: str, window_minutes: float, cap: float = VARGA_CAP) -> float: + minutes = varga_minutes(prefix) + if minutes <= 0 or window_minutes <= 0: + return 0.0 + return min(float(window_minutes) / minutes, float(cap)) + + +def finest_precision(events: Sequence[dict[str, Any]]) -> str: + ranks = {"day": 3, "month": 2, "quarter": 2, "year": 1, "range": 1, "unknown": 0} + best = "year" + best_rank = 0 + for event in events: + precision = str(event.get("precision") or "year") + rank = ranks.get(precision, 0) + if rank > best_rank: + best = precision if precision in {"day", "month", "year"} else "year" + best_rank = rank + if best_rank >= 3: + return "day" + if best_rank >= 2: + return "month" + return "year" + + +def participating_precision(events: Sequence[dict[str, Any]], domain: str | None = None) -> str: + pool = [ + event for event in events + if domain is None or str(event.get("domain") or "") == domain + ] + return finest_precision(pool or events) + + +def threshold_for(gate: str, precision: str, *, refresh: bool) -> int: + spec = GATES[gate] + key = precision if precision in spec else "year" + if refresh and gate == "G0": + return REFRESH_MIN_BOUNDARY_DAYS + return int(spec[key]) + + +def count_precision(events: Sequence[dict[str, Any]]) -> dict[str, int]: + tallies = {"day": 0, "month": 0, "year": 0, "other": 0} + for event in events: + precision = str(event.get("precision") or "year") + if precision in tallies: + tallies[precision] += 1 + else: + tallies["other"] += 1 + return tallies + + +def treat_events(events: Sequence[dict[str, Any]], treatment: str) -> list[dict[str, Any]]: + if treatment == "A": + return [dict(event) for event in events] + rows: list[dict[str, Any]] = [] + for event in events: + item = dict(event) + precision = str(item.get("precision") or "year") + if treatment == "B" and precision == "day": + item["precision"] = "month" + elif treatment == "C": + item["precision"] = "year" + rows.append(item) + return rows + + +def jitter_day_events( + events: Sequence[dict[str, Any]], + span: int, + *, + case_id: str, + seed: int = JITTER_SEED, +) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for event in events: + item = dict(event) + if str(item.get("precision") or "") != "day": + rows.append(item) + continue + raw = str(item.get("date") or "") + try: + original = date.fromisoformat(raw[:10]) + except ValueError: + rows.append(item) + continue + rng = Random(f"{seed}:{case_id}:{item.get('id')}:{span}") + offset = 0 + while offset == 0: + offset = rng.randint(-int(span), int(span)) + item["date"] = (original + timedelta(days=offset)).isoformat() + item["jitter_days"] = offset + rows.append(item) + return rows + + +def varga_sign_index(context: dict[str, Any], prefix: str) -> int | None: + charts = context.get("varga_charts") if isinstance(context.get("varga_charts"), dict) else {} + chart = charts.get(prefix) + if not isinstance(chart, dict): + return None + raw = (chart.get("Ascendant") or {}).get("sign_idx") + return int(raw) if isinstance(raw, int) else None + + +def changing_vargas( + contexts: Sequence[dict[str, Any]], + prefixes: Sequence[str] = PRODUCTION_VARGA_PREFIXES, +) -> frozenset[str]: + changed: set[str] = set() + for prefix in prefixes: + previous = None + for context in contexts: + current = varga_sign_index(context, prefix) + if previous is not None and current is not None and current != previous: + changed.add(prefix) + break + previous = current + return frozenset(changed) + + +def ensure_d60(context: dict[str, Any]) -> dict[str, Any] | None: + charts = context.setdefault("varga_charts", {}) + existing = charts.get("D60") + if isinstance(existing, dict): + return existing + planets = context.get("planet_longitudes") or {} + natal = context.get("chart") or {} + ascendant = natal.get("ascendant") if isinstance(natal.get("ascendant"), dict) else {} + lon = ascendant.get("lon") + if not isinstance(lon, (int, float)) or not planets: + return None + computed = varga.calc_all_vargas(planets, float(lon), divisions=[60]) + chart = _varga_chart(computed, "D60") + if isinstance(chart, dict): + charts["D60"] = chart + return chart + + +def attach_d60(contexts: Sequence[dict[str, Any]]) -> None: + for context in contexts: + ensure_d60(context) + + +@contextmanager +def patched_boundary_gate(*, initial: int, refresh: int) -> Iterator[None]: + import scripts.rectification.event_probes as ep + previous = (ep.MIN_BOUNDARY_DAYS, ep.REFRESH_MIN_BOUNDARY_DAYS) + ep.MIN_BOUNDARY_DAYS = int(initial) + ep.REFRESH_MIN_BOUNDARY_DAYS = int(refresh) + try: + yield + finally: + ep.MIN_BOUNDARY_DAYS, ep.REFRESH_MIN_BOUNDARY_DAYS = previous + + +def production_defaults_intact() -> bool: + import scripts.rectification.event_probes as ep + return ep.MIN_BOUNDARY_DAYS == 45 and ep.REFRESH_MIN_BOUNDARY_DAYS == 30 + + +def close_same_year_windows(left: date, right: date, min_days: int) -> list[date]: + return list(_boundary_windows([left], [right], min_days=min_days)) + + +def _record_varga(policy: VargaPolicy, time: str, prefix: str, delta: float) -> None: + policy.hits[prefix] = policy.hits.get(prefix, 0) + 1 + policy.points[prefix] = round(policy.points.get(prefix, 0.0) + delta, 6) + by_time = policy.points_by_time.setdefault(time, {}) + by_time[prefix] = round(by_time.get(prefix, 0.0) + delta, 6) + + +def score_event_with_policy( + *, + candidate_time: str, + event: dict[str, Any], + natal_chart: dict[str, Any], + varga_by_prefix: dict[str, dict[str, Any]], + vimshottari: tuple[str, str, str], + narayana: tuple[int | None, int | None], + arudha_padas: dict[str, Any], + policy: VargaPolicy, +) -> dict[str, Any]: + _, target_houses = DOMAIN_CONFIG[event["domain"]] + ascendant_index = int(natal_chart["ascendant"]["lon"] // 30) + target_lords = _house_lords(ascendant_index, target_houses) + functional = functional_benefics.derive_functional_benefic_malefic( + natal_chart["ascendant"].get("sign") + ) + functional_benefics_set = set(functional.get("functional_benefics") or []) + functional_malefics_set = set(functional.get("functional_malefics") or []) + major_lord, minor_lord, pratyantar_lord = vimshottari + rules: list[str] = [] + points = 0.0 + active = list(varga_by_prefix.items()) + count = max(len(active), 1) + + for lord, weight, label in ( + (major_lord, 2.0, "vim_md"), + (minor_lord, 1.5, "vim_ad"), + (pratyantar_lord, 0.75, "vim_pd"), + ): + if _planet_house(natal_chart, lord) in target_houses: + rules.append(f"{label}_domain_house") + points += weight + if lord in target_lords: + rules.append(f"{label}_domain_lord") + points += weight + if not active: + pass + else: + for prefix, varga_chart in active: + if _varga_house(varga_chart, lord) not in target_houses: + continue + factor = 1.0 if policy.is_baseline else varga_factor( + prefix, policy.window_minutes, policy.cap, + ) + delta = weight * factor / (2 * count) + rules.append(f"{label}_domain_varga") + points += delta + _record_varga(policy, candidate_time, prefix, delta) + if lord in functional_benefics_set: + rules.append(f"{label}_functional_benefic_auxiliary") + points += 0.2 + elif lord in functional_malefics_set: + rules.append(f"{label}_functional_malefic_auxiliary") + points -= 0.1 + + for sign_index, weight, label in ( + (narayana[0], 2.0, "narayana_md"), + (narayana[1], 1.0, "narayana_ad"), + ): + if sign_index is not None and _relative_house(sign_index, ascendant_index) in target_houses: + rules.append(f"{label}_domain_house") + points += weight + arudha_keys = ( + ("A7", "UL") if event["domain"] == "relationship" + else ("A10",) if event["domain"] in {"career", "occupation"} + else () + ) + arudha_signs = { + value.get("sign_idx") for key in arudha_keys + if isinstance((value := arudha_padas.get(key)), dict) and isinstance(value.get("sign_idx"), int) + } + if arudha_signs: + for lord, label in ((major_lord, "vim_md"), (minor_lord, "vim_ad"), (pratyantar_lord, "vim_pd")): + planet = natal_chart.get("planets", {}).get(lord) or {} + if isinstance(planet.get("lon"), (int, float)) and int(planet["lon"] // 30) in arudha_signs: + rules.append(f"{label}_arudha_auxiliary") + points += 0.35 + + event_kind = event.get("event_kind", event["domain"]) + if event["domain"] in AUXILIARY_DOMAINS: + points *= AUXILIARY_SCORE_FACTOR + rules.append( + "occupation_auxiliary_not_primary" + if event["domain"] == "occupation" + else "appearance_auxiliary_not_primary" + ) + if not rules: + rules.append("no_domain_activation") + rules.append(f"event_kind:{event_kind}") + weighted_points = round(points * precision_weight(event["precision"]), 4) + return { + "event_id": event["id"], + "domain": event["domain"], + "candidate_time": candidate_time, + "rule_ids": rules, + "points": weighted_points, + } + + +def domain_varga_prefixes(domain: str, policy: VargaPolicy) -> tuple[str, ...]: + prefixes = list(DOMAIN_CONFIG[domain][0]) + if policy.use_d60 and "D60" not in prefixes: + prefixes.append("D60") + if policy.name in {"V2", "V3"}: + prefixes = [item for item in prefixes if item in policy.changing] + return tuple(prefixes) + + +def candidate_row_with_policy( + request: dict[str, Any], + context: dict[str, Any], + policy: VargaPolicy, +) -> dict[str, Any]: + candidate_at = context["candidate_at"] + chart = context["chart"] + planet_longitudes = context["planet_longitudes"] + ascendant_index = context["ascendant_index"] + arudha_padas = context["arudha_padas"] + varga_charts = context["varga_charts"] + moon_longitude = planet_longitudes["Moon"] + evidence: list[dict[str, Any]] = [] + missing_layers: list[str] = [] + stamp = candidate_at.strftime("%H:%M") + + for event in request["events"]: + event_at = _event_datetime(event) + prefixes = domain_varga_prefixes(event["domain"], policy) + selected = {prefix: varga_charts.get(prefix) for prefix in prefixes} + if prefixes and any(chart is None for chart in selected.values()): + missing_layers.extend(prefixes) + continue + try: + vimshottari = _active_vimshottari(candidate_at.date().isoformat(), moon_longitude, event_at) + except (KeyError, TypeError, ValueError): + missing_layers.append("Vimshottari_MD_AD_PD") + continue + try: + narayana = _active_narayana( + ascendant_index, planet_longitudes, candidate_at, event_at, + ) + except (KeyError, TypeError, ValueError): + missing_layers.append("Narayana_MD_AD") + continue + if narayana[0] is None or narayana[1] is None: + missing_layers.append("Narayana_MD_AD") + continue + usable = {prefix: chart for prefix, chart in selected.items() if isinstance(chart, dict)} + evidence.append(score_event_with_policy( + candidate_time=stamp, + event=event, + natal_chart=chart, + varga_by_prefix=usable, + vimshottari=vimshottari, + narayana=narayana, + arudha_padas=arudha_padas, + policy=policy, + )) + transit_rules = _controlled_transit_rules( + request, event, ascendant_index, DOMAIN_CONFIG[event["domain"]][1], + ) + if transit_rules: + evidence[-1]["rule_ids"].extend(transit_rules) + evidence[-1]["points"] = round( + evidence[-1]["points"] + 0.25 * len(transit_rules) * precision_weight(event["precision"]), + 4, + ) + av_rules, av_points = _ashtakavarga_auxiliary( + chart, ascendant_index, DOMAIN_CONFIG[event["domain"]][1], + ) + if av_rules: + evidence[-1]["rule_ids"].extend(av_rules) + evidence[-1]["points"] = round( + evidence[-1]["points"] + av_points * precision_weight(event["precision"]), 4, + ) + shadbala_rules, shadbala_points = _shadbala_verified_components_auxiliary( + chart, candidate_at.hour + candidate_at.minute / 60, vimshottari, + ) + if shadbala_rules: + evidence[-1]["rule_ids"].extend(shadbala_rules) + evidence[-1]["points"] = round( + evidence[-1]["points"] + shadbala_points * precision_weight(event["precision"]), 4, + ) + + return { + "time": stamp, + "score": round(sum(item["points"] for item in evidence), 4), + "evidence": evidence, + "missing_layers": sorted(set( + missing_layers + + [layer for layer in context["feature"]["blocked_layers"] if layer not in OBSERVATION_ONLY_LAYERS] + )), + } + + +def make_row_provider(static_contexts: Sequence[dict[str, Any]], policy: VargaPolicy): + def provider(request: dict[str, Any]) -> list[dict[str, Any]]: + return [candidate_row_with_policy(request, context, policy) for context in static_contexts] + return provider + + +def ablate_top1( + scores: dict[str, float], + varga_points_by_time: dict[str, dict[str, float]], + true_time: str, +) -> dict[str, Any]: + if not scores: + return {"baseline_top1": None, "flips": {}} + baseline = max(scores, key=lambda time: (scores[time], time)) + flips: dict[str, int] = {} + prefixes = sorted({prefix for row in varga_points_by_time.values() for prefix in row}) + for prefix in prefixes: + adjusted = { + time: scores[time] - float((varga_points_by_time.get(time) or {}).get(prefix) or 0.0) + for time in scores + } + leader = max(adjusted, key=lambda time: (adjusted[time], time)) + flips[prefix] = int(leader != baseline) + return { + "baseline_top1": baseline, + "true_is_top1": baseline == true_time[:5], + "flips": flips, + } + + +def coverage_ok(candidate: dict[str, Any], baseline: dict[str, Any]) -> bool: + if candidate.get("coverage") is None or baseline.get("coverage") is None: + return False + return float(candidate["coverage"]) + 1e-9 >= float(baseline["coverage"]) + + +def squeezed_ok(candidate: dict[str, Any], baseline: dict[str, Any]) -> bool: + return int(candidate.get("squeezed") or 0) <= int(baseline.get("squeezed") or 0) + + +def gate_verdict( + baseline: dict[str, Any], + candidate: dict[str, Any], + *, + jitter7_squeezed: int | None = None, +) -> str: + if not baseline.get("n") or not candidate.get("n"): + return "uncertain" + if jitter7_squeezed is not None and int(jitter7_squeezed) > 0: + return "no_benefit" + if not coverage_ok(candidate, baseline) or not squeezed_ok(candidate, baseline): + return "no_benefit" + hit_same_or_up = float(candidate["top1"]) + 1e-9 >= float(baseline["top1"]) + width_down = ( + candidate.get("width_median") is not None + and baseline.get("width_median") is not None + and float(candidate["width_median"]) < float(baseline["width_median"]) - 1e-9 + ) + extra = float(candidate.get("refresh_mean") or 0) - float(baseline.get("refresh_mean") or 0) + width_gain = 0.0 + if candidate.get("width_median") is not None and baseline.get("width_median") is not None: + width_gain = float(baseline["width_median"]) - float(candidate["width_median"]) + if not hit_same_or_up: + return "no_benefit" + if width_down: + if extra >= 8 and width_gain <= 2: + return "no_benefit" + return "benefit" + return "no_benefit" + + +def clone_events(events: Sequence[dict[str, Any]]) -> list[dict[str, Any]]: + return [deepcopy(dict(event)) for event in events] diff --git a/scripts/research/precision_gate_sweep.py b/scripts/research/precision_gate_sweep.py new file mode 100644 index 00000000..2b927a8e --- /dev/null +++ b/scripts/research/precision_gate_sweep.py @@ -0,0 +1,967 @@ +#!/usr/bin/env python3 +"""Offline sweep: precision-adaptive probe gates and varga sensitivity weights. + +Does not change production defaults. Writes docs/research/precision_gate_2026_09_14.md. +""" + +from __future__ import annotations + +import argparse +import json +import statistics +import sys +import traceback +from datetime import date +from pathlib import Path +from typing import Any, Sequence + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from scripts.active_rectification_event_engine import ( # noqa: E402 + AYANAMSA, + NODE_MODE, + compute_candidate_static_contexts, +) +from scripts.rectification.event_probes import discriminating_event_probes # noqa: E402 +import scripts.rectification.event_probes as event_probes # noqa: E402 +from scripts.rectification.refinement_packet import window_scan # noqa: E402 +from scripts.rectification.scoring_service import ( # noqa: E402 + build_event_contribution_matrix, + score_from_matrix, + scoreable_request, +) +from scripts.research.cluster_width_lib import ( # noqa: E402 + SEPARATION_LEAD, + delivery_from_public, + merge_adjacent_traced, + metrics_bundle, + public_from_clusters, + raw_signature_clusters, + still_valid_public, +) +from scripts.research.cluster_width_probe import replay_public # noqa: E402 +from scripts.research.minute_resolution_sweep import MINUTE_STEP, scoring_request_for # noqa: E402 +from scripts.research.precision_gate_lib import ( # noqa: E402 + GATES, + JITTER_SPANS, + PRODUCTION_VARGA_PREFIXES, + TREATMENTS, + VargaPolicy, + ablate_top1, + attach_d60, + changing_vargas, + count_precision, + finest_precision, + gate_verdict, + jitter_day_events, + make_row_provider, + patched_boundary_gate, + threshold_for, + treat_events, + window_minutes_for_radius, +) +from scripts.research.probe_supply_after_six import ( # noqa: E402 + ASK_COUNT, + asked_key, + remaining_after_six, + separates_true, +) + +HOLDOUT = ROOT / "references" / "real_case_calibration" / "minute_rectification_holdout_v4.json" +REPORT_MD = ROOT / "docs" / "research" / "precision_gate_2026_09_14.md" +REPORT_JSON = ROOT / "docs" / "research" / "precision_gate_2026_09_14.json" +TODAY = date(2026, 9, 14) +RADII = (10, 30, 60) +ASK = ASK_COUNT +VARGA_NAMES = ("V0", "V1", "V2", "V3") + + +def holdout_precision_counts(cases: Sequence[dict[str, Any]]) -> dict[str, int]: + events = [event for case in cases for event in case.get("events") or []] + return count_precision(events) + + +def score_bundle( + case: dict[str, Any], + radius: int, + events: list[dict[str, Any]], + static_contexts: Sequence[dict[str, Any]], + policy: VargaPolicy | None, +) -> dict[str, Any]: + clone = {**case, "events": events, "candidate_radius_minutes": radius} + request = scoring_request_for(clone, radius) + if policy is None: + built = build_event_contribution_matrix(request, static_contexts=static_contexts) + else: + built = build_event_contribution_matrix( + request, + row_provider=make_row_provider(static_contexts, policy), + static_contexts=static_contexts, + ) + rows = score_from_matrix(request, built) + return {"request": scoreable_request(request), "built": built, "rows": rows} + + +def generate_probes( + request: dict[str, Any], + built: dict[str, Any], + times: Sequence[str], + true_time: str, + *, + gate: str, + precision: str, + refresh: bool, + asked_keys: Sequence[str] = (), +) -> list[dict[str, Any]]: + payload = { + **request, + "refresh_probes": refresh, + "asked_probe_keys": list(asked_keys), + } + initial = threshold_for(gate, precision, refresh=False) + refresh_days = threshold_for(gate, precision, refresh=True) + with patched_boundary_gate(initial=initial, refresh=refresh_days): + return discriminating_event_probes( + payload, + built, + scan=window_scan(built), + candidate_times=list(times), + representative_time=true_time, + today=TODAY, + ) + + +def evaluate_delivery( + *, + rows: Sequence[dict[str, Any]], + contexts: Sequence[dict[str, Any]], + probes: Sequence[dict[str, Any]], + true_time: str, +) -> dict[str, Any]: + window_times = [str(row["time"])[:5] for row in rows] + raw = raw_signature_clusters(contexts) + by_time = {str(row["time"])[:5]: row for row in rows if str(row.get("time"))} + merged, _trace = merge_adjacent_traced(raw, by_time) + public = public_from_clusters(merged, rows) + prior = {str(row["time"])[:5]: float(row.get("score") or 0) for row in public} + replay = replay_public(probes=probes, public=public, prior=prior, true_time=true_time) + posterior = [] + eliminated = set(replay["eliminated"]) + for row in public: + stamp = str(row["time"])[:5] + posterior.append({**row, "score": replay["scores"].get(stamp, row["score"])}) + valid = still_valid_public(posterior, replay["scores"], eliminated, lead=SEPARATION_LEAD) + delivery = delivery_from_public(valid) + metrics = metrics_bundle( + public=[row for row in posterior if str(row["time"])[:5] not in eliminated], + true_time=true_time, + window_times=window_times, + delivery_times=delivery["times"], + delivery_width=delivery["width"], + independent=True, + entropy_scores=[ + replay["scores"].get(str(row["time"])[:5], 0.0) + for row in posterior + if str(row["time"])[:5] not in eliminated + ], + ) + reps = [str(row["time"])[:5] for row in public] + remaining, remaining_mode, true_alive = remaining_after_six( + all_times=reps, + scores=replay["scores"], + eliminated=eliminated, + clusters=merged, + true_time=true_time, + ) + asked = list(probes)[:ASK] + return { + "metrics": metrics, + "asked": len(asked), + "probes": len(probes), + "entropy0": (replay["entropy"] or [None])[0], + "entropy6": (replay["entropy"] or [None])[-1], + "eliminated": len(eliminated), + "true_alive": replay["true_alive"], + "remaining": remaining, + "remaining_mode": remaining_mode, + "remaining_alive": true_alive, + "asked_keys": [asked_key(item) for item in asked if asked_key(item)], + "width": delivery["width"], + "coverage": metrics["coverage"], + "top1": metrics["top1_hit"], + "tie": metrics["tie"], + "squeezed": metrics["truth_squeezed"], + } + + +def refresh_stats( + *, + request: dict[str, Any], + built: dict[str, Any], + remaining: Sequence[str], + true_time: str, + gate: str, + precision: str, + asked_keys: Sequence[str], + clusters: Sequence[dict[str, Any]], +) -> dict[str, Any]: + if len(remaining) < 2: + return { + "refresh": 0, "discriminative": 0, "unique": 0, + "mean_gain": None, + } + probes = generate_probes( + request, built, remaining, true_time, + gate=gate, precision=precision, refresh=True, asked_keys=asked_keys, + ) + new_probes = [item for item in probes if asked_key(item) and asked_key(item) not in set(asked_keys)] + discriminative = [ + item for item in new_probes + if separates_true(item, true_time, remaining, clusters) + ] + splits = set() + for probe in discriminative: + yes = tuple(sorted(str(time)[:5] for row in probe.get("expected_outcomes") or [] + for time in (row.get("supports") or []) + if str(row.get("answer_class") or "") in {"yes", "weak_yes"})) + no = tuple(sorted(str(time)[:5] for row in probe.get("expected_outcomes") or [] + for time in (row.get("supports") or []) + if str(row.get("answer_class") or "") == "no")) + splits.add((yes, no)) + gains = [float(item.get("information_gain") or 0) for item in discriminative] + return { + "refresh": len(new_probes), + "discriminative": len(discriminative), + "unique": len(splits), + "mean_gain": round(sum(gains) / len(gains), 4) if gains else None, + } + + +def varga_policy_for( + name: str, + radius: int, + contexts: Sequence[dict[str, Any]], +) -> VargaPolicy: + window = window_minutes_for_radius(radius) + use_d60 = name == "V3" and radius <= 10 + if use_d60: + attach_d60(contexts) + prefixes = PRODUCTION_VARGA_PREFIXES + (("D60",) if use_d60 else ()) + changing = changing_vargas(contexts, prefixes) + return VargaPolicy( + name=name, + window_minutes=window, + changing=changing, + use_d60=use_d60, + ) + + +def run_variant( + *, + request: dict[str, Any], + built: dict[str, Any], + rows: Sequence[dict[str, Any]], + true_time: str, + gate: str, + precision: str, +) -> dict[str, Any]: + contexts = list(built.get("static_contexts") or []) + times = [str(row["time"])[:5] for row in rows] + probes = generate_probes( + request, built, times, true_time, + gate=gate, precision=precision, refresh=False, + ) + delivery = evaluate_delivery( + rows=rows, contexts=contexts, probes=probes, true_time=true_time, + ) + clusters = raw_signature_clusters(contexts) + extra = refresh_stats( + request=request, + built=built, + remaining=delivery["remaining"], + true_time=true_time, + gate=gate, + precision=precision, + asked_keys=delivery["asked_keys"], + clusters=clusters, + ) + return { + "top1": delivery["top1"], + "coverage": delivery["coverage"], + "width": delivery["width"], + "tie": delivery["tie"], + "squeezed": delivery["squeezed"], + "entropy0": delivery["entropy0"], + "entropy6": delivery["entropy6"], + "probes": delivery["probes"], + "asked": delivery["asked"], + "refresh": extra["refresh"], + "discriminative": extra["discriminative"], + "unique": extra["unique"], + "mean_gain": extra["mean_gain"], + "remaining_mode": delivery["remaining_mode"], + "true_alive": delivery["true_alive"], + } + + +def summarize(rows: Sequence[dict[str, Any]]) -> dict[str, Any]: + if not rows: + return { + "n": 0, "top1": None, "coverage": None, "width_median": None, "tie": None, + "squeezed": 0, "entropy0": None, "entropy6": None, "probes_mean": None, + "refresh_mean": None, "discriminative_mean": None, "unique_mean": None, + } + widths = [row["width"] for row in rows if row.get("width") is not None] + entropy0 = [row["entropy0"] for row in rows if row.get("entropy0") is not None] + entropy6 = [row["entropy6"] for row in rows if row.get("entropy6") is not None] + return { + "n": len(rows), + "top1": round(sum(1 for row in rows if row.get("top1")) / len(rows), 4), + "coverage": round(sum(1 for row in rows if row.get("coverage")) / len(rows), 4), + "width_median": statistics.median(widths) if widths else None, + "tie": round(sum(1 for row in rows if row.get("tie")) / len(rows), 4), + "squeezed": sum(1 for row in rows if row.get("squeezed")), + "entropy0": round(sum(entropy0) / len(entropy0), 4) if entropy0 else None, + "entropy6": round(sum(entropy6) / len(entropy6), 4) if entropy6 else None, + "probes_mean": round(sum(int(row.get("probes") or 0) for row in rows) / len(rows), 4), + "refresh_mean": round(sum(int(row.get("refresh") or 0) for row in rows) / len(rows), 4), + "discriminative_mean": round(sum(int(row.get("discriminative") or 0) for row in rows) / len(rows), 4), + "unique_mean": round(sum(int(row.get("unique") or 0) for row in rows) / len(rows), 4), + "alive": sum(1 for row in rows if row.get("true_alive")), + } + + +def md_table(headers: Sequence[str], rows: Sequence[Sequence[Any]]) -> str: + def cell(value: Any) -> str: + if value is None: + return "—" + if isinstance(value, float): + return f"{value:.3f}".rstrip("0").rstrip(".") + return str(value) + + line = "| " + " | ".join(headers) + " |" + sep = "| " + " | ".join("---" if index == 0 else "---:" for index in range(len(headers))) + " |" + body = ["| " + " | ".join(cell(item) for item in row) + " |" for row in rows] + return "\n".join([line, sep, *body]) + + +def decide( + radii: Sequence[int], + m1: dict[str, Any], + m1b: dict[str, Any], + m2: dict[str, Any], + v3_jitter7: dict[str, Any], + combos: dict[str, Any], +) -> tuple[dict[str, dict[str, str]], str, str]: + verdicts: dict[str, dict[str, str]] = {} + for gate in GATES: + if gate == "G0": + continue + labels = [] + notes = [] + for radius in radii: + base = m1[str(radius)]["G0"] + cand = m1[str(radius)][gate] + jitter_row = (m2.get("7") or {}).get(str(radius), {}).get(gate) or {} + squeezed7 = jitter_row.get("squeezed") if jitter_row.get("n") else None + label = gate_verdict(base, cand, jitter7_squeezed=squeezed7) + labels.append(label) + notes.append(f"±{radius}:{label} squeezed7={squeezed7}") + verdicts[gate] = {"verdict": overall_verdict(labels), "note": ";".join(notes)} + for name in ("V1", "V2", "V3"): + labels = [] + notes = [] + for radius in radii: + base = m1b[str(radius)]["V0"] + cand = m1b[str(radius)][name] + jitter_row = v3_jitter7.get(str(radius)) or {} + squeezed7 = jitter_row.get("squeezed") if name == "V3" and jitter_row.get("n") else None + label = gate_verdict(base, cand, jitter7_squeezed=squeezed7) + labels.append(label) + notes.append(f"±{radius}:{label}") + verdicts[name] = {"verdict": overall_verdict(labels), "note": ";".join(notes)} + for radius in radii: + for name, summary in combos.get(str(radius), {}).items(): + base = m1[str(radius)]["G0"] + verdicts[f"{name}@±{radius}"] = { + "verdict": gate_verdict(base, summary, jitter7_squeezed=None), + "note": f"combo vs G0 V0 at ±{radius}", + } + gate_labels = [verdicts[name]["verdict"] for name in GATES if name != "G0"] + varga_labels = [verdicts[name]["verdict"] for name in ("V1", "V2", "V3")] + if any(item == "benefit" for item in gate_labels + varga_labels): + recommended = [ + name for name, row in verdicts.items() + if row["verdict"] == "benefit" and name in {*GATES, "V1", "V2", "V3"} + ] + headline = f"**有收益。** 可另立实现单,推荐:{', '.join(recommended) or '见分表'}。" + decision = "benefit" + elif all(item == "no_benefit" for item in gate_labels + varga_labels): + headline = ( + "**无收益,不立实现单。** 线上 `MIN_BOUNDARY_DAYS=45` / 刷新 30 与等权分盘保持不动。" + "真机卡住的 ±10 窗上,放宽闸门能把宽度从 15 收到 11,但头名从 0.80 掉到 0.75;" + "宽窗上宽度反而变大。V1/V2 与基线相同,V3(D60)在 ±10 把头名降到 0.70。" + "唯一看起来像过门的格子是 G1+V3 在 ±10(命中仍 0.80、宽度 15→11)," + "宽窗上不成立,也没有对该组合做 ±7 记错,不够立实现单。" + ) + decision = "no_benefit" + else: + headline = "**不确定,不立实现单。** 分半径或分档结果不一致,见下表。" + decision = "uncertain" + return verdicts, headline, decision + + +def overall_verdict(labels: Sequence[str]) -> str: + if not labels: + return "uncertain" + if all(item == "benefit" for item in labels): + return "benefit" + if all(item == "no_benefit" for item in labels): + return "no_benefit" + return "uncertain" + + +def write_report(payload: dict[str, Any]) -> None: + m0 = payload.get("m0") or {} + m1 = payload.get("m1") or {} + m1b = payload.get("m1b") or {} + m2 = payload.get("m2") or {} + m3 = payload.get("m3") or {} + verdicts = payload.get("verdicts") or {} + headline = payload.get("headline") or "**不确定**" + counts = payload.get("precision_counts") or {} + main_verdicts = { + name: row for name, row in verdicts.items() + if name in {"G1", "G2", "G3", "G4", "V1", "V2", "V3"} + } + lines = [ + "# 出题闸门按证据精度分档测量(2026-09-14)", + "", + f"- 口径:ayanamsa `{payload['ayanamsa']}`,node mode `{payload['node_mode']}`。不得与上游 true-node 数字直接对比。", + f"- 数据:`{payload['holdout']}`,{payload['case_count']} 例公开 Rodden-AA。" + f"精度 日 {counts.get('day', 0)} / 月 {counts.get('month', 0)} / 年 {counts.get('year', 0)}。", + f"- 半径:{', '.join(f'±{item}' for item in payload['radii'])},步长 {payload['minute_step']} 分钟。", + "- 性质:离线测量。生产 `event_probes.py` 的 `MIN_BOUNDARY_DAYS=45` / `REFRESH_MIN_BOUNDARY_DAYS=30` 未改;计分引擎等权分盘未改。", + "- 线上对照:`unionStillValidRange` = 未淘汰且落后头名不足 8 分的簇覆盖并集。", + "", + "## 结论", + "", + headline, + "", + "判定口径:命中不降 **且** 宽度下降才算有收益;只多出题而宽度变宽不算。" + "±7 天记错挤出真值的档位一律不推荐。本网格覆盖率始终 20/20,M2 挤出全是 0。", + "", + md_table( + ["方案", "判定", "说明"], + [[name, row.get("verdict"), row.get("note")] for name, row in main_verdicts.items()], + ), + "", + "过运与三大外部引擎未参与本单(纯出题闸门 / 分盘配权,不解释运势)。", + "", + "## M0 · 精度处理三组(生产闸门)", + "", + "同一批例子、生产 45/30 闸。A 原样,B 日→月,C 全部→年。用来隔离精度本身,而不是闸门。", + "", + ] + for radius in payload["radii"]: + lines.append(f"### ±{radius}") + lines.append("") + table = [] + for treatment in TREATMENTS: + summary = ((m0.get(str(radius)) or {}).get(treatment) or {}) + table.append([ + treatment, + summary.get("top1"), + summary.get("coverage"), + summary.get("width_median"), + summary.get("tie"), + summary.get("squeezed"), + summary.get("probes_mean"), + summary.get("refresh_mean"), + ]) + lines.append(md_table( + ["组", "头名命中", "区间覆盖", "宽度中位", "并列率", "挤出", "首轮出题均", "六题后再出"], + table, + )) + lines.append("") + lines.append( + "对 BUG-689 邀请文案:组 A 与把日精度降成月的组 B 在 ±10 上命中/宽度完全一样(0.80 / 15)。" + "全部降成年精度的组 C 命中只掉到 0.75。v4 没有原生月精度事件," + "**不能**把「记得到天比记得到月有用得多」写成已证实,更不能承诺能定到分钟。" + ) + lines.append("") + lines.extend(["## M1 · 闸门分档(组 A)", ""]) + for radius in payload["radii"]: + lines.append(f"### ±{radius}") + lines.append("") + table = [] + for gate in GATES: + summary = ((m1.get(str(radius)) or {}).get(gate) or {}) + table.append([ + gate, + summary.get("top1"), + summary.get("coverage"), + summary.get("width_median"), + summary.get("tie"), + summary.get("squeezed"), + summary.get("probes_mean"), + summary.get("refresh_mean"), + summary.get("discriminative_mean"), + ]) + lines.append(md_table( + ["档", "头名命中", "区间覆盖", "宽度中位", "并列率", "挤出", "首轮出题均", "六题后再出", "有分辨力"], + table, + )) + lines.append("") + lines.extend([ + "## M1b · 分盘按分钟敏感度配权", + "", + "V0 生产等权;V1 `min(window/varga_minutes, 1)` 再除以 `2n`;V2 只计窗内至少变一次的盘;V3 = V2 + D60(仅 ±10)。", + "未出现在上游表里的 D2/D3/D5/D7/D11 用 `120/n` 分钟。" + "按盘贡献表本轮是空的:V0 采集误走了生产计分、没有打上分盘标签;" + "但 V1/V2 三档半径都与 V0 数字相同,V3 只在 ±10 把命中从 0.80 降到 0.70,已经够判断这三档不能上线。", + "", + ]) + for radius in payload["radii"]: + lines.append(f"### ±{radius}") + lines.append("") + table = [] + for name in VARGA_NAMES: + summary = ((m1b.get(str(radius)) or {}).get(name) or {}) + table.append([ + name, + summary.get("top1"), + summary.get("coverage"), + summary.get("width_median"), + summary.get("tie"), + summary.get("squeezed"), + ]) + lines.append(md_table( + ["改法", "头名命中", "区间覆盖", "宽度中位", "并列率", "挤出"], + table, + )) + lines.append("") + combos = payload.get("combos") or {} + if combos: + lines.extend(["### 闸门 × 分盘组合(组 A)", ""]) + for radius in payload["radii"]: + table = [] + for name, summary in (combos.get(str(radius)) or {}).items(): + table.append([ + name, + summary.get("top1"), + summary.get("coverage"), + summary.get("width_median"), + summary.get("squeezed"), + summary.get("refresh_mean"), + ]) + if table: + lines.append(f"±{radius}") + lines.append("") + lines.append(md_table( + ["组合", "头名命中", "区间覆盖", "宽度中位", "挤出", "六题后再出"], + table, + )) + lines.append("") + contrib = payload.get("varga_contribution") or {} + if contrib: + lines.extend(["### 各分盘触发与头名贡献", ""]) + table = [] + for prefix, row in contrib.items(): + table.append([ + prefix, + row.get("hits"), + row.get("top1_flips"), + row.get("flip_rate"), + ]) + lines.append(md_table(["分盘", "触发次数", "去掉后头名变化例数", "变化率"], table)) + lines.append("") + lines.extend([ + "## M2 · 答错容忍度(日精度事件日期偏移)", + "", + "组 A 的日精度事件按例、按事件独立随机偏移(种子 20260914,排除 0)。±7 天出现真值挤出的档位不得推荐。", + "", + ]) + for span in JITTER_SPANS: + lines.append(f"### ±{span} 天") + lines.append("") + table = [] + for radius in payload["radii"]: + for gate in GATES: + summary = (((m2.get(str(span)) or {}).get(str(radius)) or {}).get(gate) or {}) + table.append([ + f"±{radius} {gate}", + summary.get("top1"), + summary.get("coverage"), + summary.get("squeezed"), + summary.get("width_median"), + ]) + lines.append(md_table(["格子", "头名命中", "区间覆盖", "挤出", "宽度中位"], table)) + lines.append("") + v3_jitter = payload.get("v3_jitter7") or {} + if v3_jitter: + lines.append("V3 在 ±7 天偏移下:") + lines.append("") + table = [] + for radius in payload["radii"]: + summary = v3_jitter.get(str(radius)) or {} + table.append([ + f"±{radius}", + summary.get("top1"), + summary.get("coverage"), + summary.get("squeezed"), + ]) + lines.append(md_table(["半径", "头名命中", "区间覆盖", "挤出"], table)) + lines.append("") + lines.extend([ + "## M3 · 六题之后还能再出几道", + "", + "数字见 M1 表的「六题后再出」与「有分辨力」。若新增 ≥8 道而宽度只收 1–2 分钟,判定里记为问答成本高于收益。", + "", + md_table( + ["半径", "档", "再出均", "有分辨力均", "宽度中位"], + m3.get("rows") or [], + ), + "", + "## 方法", + "", + "1. 每例先算一次静态盘(与事件无关),再按精度处理 / 偏移 / 分盘政策套 `build_event_contribution_matrix`。", + "2. 闸门只通过研究脚本临时改 `MIN_BOUNDARY_DAYS` / `REFRESH_MIN_BOUNDARY_DAYS`,函数返回后立刻恢复。", + "3. 参与该题的证据精度取该例事件包里最细的一档(有日用日,否则月,否则年)。", + "4. 六题回放用真值簇最优 yes/no(上界,不是真人会怎么答)。交付口径与簇宽度研究相同:lead 8 未淘汰并集。", + "5. 刷新出题走生产 `refresh_probes=true`(含已落地的 R3/R4),只换边界天数闸。", + "", + f"错误 {len(payload.get('errors') or [])} 例。{payload.get('case_count', 0)} 例跑完。", + "", + ]) + if payload.get("errors"): + lines.append("失败例子:") + for row in payload["errors"]: + lines.append(f"- `{row.get('case_id')}` {row.get('error')}") + lines.append("") + REPORT_MD.write_text("\n".join(lines) + "\n", encoding="utf-8") + slim = dict(payload) + slim.pop("case_rows", None) + REPORT_JSON.write_text(json.dumps(slim, ensure_ascii=True, indent=2) + "\n", encoding="utf-8") + + +CHECKPOINT = ROOT / "scratch" / "precision_gate_checkpoint.json" + + +def _log(message: str) -> None: + print(message, flush=True) + path = ROOT / "scratch" / "precision_gate_sweep.log" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + handle.write(message + "\n") + + +def _empty_state(radii: Sequence[int]) -> dict[str, Any]: + return { + "done": [], + "m0_rows": {str(radius): {name: [] for name in TREATMENTS} for radius in radii}, + "m1_rows": {str(radius): {name: [] for name in GATES} for radius in radii}, + "m1b_rows": {str(radius): {name: [] for name in VARGA_NAMES} for radius in radii}, + "combo_rows": {str(radius): {} for radius in radii}, + "m2_rows": { + str(span): {str(radius): {name: [] for name in GATES} for radius in radii} + for span in JITTER_SPANS + }, + "v3_jitter_rows": {str(radius): [] for radius in radii}, + "ablations": [], + "errors": [], + } + + +def _save_checkpoint(state: dict[str, Any]) -> None: + CHECKPOINT.parent.mkdir(parents=True, exist_ok=True) + CHECKPOINT.write_text(json.dumps(state, ensure_ascii=True), encoding="utf-8") + + +def _load_checkpoint(radii: Sequence[int]) -> dict[str, Any]: + if not CHECKPOINT.exists(): + return _empty_state(radii) + payload = json.loads(CHECKPOINT.read_text(encoding="utf-8")) + empty = _empty_state(radii) + for key in empty: + if key in payload: + empty[key] = payload[key] + return empty + + +def run(args: argparse.Namespace) -> dict[str, Any]: + holdout = json.loads(HOLDOUT.read_text(encoding="utf-8")) + cases = list(holdout["cases"]) + if args.limit: + cases = cases[: args.limit] + radii = tuple(int(item) for item in args.radii) + precision_counts = holdout_precision_counts(holdout["cases"]) + state = _load_checkpoint(radii) if args.resume else _empty_state(radii) + m0_rows = state["m0_rows"] + m1_rows = state["m1_rows"] + m1b_rows = state["m1b_rows"] + combo_rows = state["combo_rows"] + m2_rows = state["m2_rows"] + v3_jitter_rows = state["v3_jitter_rows"] + ablations = state["ablations"] + errors = state["errors"] + done = set(state.get("done") or []) + assert event_probes.MIN_BOUNDARY_DAYS == 45 + assert event_probes.REFRESH_MIN_BOUNDARY_DAYS == 30 + + for case in cases: + case_id = case["case_id"] + true_time = str(case["birth"]["time"])[:5] + if case_id in done: + _log(f"skip {case_id}") + continue + _log(f"case {case_id}") + try: + for radius in radii: + base_request = scoring_request_for({**case, "candidate_radius_minutes": radius}, radius) + contexts = compute_candidate_static_contexts(base_request) + original_events = list(case.get("events") or []) + scored: dict[str, dict[str, Any]] = {} + for treatment in TREATMENTS: + events = treat_events(original_events, treatment) + scored[treatment] = score_bundle(case, radius, events, contexts, None) + precision = finest_precision(events) + measured = run_variant( + request=scored[treatment]["request"], + built=scored[treatment]["built"], + rows=scored[treatment]["rows"], + true_time=true_time, + gate="G0", + precision=precision, + ) + measured["case_id"] = case_id + m0_rows[str(radius)][treatment].append(measured) + a_precision = finest_precision(original_events) + for gate in GATES: + if gate == "G0": + measured = dict(m0_rows[str(radius)]["A"][-1]) + else: + measured = run_variant( + request=scored["A"]["request"], + built=scored["A"]["built"], + rows=scored["A"]["rows"], + true_time=true_time, + gate=gate, + precision=a_precision, + ) + measured["case_id"] = case_id + m1_rows[str(radius)][gate].append(measured) + scores_a = { + str(row["time"])[:5]: float(row.get("score") or 0) + for row in scored["A"]["rows"] + } + v0_policy = varga_policy_for("V0", radius, contexts) + v0_row = candidate_ablation = None + _log(f" radius ±{radius} varga") + for v_name in VARGA_NAMES: + policy = varga_policy_for(v_name, radius, contexts) + if v_name == "V0": + bundle = scored["A"] + v0_policy = policy + else: + bundle = score_bundle(case, radius, original_events, contexts, policy) + measured = run_variant( + request=bundle["request"], + built=bundle["built"], + rows=bundle["rows"], + true_time=true_time, + gate="G0", + precision=a_precision, + ) + measured["case_id"] = case_id + measured["changing"] = sorted(policy.changing) + m1b_rows[str(radius)][v_name].append(measured) + if v_name == "V0": + v0_row = bundle + scored[v_name] = bundle + if v_name != "V0": + for gate in GATES: + if gate == "G0": + continue + combo = run_variant( + request=bundle["request"], + built=bundle["built"], + rows=bundle["rows"], + true_time=true_time, + gate=gate, + precision=a_precision, + ) + combo["case_id"] = case_id + combo_rows[str(radius)].setdefault(f"{gate}+{v_name}", []).append(combo) + if v0_row is not None: + # Re-score V0 through the policy scorer only to collect per-varga points. + collector = varga_policy_for("V0", radius, contexts) + score_bundle(case, radius, original_events, contexts, collector) + candidate_ablation = ablate_top1(scores_a, collector.points_by_time, true_time) + candidate_ablation["case_id"] = case_id + candidate_ablation["radius"] = radius + candidate_ablation["hits"] = dict(collector.hits) + ablations.append(candidate_ablation) + if not args.skip_m2: + _log(f" radius ±{radius} jitter") + for span in JITTER_SPANS: + jittered = jitter_day_events(original_events, span, case_id=case_id) + bundle = score_bundle(case, radius, jittered, contexts, None) + jitter_precision = finest_precision(jittered) + for gate in GATES: + measured = run_variant( + request=bundle["request"], + built=bundle["built"], + rows=bundle["rows"], + true_time=true_time, + gate=gate, + precision=jitter_precision, + ) + measured["case_id"] = case_id + m2_rows[str(span)][str(radius)][gate].append(measured) + _log(f" radius ±{radius} V3±7") + jitter7 = jitter_day_events(original_events, 7, case_id=case_id) + v3_policy = varga_policy_for("V3", radius, contexts) + bundle = score_bundle(case, radius, jitter7, contexts, v3_policy) + measured = run_variant( + request=bundle["request"], + built=bundle["built"], + rows=bundle["rows"], + true_time=true_time, + gate="G0", + precision=finest_precision(jitter7), + ) + measured["case_id"] = case_id + v3_jitter_rows[str(radius)].append(measured) + _log( + f" radius ±{radius} A_probes={m1_rows[str(radius)]['G0'][-1]['probes']} " + f"G3_probes={m1_rows[str(radius)]['G3'][-1]['probes']}", + ) + except Exception as exc: # noqa: BLE001 + errors.append({ + "case_id": case_id, + "error": f"{type(exc).__name__}: {exc}", + "trace": traceback.format_exc(), + }) + _log(f" FAIL {case_id}: {exc}") + done.add(case_id) + state = { + "done": sorted(done), + "m0_rows": m0_rows, + "m1_rows": m1_rows, + "m1b_rows": m1b_rows, + "combo_rows": combo_rows, + "m2_rows": m2_rows, + "v3_jitter_rows": v3_jitter_rows, + "ablations": ablations, + "errors": errors, + } + _save_checkpoint(state) + _log(f" checkpoint {len(done)}/{len(cases)}") + assert event_probes.MIN_BOUNDARY_DAYS == 45 + assert event_probes.REFRESH_MIN_BOUNDARY_DAYS == 30 + + m0 = {radius: {name: summarize(rows) for name, rows in treatments.items()} for radius, treatments in m0_rows.items()} + m1 = {radius: {name: summarize(rows) for name, rows in gates.items()} for radius, gates in m1_rows.items()} + m1b = {radius: {name: summarize(rows) for name, rows in variants.items()} for radius, variants in m1b_rows.items()} + combos = { + radius: {name: summarize(rows) for name, rows in variants.items()} + for radius, variants in combo_rows.items() + } + m2 = { + span: { + radius: {gate: summarize(rows) for gate, rows in gates.items()} + for radius, gates in radii_map.items() + } + for span, radii_map in m2_rows.items() + } + v3_jitter7 = {radius: summarize(rows) for radius, rows in v3_jitter_rows.items()} + contribution: dict[str, dict[str, Any]] = {} + prefixes = sorted({prefix for row in ablations for prefix in (row.get("hits") or {})}) + n_ablate = max(len(ablations), 1) + for prefix in prefixes: + hits = sum(int((row.get("hits") or {}).get(prefix) or 0) for row in ablations) + flips = sum(int((row.get("flips") or {}).get(prefix) or 0) for row in ablations) + contribution[prefix] = { + "hits": hits, + "top1_flips": flips, + "flip_rate": round(flips / n_ablate, 4), + } + verdicts, headline, decision = decide(radii, m1, m1b, m2, v3_jitter7, combos) + m3_rows = [] + for radius in radii: + for gate in GATES: + summary = m1[str(radius)][gate] + m3_rows.append([ + f"±{radius}", gate, + summary.get("refresh_mean"), + summary.get("discriminative_mean"), + summary.get("width_median"), + ]) + payload = { + "generated_at": TODAY.isoformat(), + "ayanamsa": AYANAMSA, + "node_mode": NODE_MODE, + "holdout": str(HOLDOUT.relative_to(ROOT)).replace("\\", "/"), + "case_count": len(cases), + "radii": list(radii), + "minute_step": MINUTE_STEP, + "precision_counts": precision_counts, + "headline": headline, + "decision": decision, + "m0": m0, + "m1": m1, + "m1b": m1b, + "combos": combos, + "m2": m2, + "v3_jitter7": v3_jitter7, + "m3": {"rows": m3_rows}, + "varga_contribution": contribution, + "verdicts": verdicts, + "errors": errors, + "production_gate": { + "MIN_BOUNDARY_DAYS": event_probes.MIN_BOUNDARY_DAYS, + "REFRESH_MIN_BOUNDARY_DAYS": event_probes.REFRESH_MIN_BOUNDARY_DAYS, + }, + } + write_report(payload) + return payload + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--limit", type=int, default=0) + parser.add_argument("--radii", nargs="+", default=[str(item) for item in RADII]) + parser.add_argument("--skip-m2", action="store_true") + parser.add_argument("--resume", action="store_true") + parser.add_argument("--from-json", action="store_true") + args = parser.parse_args() + if args.from_json: + payload = json.loads(REPORT_JSON.read_text(encoding="utf-8")) + radii = tuple(int(item) for item in payload["radii"]) + verdicts, headline, decision = decide( + radii, + payload["m1"], + payload["m1b"], + payload.get("m2") or {}, + payload.get("v3_jitter7") or {}, + payload.get("combos") or {}, + ) + payload["verdicts"] = verdicts + payload["headline"] = headline + payload["decision"] = decision + write_report(payload) + else: + payload = run(args) + print( + f"wrote {REPORT_MD} cases={payload['case_count']} errors={len(payload['errors'])} " + f"decision={payload['decision']}", + flush=True, + ) + return 0 if not payload["errors"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_precision_gate_research.py b/tests/test_precision_gate_research.py new file mode 100644 index 00000000..dd7f7a82 --- /dev/null +++ b/tests/test_precision_gate_research.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path + +from scripts.rectification.event_probes import MIN_BOUNDARY_DAYS, REFRESH_MIN_BOUNDARY_DAYS +import scripts.rectification.event_probes as event_probes +from scripts.research.precision_gate_lib import ( + GATES, + close_same_year_windows, + count_precision, + finest_precision, + gate_verdict, + jitter_day_events, + patched_boundary_gate, + production_defaults_intact, + threshold_for, + treat_events, + varga_factor, + varga_minutes, + window_minutes_for_radius, +) + +HOLDOUT = Path("references/real_case_calibration/minute_rectification_holdout_v4.json") + + +def _events() -> list[dict]: + return [ + {"id": "day-1", "domain": "career", "date": "2011-08-24", "precision": "day"}, + {"id": "year-1", "domain": "education", "date": "2003", "precision": "year"}, + ] + + +def test_holdout_v4_has_the_stated_precision_mix() -> None: + payload = json.loads(HOLDOUT.read_text(encoding="utf-8")) + tallies = count_precision([ + event for case in payload["cases"] for event in case["events"] + ]) + assert tallies["day"] == 59 + assert tallies["year"] == 84 + assert tallies["month"] == 0 + assert len(payload["cases"]) == 20 + + +def test_treatment_b_downgrades_day_only() -> None: + treated = treat_events(_events(), "B") + assert treated[0]["precision"] == "month" + assert treated[1]["precision"] == "year" + + +def test_treatment_c_downgrades_everything_to_year() -> None: + treated = treat_events(_events(), "C") + assert {item["precision"] for item in treated} == {"year"} + + +def test_finest_precision_prefers_day() -> None: + assert finest_precision(_events()) == "day" + assert finest_precision(treat_events(_events(), "B")) == "month" + assert finest_precision(treat_events(_events(), "C")) == "year" + + +def test_g0_refresh_stays_at_production_30() -> None: + assert threshold_for("G0", "day", refresh=False) == 45 + assert threshold_for("G0", "day", refresh=True) == 30 + assert threshold_for("G1", "day", refresh=True) == 10 + assert threshold_for("G3", "day", refresh=False) == 5 + assert set(GATES) == {"G0", "G1", "G2", "G3", "G4"} + + +def test_jitter_stays_inside_the_span_and_is_reproducible() -> None: + first = jitter_day_events(_events(), 7, case_id="demo") + second = jitter_day_events(_events(), 7, case_id="demo") + assert first[0]["date"] == second[0]["date"] + original = date.fromisoformat("2011-08-24") + shifted = date.fromisoformat(first[0]["date"]) + delta = abs((shifted - original).days) + assert 1 <= delta <= 7 + assert first[1]["date"] == "2003" + + +def test_varga_factor_downweights_slow_charts_in_a_20_minute_window() -> None: + assert window_minutes_for_radius(10) == 20 + assert varga_minutes("D30") == 4.0 + assert varga_minutes("D2") == 60.0 + assert varga_factor("D30", 20) == 1.0 + assert abs(varga_factor("D2", 20) - 20 / 60) < 1e-9 + assert varga_minutes("D4") == 7.5 + + +def test_forty_day_same_year_window_needs_the_lower_gate() -> None: + left = date(2001, 3, 1) + right = date(2001, 4, 10) + assert close_same_year_windows(left, right, 45) == [] + kept = close_same_year_windows(left, right, 30) + assert [item.month for item in kept] == [3, 4] + + +def test_patched_gate_restores_production_defaults() -> None: + assert event_probes.MIN_BOUNDARY_DAYS == 45 + with patched_boundary_gate(initial=10, refresh=7): + assert event_probes.MIN_BOUNDARY_DAYS == 10 + assert event_probes.REFRESH_MIN_BOUNDARY_DAYS == 7 + assert event_probes.MIN_BOUNDARY_DAYS == 45 + assert event_probes.REFRESH_MIN_BOUNDARY_DAYS == 30 + assert production_defaults_intact() + assert MIN_BOUNDARY_DAYS == 45 + assert REFRESH_MIN_BOUNDARY_DAYS == 30 + + +def test_verdict_blocks_a_plus_minus_seven_squeeze() -> None: + baseline = { + "n": 20, "top1": 0.8, "coverage": 1.0, "width_median": 15, + "squeezed": 0, "probes_mean": 4, "refresh_mean": 1, "discriminative_mean": 1, + } + narrower = {**baseline, "width_median": 9, "refresh_mean": 3, "discriminative_mean": 2} + squeezed = {**narrower, "coverage": 0.95, "squeezed": 1} + assert gate_verdict(baseline, narrower, jitter7_squeezed=0) == "benefit" + assert gate_verdict(baseline, narrower, jitter7_squeezed=1) == "no_benefit" + assert gate_verdict(baseline, squeezed, jitter7_squeezed=0) == "no_benefit" + + +def test_eight_extra_questions_for_two_minutes_is_not_worth_it() -> None: + baseline = { + "n": 20, "top1": 0.8, "coverage": 1.0, "width_median": 15, + "squeezed": 0, "probes_mean": 4, "refresh_mean": 1, "discriminative_mean": 1, + } + costly = { + **baseline, "width_median": 13, "refresh_mean": 10, "discriminative_mean": 8, + } + assert gate_verdict(baseline, costly, jitter7_squeezed=0) == "no_benefit" + + +def test_more_probes_with_a_wider_range_is_not_a_benefit() -> None: + baseline = { + "n": 20, "top1": 0.55, "coverage": 1.0, "width_median": 33, + "squeezed": 0, "probes_mean": 7.1, "refresh_mean": 5.45, "discriminative_mean": 5.45, + } + wider = { + **baseline, "width_median": 37, "refresh_mean": 5.5, "discriminative_mean": 5.5, + } + assert gate_verdict(baseline, wider, jitter7_squeezed=0) == "no_benefit"