153 lines
7.3 KiB
Python
153 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
"""ГЕЙТ ПОЗИЦИОННОГО НАКЛОНА. $0, только чтение сырья.
|
||
|
||
⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Норма рига требует наклон ЗАМЕРИТЬ, ВЫЧЕСТЬ и СТОРОЖИТЬ ГЕЙТОМ, и отчёт
|
||
фазы (23-editor-tier.md:1027) прямо утверждает, что так и делается. Фактически в зоне фазы Д кода
|
||
наклона не было ни строки: замер сделан ОДИН раз руками при финальном аудите, уже ПОСЛЕ вердиктов
|
||
(девиация объявлена в Д10), сторожа нет. Здесь он есть.
|
||
|
||
ЧТО МЕРИТСЯ. Наклон — это зависимость выставленного счёта от НОМЕРА МЕТКИ в пачке: судья,
|
||
устающий к концу задания, ставит поздним меткам больше ошибок. Сам по себе наклон вердикту не
|
||
вредит — он сокращается в контрасте, ЕСЛИ средние позиции двух армов совпадают. Вредит
|
||
ПРОИЗВЕДЕНИЕ наклона на дисбаланс позиций, и именно оно здесь и гейтится.
|
||
|
||
поправка контраста = наклон × (средняя позиция арма A − средняя позиция арма B)
|
||
|
||
ГЕЙТ: пачка не годна, если поправка хоть к одному первичному контрасту превышает
|
||
`MAX_SHARE` от порога различимости этой оси — то есть если перестановка меток способна
|
||
съесть заметную долю решающей величины.
|
||
|
||
Запуск: naklon.py [--pass=<проход>] | --selftest
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import collections
|
||
import json
|
||
import pathlib
|
||
import re
|
||
import statistics as st
|
||
import sys
|
||
|
||
BOOKS = pathlib.Path.home() / "books"
|
||
AXRE = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
|
||
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК")
|
||
MAX_SHARE = 0.25 # доля порога, которую поправке позволено съедать
|
||
|
||
PASSES = {
|
||
"tier": (["editor-tier/aj-tier"], "editor-tier/blind-keys/tier-KEY.json",
|
||
[("T1", "R0"), ("T2", "R0"), ("T3", "R0")], 1.02),
|
||
"tier2": (["editor-tier/tier2"], "editor-tier/blind-keys/tier2-KEY.json",
|
||
[("T1", "R0"), ("T2", "R0"), ("T3", "R0")], 1.84),
|
||
"border": (["editor-tier/aj-border"], "editor-tier/blind-keys/border-KEY.json",
|
||
[("R2", "R0")], 1.48),
|
||
"d4": (["judging/sud-d4/p1-answers", "judging/sud-d4/p2-answers"], "dovodka/blind-keys-d4",
|
||
[("A1B", "A0"), ("A3", "A0"), ("A6", "A0")], 1.65),
|
||
"d3": (["judging/sud-d3/p1-answers", "judging/sud-d3/p2-answers"], "dovodka/blind-keys-d3",
|
||
[("E0", "D0")], 0.90),
|
||
"d6": (["judging/sud-d6/p1-answers", "judging/sud-d6/p2-answers"], "dovodka/blind-keys-d6",
|
||
[("J0", "D0")], 2.10),
|
||
"d1": (["judging/sud-d1/p1-answers", "judging/sud-d1/p2-answers"], "dovodka/blind-keys-d1",
|
||
[("R1", "A0")], 1.47),
|
||
}
|
||
|
||
|
||
def load_key(rel: str) -> dict:
|
||
p = BOOKS / rel
|
||
if p.is_dir():
|
||
k: dict = {}
|
||
for f in sorted(p.glob("*-KEY.json")):
|
||
k.update(json.loads(f.read_text(encoding="utf-8")))
|
||
return k
|
||
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
|
||
|
||
|
||
def collect(rels: list[str], keyrel: str):
|
||
"""[(номер метки, арм, счёт)] по всем разобранным клеткам прохода."""
|
||
key = load_key(keyrel)
|
||
rows = []
|
||
for rel in rels:
|
||
d = BOOKS / rel
|
||
if not d.exists():
|
||
continue
|
||
for f in sorted(d.glob("*.txt")):
|
||
km = key.get(f.stem)
|
||
if not isinstance(km, dict):
|
||
continue
|
||
cells: dict[str, dict[str, int]] = collections.defaultdict(dict)
|
||
for m in AXRE.finditer(f.read_text(encoding="utf-8", errors="replace")):
|
||
cells["Т" + m.group(1)][m.group(2)] = int(m.group(3))
|
||
for lab, ax in cells.items():
|
||
meta = km.get(lab)
|
||
if isinstance(meta, dict) and meta.get("arm"):
|
||
rows.append((int(lab[1:]), meta["arm"], sum(ax.get(a, 0) for a in CARRY)))
|
||
return rows
|
||
|
||
|
||
def slope(rows) -> float:
|
||
"""Ошибок на ШАГ метки — обычная линейная регрессия счёта на номер метки."""
|
||
if len(rows) < 4:
|
||
return 0.0
|
||
xs = [r[0] for r in rows]
|
||
ys = [r[2] for r in rows]
|
||
mx, my = st.mean(xs), st.mean(ys)
|
||
den = sum((x - mx) ** 2 for x in xs)
|
||
return sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den if den else 0.0
|
||
|
||
|
||
def report(name: str) -> int:
|
||
rels, keyrel, fam, thr = PASSES[name]
|
||
rows = collect(rels, keyrel)
|
||
if not rows:
|
||
print(f"{name:8s} — ответов нет")
|
||
return 0
|
||
s = slope(rows)
|
||
pos = collections.defaultdict(list)
|
||
for lab, arm, _ in rows:
|
||
pos[arm].append(lab)
|
||
mean_pos = {a: st.mean(v) for a, v in pos.items()}
|
||
print(f"\n=== {name} · точек {len(rows)} · наклон {s:+.4f} ошибки на шаг метки · "
|
||
f"порог оси {thr:.2f} ===")
|
||
rc = 0
|
||
for a, b in fam:
|
||
if a not in mean_pos or b not in mean_pos:
|
||
continue
|
||
dp = mean_pos[a] - mean_pos[b]
|
||
corr = s * dp
|
||
share = abs(corr) / thr if thr else 0.0
|
||
mark = " ⛔ ПРОБОЙ" if share > MAX_SHARE else ""
|
||
print(f" {a + '/' + b:12s} позиции {mean_pos[a]:5.2f} против {mean_pos[b]:5.2f} "
|
||
f"(разница {dp:+.2f}) → поправка {corr:+.3f} = {share:.0%} порога{mark}")
|
||
if share > MAX_SHARE:
|
||
rc = 1
|
||
return rc
|
||
|
||
|
||
def selftest() -> int:
|
||
fails = []
|
||
# синтетика с ИЗВЕСТНЫМ наклоном: счёт = 2 × номер метки
|
||
rows = [(i, "A" if i % 2 else "B", 2 * i) for i in range(1, 21)]
|
||
s = slope(rows)
|
||
if abs(s - 2.0) > 1e-9:
|
||
fails.append(f"наклон на синтетике {s:.4f}, ожидалось 2.0")
|
||
flat = [(i, "A", 5) for i in range(1, 21)]
|
||
if abs(slope(flat)) > 1e-9:
|
||
fails.append("на плоских данных наклон обязан быть нулём")
|
||
if len(collect(*PASSES["d4"][:2])) < 100:
|
||
fails.append("проход d4 не разбирается")
|
||
for m in fails:
|
||
print("ПРОВАЛ:", m)
|
||
print(f"селфтест наклона: провалов {len(fails)}")
|
||
return 1 if fails else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
args = sys.argv[1:]
|
||
if "--selftest" in args:
|
||
sys.exit(selftest())
|
||
want = [a.split("=", 1)[1] for a in args if a.startswith("--pass=")] or list(PASSES)
|
||
rc = 0
|
||
for n in want:
|
||
rc |= report(n)
|
||
print(f"\n⚠ Гейт роняет проход, если поправка съедает больше {MAX_SHARE:.0%} порога "
|
||
f"различимости: столько способна дать одна перестановка меток.")
|
||
sys.exit(rc)
|