File size: 6,513 Bytes
99b82f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""Ink Lens: the control-set threshold and the region status (numpy only).

The rule, frozen in docs/prereg/2026-10-20-ink-lens-gate.md before any map of it existed:

- F and R are the mean ink probabilities (0 to 1) of the region with its layers as stored and reversed, each averaged
  over the same depth windows and readers.
- Papyrus: pixels whose middle CT layer is above the region's 5th percentile of CT and above 0 (so air and the
  masked outside of a segment do not lower the threshold).
- T is the 99th percentile of R over the papyrus pixels: the level that only 1 % of the control's own pixels pass.
- Kept ink: papyrus pixels with F > T. Excess: the fraction of papyrus pixels with F > T, divided by 0.01.
- Status: ink when excess >= 3, weak when 1.5 <= excess < 3, none below 1.5, too_small under 100,000 papyrus pixels
  (about 3 mm square at 9.6 um), unreadable when a map is not finite, constant, or identical to its control.

A region with no text, or where the model reads the reversed volume as well as the stored one, has an excess near 1
and the status none: the check can fail. Everything it keeps is model output, not a reading.
"""

from __future__ import annotations

RULE_ID = "ink-lens-gate-v1"
CT_PERCENTILE = 5.0
CONTROL_PERCENTILE = 99.0
BASE_RATE = 0.01
INK_EXCESS = 3.0
WEAK_EXCESS = 1.5
MIN_PAPYRUS = 100_000
STATUSES = ("ink", "weak", "none", "too_small", "unreadable")
STATUS_TEXT = {
    "ink": "ink above the control: at least 3 times as many pixels pass the control's 99th percentile as the control",
    "weak": "some ink above the control: 1.5 to 3 times the control's share; look before trusting it",
    "none": "no ink above the control: the stored order passes the control's 99th percentile no more than 1.5 times "
            "as often as the control itself",
    "too_small": "too little papyrus in the region to judge (under 100,000 pixels, about 3 mm square)",
    "unreadable": "the maps cannot be judged (not finite, constant, or the same as the control)",
}


class GateError(ValueError):
    """Maps the gate cannot take at all (wrong shapes); the message says why."""


def papyrus(ct_middle, percentile=CT_PERCENTILE):
    """Boolean mask of papyrus pixels in a region: middle CT layer above its own percentile and above 0."""
    import numpy as np
    ct = np.asarray(ct_middle)
    if ct.ndim != 2:
        raise GateError(f"the CT layer is 2-D, not {ct.shape}")
    nz = ct[ct > 0]
    if nz.size == 0:
        return np.zeros(ct.shape, bool)
    floor = max(float(np.percentile(ct, percentile)), 0.0)
    return ct > floor


def _unreadable(F, R, mask):
    import numpy as np
    reasons = []
    for name, m in (("stored-order map", F), ("control map", R)):
        if not np.all(np.isfinite(m)):
            reasons.append(f"the {name} has values that are not finite")
        elif mask.any() and float(m[mask].max()) - float(m[mask].min()) < 1e-6:
            reasons.append(f"the {name} is constant on the papyrus")
    if not reasons and mask.any() and np.array_equal(F[mask], R[mask]):
        reasons.append("the stored-order map is identical to its control (a broken run, or the control was not run)")
    return reasons


def decide(F, R, ct_middle, min_papyrus=MIN_PAPYRUS):
    """The gate on one region: threshold, excess, status and counts, as a JSON-able dict."""
    import numpy as np
    F = np.asarray(F, dtype=np.float32)
    R = np.asarray(R, dtype=np.float32)
    if F.shape != R.shape or F.ndim != 2:
        raise GateError(f"the map {F.shape} and its control {R.shape} must be 2-D and the same shape")
    mask = papyrus(ct_middle)
    if mask.shape != F.shape:
        raise GateError(f"the CT layer {mask.shape} and the maps {F.shape} differ in shape")
    n = int(mask.sum())
    out = {"rule": RULE_ID, "papyrus_px": n, "region_px": int(mask.size), "threshold": None, "excess": None,
           "forward_above": None, "control_above": None, "status": None}
    reasons = _unreadable(F, R, mask)
    if reasons:
        out.update(status="unreadable", reasons=reasons)
    elif n < min_papyrus:
        out.update(status="too_small")
    else:
        T = float(np.percentile(R[mask], CONTROL_PERCENTILE))
        f_above = float((F[mask] > T).mean())
        r_above = float((R[mask] > T).mean())
        excess = f_above / BASE_RATE
        status = "ink" if excess >= INK_EXCESS else "weak" if excess >= WEAK_EXCESS else "none"
        out.update(threshold=round(T, 6), excess=round(excess, 4), forward_above=round(f_above, 6),
                   control_above=round(r_above, 6), status=status)
    out["status_text"] = STATUS_TEXT[out["status"]]
    out["note"] = "Model output, not a reading."
    return out


def kept(F, decision, ct_middle):
    """F where it passes the control's threshold on papyrus, else 0 (float32). Nothing passes unless the status is
    ink or weak."""
    import numpy as np
    F = np.asarray(F, dtype=np.float32)
    out = np.zeros_like(F)
    if decision.get("status") not in ("ink", "weak"):
        return out
    m = papyrus(ct_middle) & (F > float(decision["threshold"]))
    out[m] = F[m]
    return out


def to_u8(kept_map):
    """The kept map as uint8 for an overlay: 0 where nothing is kept (transparent in VC3D), else 1 to 255."""
    import numpy as np
    k = np.asarray(kept_map, dtype=np.float32)
    out = np.zeros(k.shape, np.uint8)
    on = k > 0
    out[on] = np.clip(np.rint(k[on] * 254) + 1, 1, 255).astype(np.uint8)
    return out


def label_shares(kept_map, ink, supervised, inner=64):
    """The preregistered label check: the share of labeled ink pixels kept and the share of labeled background
    pixels kept, inside the supervision mask with an inner edge left out."""
    import numpy as np
    k = np.asarray(kept_map) > 0
    m = np.asarray(supervised, bool).copy()
    if inner:
        m[:inner] = m[-inner:] = False
        m[:, :inner] = m[:, -inner:] = False
    ink = np.asarray(ink, bool)
    ink_px, bg_px = int((m & ink).sum()), int((m & ~ink).sum())
    ink_share = float((k & m & ink).sum()) / ink_px if ink_px else None
    bg_share = float((k & m & ~ink).sum()) / bg_px if bg_px else None
    passes = None if ink_share is None or bg_share is None else ink_share > bg_share
    return {"ink_px": ink_px, "background_px": bg_px, "ink_kept_share": None if ink_share is None else round(ink_share, 5),
            "background_kept_share": None if bg_share is None else round(bg_share, 5), "ink_share_larger": passes,
            "inner": inner}