rosetta-eclab 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rosetta_eclab/__init__.py +0 -0
- rosetta_eclab/cli.py +113 -0
- rosetta_eclab/data/stats.json +1 -0
- rosetta_eclab/data/stats_features.npy +0 -0
- rosetta_eclab/debug.py +85 -0
- rosetta_eclab/engine.py +203 -0
- rosetta_eclab/evaluate.py +196 -0
- rosetta_eclab/fallback.py +130 -0
- rosetta_eclab/golden.py +74 -0
- rosetta_eclab/library.py +708 -0
- rosetta_eclab/misses.py +39 -0
- rosetta_eclab/mprfile.py +178 -0
- rosetta_eclab-0.1.1.dist-info/METADATA +136 -0
- rosetta_eclab-0.1.1.dist-info/RECORD +31 -0
- rosetta_eclab-0.1.1.dist-info/WHEEL +5 -0
- rosetta_eclab-0.1.1.dist-info/entry_points.txt +2 -0
- rosetta_eclab-0.1.1.dist-info/licenses/LICENSE +674 -0
- rosetta_eclab-0.1.1.dist-info/top_level.txt +2 -0
- spike2/__init__.py +0 -0
- spike2/agent.py +372 -0
- spike2/api_check.py +117 -0
- spike2/baselines.py +153 -0
- spike2/corpus.py +162 -0
- spike2/dsl.py +167 -0
- spike2/elim.py +52 -0
- spike2/features.py +137 -0
- spike2/lock.py +59 -0
- spike2/run.py +218 -0
- spike2/score.py +223 -0
- spike2/tasks.py +355 -0
- spike2/vocab.py +126 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Evaluate the standalone namer.
|
|
2
|
+
|
|
3
|
+
python -m namer.evaluate --pool # development pool: every corpus column NOT used by the v2 test split
|
|
4
|
+
python -m namer.evaluate --split dev # v2 dev tasks -> runs3/rules_dev.jsonl (score with bench.claude_cli --score)
|
|
5
|
+
python -m namer.evaluate --split test # v2 test tasks -> runs3/rules_test.jsonl (run rarely; record in results/open_runs.md)
|
|
6
|
+
|
|
7
|
+
Pipeline per column: constant -> ABSTAIN; identical to a known column -> DUPLICATE;
|
|
8
|
+
name elimination (spike2/elim.py) -> NAME; otherwise the formula library (namer/library.py).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from collections import Counter, defaultdict
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from spike2 import corpus, elim, features, tasks, vocab
|
|
22
|
+
|
|
23
|
+
from .engine import Context, Known, name_column
|
|
24
|
+
from .fallback import Stats, lag1_autocorr
|
|
25
|
+
from .library import LIBRARY
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
28
|
+
RUNS = ROOT / "runs3"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_STATS: Stats | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def consistent(name: str, ctx: Context) -> bool:
|
|
35
|
+
"""A statistics guess must not contradict what the library knows about the quantity's form."""
|
|
36
|
+
from .library import DTYPES
|
|
37
|
+
|
|
38
|
+
exp = DTYPES.get(name)
|
|
39
|
+
if exp and ctx.target_dtype not in exp:
|
|
40
|
+
return False
|
|
41
|
+
spec = LIBRARY.get(name)
|
|
42
|
+
from .library import is_clock, piecewise_constant
|
|
43
|
+
|
|
44
|
+
if name in ("Analog IN 1", "Analog IN 2", "Analog OUT"):
|
|
45
|
+
return False # external inputs have no physics to check; only the sibling rule may name them
|
|
46
|
+
if is_clock(ctx) and name not in ("time", "step time", "(Q-Qo)", "Energy we charge", "Energy we discharge", "|Energy|"):
|
|
47
|
+
return False # a monotonic clock is a time (or a running total), nothing else
|
|
48
|
+
from .engine import has_ce_channel, needs_ce_channel
|
|
49
|
+
|
|
50
|
+
if needs_ce_channel(name) and not has_ce_channel(set(ctx.known)):
|
|
51
|
+
return False
|
|
52
|
+
if name == "control" and piecewise_constant(ctx) < 0.5:
|
|
53
|
+
return False # a set point holds still between changes
|
|
54
|
+
form = [sh for sh in (spec.shapes if spec is not None else []) if sh.intrinsic]
|
|
55
|
+
if form and max(float(sh.test(ctx)) for sh in form) <= 0:
|
|
56
|
+
return False
|
|
57
|
+
if spec is not None:
|
|
58
|
+
f = ctx.col("freq")
|
|
59
|
+
impedance_record = f is not None and np.mean(f > 0) > 0.5
|
|
60
|
+
from .library import EIS_FAMILY as _EIS
|
|
61
|
+
if not (impedance_record and name not in _EIS):
|
|
62
|
+
# In impedance records the instrument integrates charge and current over measurement windows that the
|
|
63
|
+
# time stamps do not show, so DC relations cannot rule a name out there.
|
|
64
|
+
ev = spec.best_reliable_relation(ctx)
|
|
65
|
+
if ev is not None and ev.score < 0.3:
|
|
66
|
+
return False # its defining relation was testable here and does not hold with the right units
|
|
67
|
+
from .library import EIS_FAMILY
|
|
68
|
+
if name in EIS_FAMILY and ctx.col("freq") is None:
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def stats() -> Stats:
|
|
74
|
+
global _STATS
|
|
75
|
+
if _STATS is None:
|
|
76
|
+
_STATS = Stats()
|
|
77
|
+
return _STATS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def answer(target, dtype, known_cols: list, technique, vocabulary: list[str], abstain_below: float = 0.6,
|
|
81
|
+
exclude_group: str | None = None, noise_autocorr: float = 0.3, fallback_min_share: float = 0.5) -> dict:
|
|
82
|
+
if features.is_constant(target):
|
|
83
|
+
return {"verdict": "ABSTAIN", "name": "NONE", "confidence": 1.0, "evidence": "constant column"}
|
|
84
|
+
same = [c.name for c in known_cols if features.identical(target, c.values)]
|
|
85
|
+
if same:
|
|
86
|
+
return {"verdict": "DUPLICATE", "name": "NONE", "duplicate_of": same[0], "confidence": 1.0,
|
|
87
|
+
"evidence": f"identical to {same[0]}"}
|
|
88
|
+
e = elim.eliminate([c.name for c in known_cols], vocabulary)
|
|
89
|
+
if e:
|
|
90
|
+
return {"verdict": "NAME", "name": e, "confidence": 0.9, "evidence": "name elimination (sibling gap)"}
|
|
91
|
+
known = {}
|
|
92
|
+
for c in known_cols:
|
|
93
|
+
known.setdefault(vocab.canonical(c.name), Known(vocab.canonical(c.name), c.unit, np.asarray(c.values), c.dtype))
|
|
94
|
+
ctx = Context(technique, known, np.asarray(target), dtype)
|
|
95
|
+
pr = stats().prior(technique, exclude_group)
|
|
96
|
+
gl = stats().prior(None, exclude_group, all_techniques=True)
|
|
97
|
+
top, gtop = (max(pr.values()) if pr else 0), (max(gl.values()) if gl else 0)
|
|
98
|
+
prior = {k: (pr.get(k, 0) / top if top else 0) + 0.3 * (gl.get(k, 0) / gtop if gtop else 0) for k in set(pr) | set(gl)}
|
|
99
|
+
a = name_column(ctx, LIBRARY, vocabulary, abstain_below, prior,
|
|
100
|
+
cooccur=lambda cand, sib: stats().cooccur(cand, sib, exclude_group))
|
|
101
|
+
if a["verdict"] == "NAME":
|
|
102
|
+
return a
|
|
103
|
+
# (Tried 2026-10-15: naming "the column that similar files always record and this file lacks". It names
|
|
104
|
+
# decoys whenever a file naturally lacks a common column: 10/19 decoys on hold-out 4, still 6/19 when limited
|
|
105
|
+
# to structured columns. Trustworthiness wins: not used.)
|
|
106
|
+
# No formula or shape identifies the column: statistics fallback, unless it looks like noise.
|
|
107
|
+
ac = lag1_autocorr(target)
|
|
108
|
+
if ac < noise_autocorr:
|
|
109
|
+
a["evidence"] = f"no formula fits and the column looks like noise (lag-1 autocorrelation {ac:.2f})"
|
|
110
|
+
return a
|
|
111
|
+
name, share, alts = stats().predict(target, dtype, known_cols, technique, set(vocabulary), set(known),
|
|
112
|
+
exclude_group)
|
|
113
|
+
if name and not consistent(name, ctx):
|
|
114
|
+
a["evidence"] = f"statistics suggest {name}, but the column does not have its form (shape or storage type)"
|
|
115
|
+
return a
|
|
116
|
+
if name and share >= fallback_min_share:
|
|
117
|
+
return {"verdict": "NAME", "name": name, "confidence": round(0.5 * share, 3), "alternatives": alts,
|
|
118
|
+
"evidence": f"no formula fits; statistics of similar columns in other files (vote share {share:.2f})"}
|
|
119
|
+
return a
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_keys() -> set[tuple[str, int]]:
|
|
123
|
+
m = json.loads((ROOT / "prereg2" / "tasks_manifest.json").read_text(encoding="utf-8"))
|
|
124
|
+
return {(t["file"], t["position"]) for t in m["tasks"] if t["split"] == "test" and t["position"] is not None}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def pool(verbose: int) -> None:
|
|
128
|
+
excluded = test_keys()
|
|
129
|
+
V = vocab.names()
|
|
130
|
+
good, by_name, confusions = Counter(), defaultdict(list), Counter()
|
|
131
|
+
for p in corpus.corpus_files():
|
|
132
|
+
try:
|
|
133
|
+
fd = tasks.file_data(p.name)
|
|
134
|
+
except Exception:
|
|
135
|
+
continue
|
|
136
|
+
if fd.n_points < tasks.MIN_ROWS:
|
|
137
|
+
continue
|
|
138
|
+
cols = tasks.usable_columns(fd)
|
|
139
|
+
for c in cols:
|
|
140
|
+
if (p.name, c.position) in excluded or features.is_constant(c.values):
|
|
141
|
+
continue
|
|
142
|
+
others = [o for o in cols if o is not c]
|
|
143
|
+
if any(features.identical(c.values, o.values) for o in others):
|
|
144
|
+
continue
|
|
145
|
+
truth = vocab.canonical(c.name)
|
|
146
|
+
a = answer(c.values, c.dtype, others, fd.technique, V, exclude_group=corpus.source_group(p.name))
|
|
147
|
+
ok = a["verdict"] == "NAME" and a["name"] == truth
|
|
148
|
+
by_name[truth].append(ok)
|
|
149
|
+
if not ok:
|
|
150
|
+
confusions[(truth, a["name"] if a["verdict"] == "NAME" else a["verdict"])] += 1
|
|
151
|
+
macro = np.mean([np.mean(v) for v in by_name.values()])
|
|
152
|
+
micro = np.mean([x for v in by_name.values() for x in v])
|
|
153
|
+
print(f"pool: {sum(len(v) for v in by_name.values())} columns, {len(by_name)} names, macro {macro:.3f}, micro {micro:.3f}")
|
|
154
|
+
for n, v in sorted(by_name.items(), key=lambda kv: np.mean(kv[1])):
|
|
155
|
+
if np.mean(v) < 1.0 or verbose > 1:
|
|
156
|
+
print(f" {np.mean(v):.2f} ({sum(v)}/{len(v)}) {n}")
|
|
157
|
+
if verbose:
|
|
158
|
+
print("top confusions:")
|
|
159
|
+
for (t, a), k in confusions.most_common(25):
|
|
160
|
+
print(f" {k:3d} {t} -> {a}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def bench(split: str, abstain_below: float) -> None:
|
|
164
|
+
RUNS.mkdir(exist_ok=True)
|
|
165
|
+
out = []
|
|
166
|
+
for t in tasks.load(split):
|
|
167
|
+
x, dtype, kv, kc = tasks.hidden_and_known(t)
|
|
168
|
+
a = answer(x, dtype, list(kc.values()), t["technique"], tasks.task_vocabulary(t), abstain_below,
|
|
169
|
+
exclude_group=corpus.source_group(t["file"]))
|
|
170
|
+
out.append({"task_id": t["task_id"], "split": split, "status": "ok", "arm": "rules", **a})
|
|
171
|
+
path = RUNS / f"rules_{split}.jsonl"
|
|
172
|
+
path.write_text("\n".join(json.dumps(r, ensure_ascii=False, default=str) for r in out) + "\n",
|
|
173
|
+
encoding="utf-8", newline="\n")
|
|
174
|
+
print(f"{len(out)} answers -> {path}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main() -> int:
|
|
178
|
+
import warnings
|
|
179
|
+
|
|
180
|
+
warnings.filterwarnings("ignore")
|
|
181
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
182
|
+
ap = argparse.ArgumentParser()
|
|
183
|
+
ap.add_argument("--pool", action="store_true")
|
|
184
|
+
ap.add_argument("--split", choices=["dev", "test", "holdout", "holdout2", "holdout3", "holdout4", "holdout5"])
|
|
185
|
+
ap.add_argument("--abstain-below", type=float, default=0.6)
|
|
186
|
+
ap.add_argument("-v", "--verbose", action="count", default=0)
|
|
187
|
+
a = ap.parse_args()
|
|
188
|
+
if a.pool:
|
|
189
|
+
pool(a.verbose)
|
|
190
|
+
if a.split:
|
|
191
|
+
bench(a.split, a.abstain_below)
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Statistics fallback for columns no formula can identify (e.g. Ewe in a file without power or current).
|
|
2
|
+
|
|
3
|
+
A k-nearest-neighbour vote over scale-free column features, learned from the
|
|
4
|
+
public EC-Lab corpus, plus a technique prior used as a tie-breaker. It can only
|
|
5
|
+
name quantities it has seen, so its answers carry lower confidence than a
|
|
6
|
+
formula match. A column that looks like noise (no autocorrelation) is never
|
|
7
|
+
named by the fallback.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from collections import Counter, defaultdict
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from spike2 import corpus, features, tasks, vocab
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def lag1_autocorr(x) -> float:
|
|
22
|
+
y = np.asarray(x, dtype="f8")
|
|
23
|
+
y = y[np.isfinite(y)]
|
|
24
|
+
if y.size < 4 or np.ptp(y) == 0:
|
|
25
|
+
return 0.0
|
|
26
|
+
a, b = y[:-1] - y[:-1].mean(), y[1:] - y[1:].mean()
|
|
27
|
+
den = np.sqrt((a * a).sum() * (b * b).sum())
|
|
28
|
+
return float((a * b).sum() / den) if den else 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DATA = Path(__file__).resolve().parent / "data"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Stats:
|
|
35
|
+
"""Column statistics of the public corpus. Built from the downloaded files once, then read from
|
|
36
|
+
namer/data/stats.* so the command-line tool works without the corpus on disk."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, use_cache: bool = True):
|
|
39
|
+
meta, feats = DATA / "stats.json", DATA / "stats_features.npy"
|
|
40
|
+
if use_cache and meta.exists() and feats.exists():
|
|
41
|
+
m = json.loads(meta.read_text(encoding="utf-8"))
|
|
42
|
+
X = np.load(feats)
|
|
43
|
+
self.rows = [(g, t, n, X[i]) for i, (g, t, n) in enumerate(m["rows"])]
|
|
44
|
+
techs = defaultdict(dict)
|
|
45
|
+
for tech, g, f, names in m["files"]:
|
|
46
|
+
techs[tech][(g, f)] = set(names)
|
|
47
|
+
self._finish(techs)
|
|
48
|
+
return
|
|
49
|
+
self._build()
|
|
50
|
+
if use_cache:
|
|
51
|
+
self.save()
|
|
52
|
+
|
|
53
|
+
def save(self):
|
|
54
|
+
DATA.mkdir(exist_ok=True)
|
|
55
|
+
rows = [[g, t, n] for g, t, n, _ in self.rows]
|
|
56
|
+
files = [[tech, g, f, sorted(names)] for tech, d in self.techs.items() for (g, f), names in d.items()]
|
|
57
|
+
(DATA / "stats.json").write_text(json.dumps({"rows": rows, "files": files}, ensure_ascii=False),
|
|
58
|
+
encoding="utf-8", newline="\n")
|
|
59
|
+
np.save(DATA / "stats_features.npy", np.stack([r[3] for r in self.rows]))
|
|
60
|
+
|
|
61
|
+
def _finish(self, techs):
|
|
62
|
+
X = np.stack([r[3] for r in self.rows])
|
|
63
|
+
self.mu, self.sd = X.mean(0), X.std(0) + 1e-9
|
|
64
|
+
self.Z = (X - self.mu) / self.sd
|
|
65
|
+
self.techs = techs
|
|
66
|
+
|
|
67
|
+
def _build(self):
|
|
68
|
+
rows, techs = [], defaultdict(Counter)
|
|
69
|
+
for p in corpus.corpus_files():
|
|
70
|
+
try:
|
|
71
|
+
fd = tasks.file_data(p.name)
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
if fd.n_points < tasks.MIN_ROWS:
|
|
75
|
+
continue
|
|
76
|
+
group = corpus.source_group(p.name)
|
|
77
|
+
cols = tasks.usable_columns(fd)
|
|
78
|
+
tcol = next((c for c in cols if c.name == "time"), None)
|
|
79
|
+
names = {vocab.canonical(c.name) for c in cols}
|
|
80
|
+
techs[fd.technique][(group, p.name)] = names # type: ignore[index]
|
|
81
|
+
for c in cols:
|
|
82
|
+
if features.is_constant(c.values):
|
|
83
|
+
continue
|
|
84
|
+
f = features.knn_features(c.values, c.dtype, None if tcol is None or tcol is c else tcol.values,
|
|
85
|
+
[o.values for o in cols if o is not c])
|
|
86
|
+
rows.append((group, fd.technique, vocab.canonical(c.name), f))
|
|
87
|
+
self.rows = rows
|
|
88
|
+
self._finish(techs)
|
|
89
|
+
|
|
90
|
+
def prior(self, technique, exclude_group: str | None, all_techniques: bool = False) -> Counter:
|
|
91
|
+
cnt = Counter()
|
|
92
|
+
groups = self.techs.values() if all_techniques else [self.techs.get(technique, {})]
|
|
93
|
+
for files in groups:
|
|
94
|
+
for (g, _f), names in files.items():
|
|
95
|
+
if g != exclude_group:
|
|
96
|
+
cnt.update(names)
|
|
97
|
+
return cnt
|
|
98
|
+
|
|
99
|
+
def expected_missing(self, present: set[str], exclude_group: str | None = None, k: int = 5,
|
|
100
|
+
share: float = 0.8) -> list[str]:
|
|
101
|
+
"""Names recorded by at least `share` of the k public files whose column sets are most like this one,
|
|
102
|
+
but missing here."""
|
|
103
|
+
files = [names for t in self.techs.values() for (g, _f), names in t.items() if g != exclude_group]
|
|
104
|
+
if not files or not present:
|
|
105
|
+
return []
|
|
106
|
+
sim = sorted(files, key=lambda n: -len(n & present) / len(n | present))[:k]
|
|
107
|
+
counts = Counter(x for n in sim for x in n)
|
|
108
|
+
return sorted(n for n, c in counts.items() if c >= share * len(sim) and n not in present)
|
|
109
|
+
|
|
110
|
+
def cooccur(self, cand: str, sib: str, exclude_group: str | None = None) -> float:
|
|
111
|
+
"""Share of public files containing sib that also contain cand."""
|
|
112
|
+
with_sib = [names for files in self.techs.values() for (g, _f), names in files.items()
|
|
113
|
+
if g != exclude_group and sib in names]
|
|
114
|
+
return sum(cand in n for n in with_sib) / len(with_sib) if with_sib else 0.0
|
|
115
|
+
|
|
116
|
+
def predict(self, target, dtype, known_cols, technique, allowed: set[str], present: set[str],
|
|
117
|
+
exclude_group: str | None = None, k: int = 5):
|
|
118
|
+
tcol = next((c.values for c in known_cols if c.name == "time"), None)
|
|
119
|
+
f = (features.knn_features(target, dtype, tcol, [c.values for c in known_cols]) - self.mu) / self.sd
|
|
120
|
+
idx = [i for i, r in enumerate(self.rows) if r[0] != exclude_group and r[2] in allowed and r[2] not in present]
|
|
121
|
+
if not idx:
|
|
122
|
+
return None, 0.0, []
|
|
123
|
+
d = np.linalg.norm(self.Z[idx] - f, axis=1)
|
|
124
|
+
votes = defaultdict(float)
|
|
125
|
+
for j in np.argsort(d)[:k]:
|
|
126
|
+
votes[self.rows[idx[j]][2]] += 1.0 / (d[j] + 1e-6)
|
|
127
|
+
pr = self.prior(technique, exclude_group)
|
|
128
|
+
total = sum(votes.values())
|
|
129
|
+
ranked = sorted(votes, key=lambda n: (-votes[n], -pr.get(n, 0), n))
|
|
130
|
+
return ranked[0], votes[ranked[0]] / total, ranked[1:3]
|
rosetta_eclab/golden.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Regression safety net: snapshot every answer the namer gives, and compare later.
|
|
2
|
+
|
|
3
|
+
python -m namer.golden save # writes tests/golden/answers.jsonl (pool + every split)
|
|
4
|
+
python -m namer.golden check # recomputes and lists every difference (exit 1 if any)
|
|
5
|
+
|
|
6
|
+
A refactoring (e.g. replacing where the vocabulary or storage types come from) must
|
|
7
|
+
leave every answer identical: verdict, name, confidence and alternatives.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import warnings
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from spike2 import corpus, features, tasks, vocab
|
|
18
|
+
|
|
19
|
+
from .evaluate import answer, test_keys
|
|
20
|
+
|
|
21
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
22
|
+
GOLDEN = ROOT / "tests" / "golden" / "answers.jsonl"
|
|
23
|
+
SPLITS = ["dev", "test", "holdout", "holdout2", "holdout3", "holdout4", "holdout5"]
|
|
24
|
+
KEYS = ("verdict", "name", "confidence", "alternatives", "duplicate_of")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute() -> list[dict]:
|
|
28
|
+
warnings.filterwarnings("ignore")
|
|
29
|
+
out = []
|
|
30
|
+
excluded = test_keys()
|
|
31
|
+
V = vocab.names()
|
|
32
|
+
for p in corpus.corpus_files():
|
|
33
|
+
try:
|
|
34
|
+
fd = tasks.file_data(p.name)
|
|
35
|
+
except Exception:
|
|
36
|
+
continue
|
|
37
|
+
if fd.n_points < tasks.MIN_ROWS:
|
|
38
|
+
continue
|
|
39
|
+
cols = tasks.usable_columns(fd)
|
|
40
|
+
for c in cols:
|
|
41
|
+
if (p.name, c.position) in excluded or features.is_constant(c.values):
|
|
42
|
+
continue
|
|
43
|
+
others = [o for o in cols if o is not c]
|
|
44
|
+
a = answer(c.values, c.dtype, others, fd.technique, V, exclude_group=corpus.source_group(p.name))
|
|
45
|
+
out.append({"id": f"pool:{p.name}:{c.position}", **{k: a.get(k) for k in KEYS}})
|
|
46
|
+
for split in SPLITS:
|
|
47
|
+
for t in tasks.load(split):
|
|
48
|
+
x, dtype, kv, kc = tasks.hidden_and_known(t)
|
|
49
|
+
a = answer(x, dtype, list(kc.values()), t["technique"], tasks.task_vocabulary(t),
|
|
50
|
+
exclude_group=corpus.source_group(t["file"]))
|
|
51
|
+
out.append({"id": f"{split}:{t['task_id']}", **{k: a.get(k) for k in KEYS}})
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> int:
|
|
56
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
57
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "check"
|
|
58
|
+
rows = compute()
|
|
59
|
+
if cmd == "save":
|
|
60
|
+
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
GOLDEN.write_text("\n".join(json.dumps(r, ensure_ascii=False) for r in rows) + "\n", encoding="utf-8", newline="\n")
|
|
62
|
+
print(f"saved {len(rows)} answers -> {GOLDEN}")
|
|
63
|
+
return 0
|
|
64
|
+
old = {r["id"]: r for r in (json.loads(x) for x in GOLDEN.read_text(encoding="utf-8").splitlines() if x.strip())}
|
|
65
|
+
new = {r["id"]: r for r in rows}
|
|
66
|
+
diffs = [(k, old.get(k), new.get(k)) for k in sorted(set(old) | set(new)) if old.get(k) != new.get(k)]
|
|
67
|
+
for k, a, b in diffs[:50]:
|
|
68
|
+
print(f"DIFF {k}\n before: {a}\n after: {b}")
|
|
69
|
+
print(f"{len(new)} answers compared, {len(diffs)} differences")
|
|
70
|
+
return 1 if diffs else 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
raise SystemExit(main())
|