# Why no dither gets below ~0.08 L* with the painterly pass off: the target stores linear light, and
# the tone curve after it is convex near black, so a pixel dithered between two levels shows brighter,
# on average, than the level it stands for. This predicts that error exactly (the expected light of
# the two levels, weighted by how often each is picked) and sets it beside the measured one.
#   python3 floor.py   (out/floor.txt)
import numpy as np
import experiment as E, pipeline as P

lin = lambda d: np.where(d <= 0.04045, d / 12.92, ((d + 0.055) / 1.055) ** 2.4)
W = np.array([0.2126, 0.7152, 0.0722])
print('paint off, pixel ratio 2: band error (L*), the shipped dither measured vs predicted from the curve alone')
for s, sc in E.scenes().items():
    I = E.ideal(sc, 0, 2)
    o = E.render(sc, E.CANDIDATES['SHIP ign+ign-swap out'], 0, 2)
    t = np.zeros((E.FRAME, E.FRAME, 3))
    for kind, cx, cy, w, h, col, op, extra in sc:
        u, v, ins = P.quad_uv(E.FRAME, E.FRAME, cx, cy, w, h)
        t += np.where(ins[..., None], np.array(col)[None, None] * (E.layer_alpha(kind, u, v, extra) * op)[..., None], 0)
    c = t * 255; lo = np.floor(c); f = c - lo
    EY = ((1 - f) * lin(P.srgb(P.aces(lo / 255))) + f * lin(P.srgb(P.aces((lo + 1) / 255)))) @ W
    band = lambda Y: E.lstar_of(E.blur(Y, .187 * E.PPD[2] / 2)) - E.lstar_of(E.blur(E.luminance(I), .187 * E.PPD[2] / 2))
    m, p = band(E.luminance(o / 255.))[24:-24, 24:-24], band(EY)[24:-24, 24:-24]
    print(f'{s:11s} measured {np.sqrt((m ** 2).mean()):.3f} (mean {m.mean():+.3f})   predicted {np.sqrt((p ** 2).mean()):.3f} (mean {p.mean():+.3f})')
