File size: 3,296 Bytes
5952424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""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  # all MASK
    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