Stoicheia-code / tests /test_marks.py
anonymous-stoicheia's picture
Upload folder using huggingface_hub
7ed86c3 verified
Raw
History Blame Contribute Delete
10.5 kB
"""Label-projection correctness: alignment round-trips on real corpus lines, and the
ambiguity mask checked against the old project's `markable()` (ported verbatim)."""
import json
import os
import unicodedata
from pathlib import Path
import numpy as np
import pytest
from meter.dataset import (concat_verses, encode_macron_line, encode_plain,
encode_scan_line)
from meter.marks import (MAC_LONG, MAC_SHORT, SCAN_HEAVY, SCAN_LIGHT, SCAN_VERSE,
ambiguous_mask, bracketize, enforce_circumflex_heavy,
insert_marks, merge_vowelless_syllables,
parse_macron_line, parse_scan_line)
SRC = Path(os.path.expandvars(os.environ.get("MACRONIZER_SRC",
"$MACRONIZER_SRC")))
# The four tests below read the released meter-silver tree (MACRONIZER_SRC). Skip them
# rather than fail when it is not checked out: the rest of this file is pure unit tests.
needs_src = pytest.mark.skipif(
not (SRC / "data").is_dir(),
reason="MACRONIZER_SRC not set to a checkout of the meter-silver dataset")
# ---------------------------------------------------------------- old-project reference
DICHRONA = set("αιυ")
DIPHTHONGS = {"αι", "αυ", "ει", "ευ", "ηυ", "οι", "ου", "υι", "ωυ"}
PERISPOMENI, YPOGEGRAMMENI, DIAERESIS = "͂", "ͅ", "̈"
def _base(ch):
return unicodedata.normalize("NFD", ch)[0].lower()
def _has(ch, mark):
return mark in unicodedata.normalize("NFD", ch)
def markable_ref(chars, i):
"""Verbatim port of the macron data tree/scripts/macronize_corpus.py::markable."""
ch = chars[i]
b = _base(ch)
if b not in DICHRONA:
return False
if _has(ch, PERISPOMENI) or _has(ch, YPOGEGRAMMENI):
return False
if i > 0 and not _has(ch, DIAERESIS) and _base(chars[i - 1]) + b in DIPHTHONGS:
return False
if (i + 1 < len(chars) and not _has(chars[i + 1], DIAERESIS)
and b + _base(chars[i + 1]) in DIPHTHONGS):
return False
return True
# ---------------------------------------------------------------- macron parsing
def test_parse_macron_simple():
plain, labels = parse_macron_line("ὦ παῖ τέλος μὲν Ζεὺς ἔχει βα^ρύκτυ^πος")
assert plain == "ὦ παῖ τέλος μὲν Ζεὺς ἔχει βαρύκτυπος"
# letters: ω π α ι τ ε λ ο σ μ ε ν ζ ε υ σ ε χ ε ι β α(21) ρ υ κ τ υ(26) π ο σ
assert labels == {21: MAC_SHORT, 26: MAC_SHORT}
def test_parse_macron_combining_marks():
plain, labels = parse_macron_line("βᾱρῠ́ς") # combining macron + breve-with-acute
assert plain == "βαρύς"
assert labels == {1: MAC_LONG, 3: MAC_SHORT} # β0 α1 ρ2 υ3 ς4
@needs_src
def test_macron_roundtrip_against_plain_column():
"""TSV col1 (plain) and col2 (marked) must strip to identical letter streams."""
checked = 0
for name in ("hypotactic", "oga_0", "anthology", "theocritus_doric"):
path = SRC / "data" / f"{name}.tsv"
with open(path, encoding="utf-8") as f:
for _ in range(300):
line = f.readline()
if not line:
break
plain_col, marked = line.rstrip("\n").split("\t")[:2]
plain, labels = parse_macron_line(marked)
r1, r2 = encode_plain(plain), encode_plain(plain_col)
if r1 is None or r2 is None:
continue
assert np.array_equal(r1.chars, r2.chars), (name, marked)
if labels:
assert max(labels) < len(r1.chars)
checked += 1
assert checked > 850 # theocritus_doric has only 18 lines
def test_insert_marks_roundtrip():
for marked in ("ἥσθην δὲ βαιά^, πά^νυ^ δὲ βαιά^, τέττα^ρα^·",
"Δάφνι τά_λαν, τί_ τὺ_ τά_κεαι, ἁ_ δέ τε κώρα",
"χρὴ γι^νώσκειν ὅτι^ πά_σης τῆς γῆς ὁ περί^μετρος 0 ."):
plain, labels = parse_macron_line(marked)
again = insert_marks(plain, labels)
assert parse_macron_line(again) == (plain, labels)
assert unicodedata.normalize("NFC", again) == unicodedata.normalize("NFC", marked)
# ---------------------------------------------------------------- ambiguity mask
def _mask_via_planes(text):
rec = encode_plain(text)
return rec, ambiguous_mask(rec.chars, rec.boundary, rec.dia)
@needs_src
def test_ambiguous_mask_matches_reference():
lines = []
for name in ("hypotactic", "oga_1", "anthology", "drama_ia6"):
with open(SRC / "data" / f"{name}.tsv", encoding="utf-8") as f:
for _ in range(200):
line = f.readline()
if not line:
break
lines.append(line.split("\t")[0])
checked = 0
for text in lines:
text = unicodedata.normalize("NFC", text)
rec = encode_plain(text)
if rec is None:
continue
# reference mask over raw chars, projected to letter ordinals
chars = list(text)
ref = []
for i, ch in enumerate(chars):
if _base(ch).lower() in set("αβγδεζηθικλμνξοπρστυφχψω") | {"ς", "ϲ"}:
ref.append(markable_ref(chars, i))
if len(ref) != len(rec.chars):
continue # letters the raw walk counts differently (archaic etc.) — rare
ours = ambiguous_mask(rec.chars, rec.boundary, rec.dia)
assert ref == ours.tolist(), text
checked += 1
assert checked > 500
# ---------------------------------------------------------------- scanner parsing
def test_parse_scan_simple():
line = "[ὦ] [παῖ] {τέ}[λος] [μὲν] [Ζεὺ]{ς ἔ}[χει] {βα}[ρύκ]{τυ}[πος]"
plain, labels = parse_scan_line(line)
assert plain == "ὦ παῖ τέλος μὲν Ζεὺς ἔχει βαρύκτυπος"
# letter ordinals: ὦ=0 π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
assert labels[0] == SCAN_HEAVY and labels[3] == SCAN_HEAVY
assert labels[5] == SCAN_LIGHT and labels[8] == SCAN_HEAVY
assert labels[14] == SCAN_HEAVY and labels[16] == SCAN_LIGHT # [Ζεὺ] ends at ὺ
assert labels[29] == SCAN_VERSE
assert max(labels) == 29
@needs_src
def test_scan_corpus_lines_encode():
ok = 0
with open(SRC / "data/scanner/corpus_v3.tsv", encoding="utf-8") as f:
for _ in range(500):
line = f.readline()
if not line:
break
work, _meter, bracketed = line.rstrip("\n").split("\t")
rec = encode_scan_line(bracketed)
if rec is None:
continue
ends = (rec.y_scan > 0).sum()
assert (rec.y_scan == SCAN_VERSE).sum() == 1
assert ends >= 2, bracketed
ok += 1
assert ok > 450
def test_concat_verses_boundaries():
r1 = encode_scan_line("[ὦ] [παῖ] {τέ}[λος]")
r2 = encode_scan_line("{βα}[ρύκ]{τυ}[πος]")
joined = concat_verses([r1, r2])
n1 = len(r1)
assert joined.boundary[n1 - 1] == 1 # seam demoted to word boundary
assert joined.boundary[-1] == 2 # record end keeps sentence boundary
assert (joined.y_scan == SCAN_VERSE).sum() == 2
def test_bracketize_roundtrip():
line = "[ὦ] [παῖ] {τέ}[λος] [μὲν] [Ζεὺ]{ς ἔ}[χει] {βα}[ρύκ]{τυ}[πος]"
plain, labels = parse_scan_line(line)
out = bracketize(plain, {k: v for k, v in labels.items()})
plain2, labels2 = parse_scan_line(out)
assert plain2 == plain
assert labels2 == labels
def test_enforce_circumflex_heavy_overrides_light():
# "πᾶς" (circumflex on alpha, closed by sigma): a syllable containing a
# circumflex is always heavy in Greek prosody, regardless of what the
# per-letter classifier predicted.
rec = encode_plain("πᾶς")
labels = np.zeros(len(rec.chars), dtype=np.int64)
labels[-1] = SCAN_LIGHT # simulates the model's wrong prediction
fixed = enforce_circumflex_heavy(rec.dia, labels)
assert fixed[-1] == SCAN_HEAVY
def test_enforce_circumflex_heavy_leaves_non_circumflex_alone():
rec = encode_plain("πολις")
labels = np.zeros(len(rec.chars), dtype=np.int64)
labels[-1] = SCAN_LIGHT
fixed = enforce_circumflex_heavy(rec.dia, labels)
assert fixed[-1] == SCAN_LIGHT
def test_merge_vowelless_syllables():
# "{λε}[ν]" -> "[λεν]": a vowel-less span ("ν" alone) can't be a real
# syllable -- fold it into the preceding one, keeping its own weight.
plain, labels = parse_scan_line("{λε}[ν]")
rec = encode_plain(plain)
arr = np.zeros(len(rec.chars), dtype=np.int64)
for k, v in labels.items():
arr[k] = v
fixed = merge_vowelless_syllables(rec.chars, arr)
out = bracketize(plain, {i: int(l) for i, l in enumerate(fixed) if l})
assert out == "[λεν]"
def test_merge_vowelless_syllables_leaves_real_syllables_alone():
plain, labels = parse_scan_line("[ὦ] [παῖ]")
rec = encode_plain(plain)
arr = np.zeros(len(rec.chars), dtype=np.int64)
for k, v in labels.items():
arr[k] = v
fixed = merge_vowelless_syllables(rec.chars, arr)
assert fixed.tolist() == arr.tolist()
# ---------------------------------------------------------------- norma gold
@needs_src
def test_norma_lines_parse():
n_mac = n_syl = 0
with open(SRC / "data/norma/test.jsonl", encoding="utf-8") as f:
for line in f:
d = json.loads(line)
if d["task"] == "macronize":
plain, labels = parse_macron_line(d["text"])
rec = encode_plain(plain)
if labels:
assert rec is not None and max(labels) < len(rec.chars), d
n_mac += 1
else:
parsed = parse_scan_line(d["text"])
assert parsed is not None, d
plain, labels = parsed
rec = encode_plain(plain)
assert rec is not None and max(labels) < len(rec.chars), d
n_syl += 1
assert n_mac == 932 and n_syl == 932