File size: 3,129 Bytes
b360028
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Labels the challenge's Gemma extractor reads back in addition to the one a bullet names.

We ran 32 single-diagnosis reports through the extractor (google/gemma-4-31B-it,
temperature 0) and kept the extra labels that came back. Each link was checked
again inside a seven-bullet report; the one that did not hold there was dropped.

A report is therefore read as the closure of its bullets under these links.
When the number of bullets is limited, bullets that imply other predicted labels
are preferred.
"""
import numpy as np

IMPLIES = {
    "chronic_ischemic_stroke":     ["encephalomalacia_gliosis"],
    # lacunar_stroke -> chronic_ischemic_stroke + small_vessel_ischemic_disease held in a
    # single-diagnosis report but only 2 times in 6 inside a seven-bullet report, so it is not used.
    "intraparenchymal_hemorrhage": ["intracranial_hemorrhage"],
    "glioma":                      ["brain_tumor"],
    "brain_metastasis":            ["brain_tumor"],
    "meningioma":                  ["brain_tumor"],
    "intraventricular_tumor":      ["brain_tumor"],
    "pituitary_tumor":             ["brain_tumor"],
    "hydrocephalus_ex_vacuo":      ["cerebral_atrophy", "ventriculomegaly"],
    "craniotomy_craniectomy":      ["postsurgical_changes"],
    "tumor_resection_cavity":      ["postsurgical_changes"],
}


def closure_matrix(names):
    """C[p, c] = 1 if asserting p makes the extractor also report c (transitively)."""
    idx = {n: i for i, n in enumerate(names)}
    L = len(names)
    C = np.eye(L, dtype=bool)
    for p, cs in IMPLIES.items():
        if p not in idx:
            continue
        for c in cs:
            if c in idx:
                C[idx[p], idx[c]] = True
    for _ in range(L):                       # transitive closure (Floyd-Warshall style)
        new = C | (C @ C)
        if (new == C).all():
            break
        C = new
    return C


def apply_closure(asserted, C):
    """asserted: bool[n, L] of what we write; returns what the extractor reads."""
    return (asserted.astype(np.uint8) @ C.astype(np.uint8)) > 0


def select_with_implications(P, thr, cap, C):
    """Greedy choice of at most `cap` bullets, valued over the implication closure.

    A label's value is its margin over its threshold, `P/thr - 1`. A bullet is worth
    the total value of the labels it adds to the closure, so a parent whose children
    are also predicted is chosen first, and a parent that would add unpredicted
    children is penalised by their negative margin.
    """
    n, L = P.shape
    val = P / np.maximum(thr, 1e-9) - 1.0
    Cf = C.astype(np.float64)
    if not cap:
        return apply_closure(val > 0, C)
    have = np.zeros((n, L), dtype=bool)
    picked = np.zeros((n, L), dtype=bool)
    rows = np.arange(n)
    for _ in range(cap):
        gain = (val * ~have) @ Cf.T          # gain[i, d] = value d adds for study i
        gain[picked] = -np.inf
        d = gain.argmax(1)
        g = gain[rows, d]
        take = g > 1e-12
        picked[rows[take], d[take]] = True
        have[take] |= C[d[take]]
        if not take.any():
            break
    return picked