| """eval_span_whole: the realistic restoration task ('fill in a blank in an inscription/papyrus |
| AS EDITED, real lacunae intact elsewhere in the same document') must never place its scored |
| synthetic gap on top of a real lacuna, and must run cleanly end to end.""" |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "insc" / "eval")) |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "insc" / "data")) |
|
|
| from model.char_bert import CharBertConfig, CharBertEncoder |
| import restore |
|
|
|
|
| def _tiny_model(): |
| torch.manual_seed(0) |
| cfg = CharBertConfig(attn_impl="sdpa", d_model=32, n_heads=4, depth=1, char_window=0) |
| m = CharBertEncoder(cfg) |
| m.eval() |
| return m |
|
|
|
|
| def _fake_records_with_lacuna(n, L, seed=0): |
| rng = np.random.default_rng(seed) |
| recs = [] |
| for i in range(n): |
| chars = rng.integers(0, 24, L).astype(np.int64) |
| boundary = np.zeros(L, np.int64); boundary[4::5] = 1; boundary[-1] = 2 |
| is_real_lacuna = np.zeros(L, dtype=bool) |
| lac_s = int(rng.integers(10, L - 15)) |
| lac_len = 5 |
| chars[lac_s:lac_s + lac_len] = 24 |
| boundary[lac_s:lac_s + lac_len] = 3 |
| is_real_lacuna[lac_s:lac_s + lac_len] = True |
| recs.append(dict(chars=chars, boundary=boundary, is_real_lacuna=is_real_lacuna, |
| region_id=3, century_id=5, _lac=(lac_s, lac_s + lac_len))) |
| return recs |
|
|
|
|
| def test_eval_span_whole_runs_end_to_end(): |
| model = _tiny_model() |
| recs = _fake_records_with_lacuna(5, 100) |
| r = restore.eval_span_whole(model, recs, L=3, device=torch.device("cpu"), n=5, beam_width=4) |
| assert r["n"] > 0 |
| assert 0 <= r["top1"] <= 1 and 0 <= r["top20"] <= 1 |
|
|
|
|
| def test_eval_span_whole_never_overlaps_real_lacuna(): |
| model = _tiny_model() |
| recs = _fake_records_with_lacuna(30, 60, seed=1) |
| seen_gaps = [] |
| orig = restore.beam_restore |
|
|
| def spy(model, chars, gap, bnd_row, device, *args, **kwargs): |
| seen_gaps.append(list(gap)) |
| return orig(model, chars, gap, bnd_row, device, *args, **kwargs) |
|
|
| restore.beam_restore = spy |
| try: |
| r = restore.eval_span_whole(model, recs, L=2, device=torch.device("cpu"), n=30, |
| beam_width=2) |
| finally: |
| restore.beam_restore = orig |
| assert r["n"] > 0 |
| assert len(seen_gaps) == r["n"] |
| for gap, rec in zip(seen_gaps, recs): |
| lac_s, lac_e = rec["_lac"] |
| assert not any(lac_s <= p < lac_e for p in gap), \ |
| f"synthetic gap {gap} overlaps real lacuna [{lac_s},{lac_e})" |
|
|
|
|
| def test_eval_span_whole_skips_records_with_no_valid_position(): |
| """A record entirely consumed by real lacunae (or too short) has no valid place for the |
| synthetic gap -- must be skipped, not crash.""" |
| model = _tiny_model() |
| L = 20 |
| chars = np.zeros(L, np.int64) + 24 |
| boundary = np.full(L, 3, np.int64) |
| is_real_lacuna = np.ones(L, dtype=bool) |
| recs = [dict(chars=chars, boundary=boundary, is_real_lacuna=is_real_lacuna, |
| region_id=3, century_id=5)] |
| r = restore.eval_span_whole(model, recs, L=3, device=torch.device("cpu"), n=5, beam_width=2) |
| assert r["n"] == 0 |
|
|