devmandan's picture
QR Scan Difficulty Check
b43c3a8 verified
Raw History Blame Contribute Delete
2.38 kB
"""Place a picture on QR Scan Difficulty's scale from the five scanners' reads.
The page's scale comes from a two-parameter item-response fit of all eight scanners at seven frame sizes: each
(scanner, frame size) pair reads a code of difficulty b with chance 1 / (1 + exp(-a (theta - b))). Here the 35 curves
of the five scanners that run on Linux are frozen as fitted (model.json), and a picture's b is the fit's own estimate
for one code from its 35 reads: the most likely b under the fit's prior (normal, mean 0, sd 2.5). The log-posterior
has one peak (its slope falls as b grows), so bisection on the slope finds it; the fit's clipped Newton step can
bounce between two points when the readers are held fixed. A picture every pair read sits with the codes every
scanner read at every size (the left pile); one no pair read sits with the codes no scanner read (the right pile).
Otherwise d = 100 (b - lo) / (hi - lo), clipped to 0..100.
"""
import json
from pathlib import Path
import numpy as np
MODEL = json.loads(Path(__file__).with_name("model.json").read_text())
PILE_LO, PILE_HI = -6, 106
def map_b(y: np.ndarray, theta: np.ndarray, a: np.ndarray, prior_b: float) -> float:
"""The most likely b given reads y by readers (theta, a): where the log-posterior's slope, the fit's gradient,
is zero."""
def slope(b: float) -> float:
p = 1.0 / (1.0 + np.exp(-np.clip(a * (theta - b), -30, 30)))
return float(-(a * (y - p)).sum() - b / prior_b ** 2)
lo, hi = -30.0, 30.0 # slope > 0 at lo and < 0 at hi for any reads
for _ in range(80):
mid = (lo + hi) / 2
if slope(mid) > 0:
lo = mid
else:
hi = mid
return (lo + hi) / 2
def place(reads: dict[str, list[int]], frames: list[int]) -> dict:
"""{"d": difficulty, "pile": "lo" | "hi" | None} from {scanner: [read at each frame size]}."""
order = [frames.index(f) for f in MODEL["frames"]]
y = np.array([reads[k][i] for k in MODEL["decoders"] for i in order], dtype=float)
if y.all():
return {"d": PILE_LO, "pile": "lo"}
if not y.any():
return {"d": PILE_HI, "pile": "hi"}
b = map_b(y, np.array(MODEL["theta"]).ravel(), np.array(MODEL["a"]).ravel(), MODEL["prior_b"])
d = 100 * (b - MODEL["lo"]) / (MODEL["hi"] - MODEL["lo"])
return {"d": round(min(100.0, max(0.0, d)), 1), "pile": None}