Symbolic Rendering

Chapter 10 — Motion & Depth, or the Camera Gains a Clock and a Pupil

An explainer comparing numerical rendering with symbolic rendering,
in the spirit of Utah graphics meets Macsyma (today: Python + sympy).
Dr. L. Van Warren — Original Idea & Mathematical Development
Kimi K3 — Assistance with Mathematical Development
Python-ready for Jupyter Notebooks in Google Colab.
Requires an internet connection for mathematical typesetting (MathJax).
How to use this file. §1 what changes; §2 the scene; §3 the derivation — the pupil camera, the switching-time quadratic, the bokeh scaling law. §4 is the live simulator: numeric jitters (time × aperture), symbolic stratifies the aperture and integrates B's visibility exactly in t. §5: Cells 1 and 3 replace Chapter 9's; Cells 2, 4–8 are new. The disk light rests this chapter (its integral already stacked; the appendix stacks everything). §6–§9 as ever.
☰  contents
  1. What changes in Chapter 10
  2. The scene (Chapter-10 assumptions)
  3. The math, once, carefully
  4. Live simulator
  5. The Colab laboratory (Python / sympy)
  6. The honest ledger — Chapter-10 deltas
  7. Pitfalls gallery, continued
  8. A brief history, continued
  9. Roadmap

§1 · What changes in Chapter 10

Chapter 9's scene stands (textures, exact-gradient bump off by default, sign-logic shadows). Two camera extensions, one moving object:

  1. The pinhole becomes a pupil. A disk aperture of radius $a$ at $z = e$; rays from aperture point $q$ through the pixel's focus-conjugate point. Focus distance $f$ is a slider; the bokeh theorem (§3) prices every defocused point exactly.
  2. The frame becomes an interval. The shutter opens for $t \in [0,1]$; sphere B moves with velocity $\mathbf v$. Its visibility mask per pixel has closed-form switching times — the Shadow Lemma, reborn in time — so the symbolic side integrates B's motion exactly while numeric jitters it.
  3. The integral is now 5-D: pixel² × time × aperture². Both pipelines spend the same budget; the currencies remain variance (numeric) versus bias (symbolic) — with the symbolic side newly armed in the time dimension.

§2 · The scene (Chapter-10 assumptions)

Chapter 5's base with point lights (the disk light rests; see §7 and Cell 8). Sphere B starts at $C_0$ and moves linearly: $C(t) = C_0 + \mathbf v\,t$, $t \in [0,1]$ (default velocity carries it across the frame and toward the camera — its shadow moves too, and both pipelines track it). The camera: aperture radius $a \ge 0$, focus distance $f \gt 0$. Series units as ever; shading frozen at sub-interval midpoints where the symbolic side integrates visibility exactly — confessed in §7, bounded in Cell 7.

§3 · The math, once, carefully

The pupil camera. Aperture point $q = (q_x, q_y, e)$, $|q| \le a$; the pixel $(x, y)$'s chief ray meets the focus plane $z = f$ at $F = \big(x\tfrac{e-f}{e},\; y\tfrac{e-f}{e},\; f\big)$; the sampled ray is $P(s) = q + s\,(F - q)$. At $a = 0$ this is exactly Chapter 1's pinhole ray — the regression is structural. The floor is met at $s = e/(e-f)$; sphere quadratics are standard in the ray parameter $s$.

Theorem (Bokeh = scaled aperture). A scene point $X$ at depth $z_0$ is recorded by aperture point $q$ at sensor position $S(q)$, and $$S(q) = \frac{eX - z_0\, q}{e - z_0} \quad\Longrightarrow\quad \text{the footprint is a disk of radius } \rho_{\text{CoC}} = a\,\frac{e\,|f - z_0|}{(e - z_0)(e - f)},$$ an exact scaled copy of the aperture, centered on the pinhole image. Proof: similar triangles on the line through $X$ and $q$; Cell 2 makes sympy do it and Cell 5 traces it. Polygonal or ring apertures would inherit their shapes verbatim — bokeh shape is the aperture's shape, by linearity. ∎

Theorem (Switching times are a quadratic). For a fixed ray and $C(t) = C_0 + \mathbf v t$, the hit discriminant is $\Delta(t) = b(t)^2 - A\,c(t)$ with $b$ linear and $c$ quadratic in $t$ — so $\Delta(t) = \alpha t^2 + \beta t + \gamma$ is quadratic in time, and the hit set $\{\Delta \ge 0\}$ is an interval $[t_-, t_+]$ (or empty). The shutter integral of B's visibility mask is therefore $\big|[t_-, t_+] \cap [0,1]\big|$ — exact, closed form, no time samples. Proof: collect powers of $t$; Cell 2 prints $\alpha, \beta, \gamma$ and verifies the interval against a 2000-point scan. ∎

Corollary (The motion footprint is algebraic). The swept silhouette in the image plane is the envelope $\{\exists t : \Delta(x, y, t) = 0\}$ — eliminate $t$ from a quadratic: the outline is algebraic of degree ≤ 4 (Cell 5 draws it over the render). Every boundary in this book keeps its polynomial passport.

Three gifts of Chapter 10: (1) exact temporal coverage — the one dimension where the symbolic side gets a closed-form mask, deployed live; (2) the bokeh law, so defocus is priced per pixel before a ray is cast; (3) two regressions on sliders — $a \to 0$ and $\mathbf v \to 0$ must return Chapter 5, and do (Cell 6). Counterpoint: exactness covers visibility, not shading: within a hit interval the lighting still varies, and we evaluate it at the midpoint (bounded in Cell 7). B's moving shadow has quartic switching times (Cell 8) — stratified in time on both pipelines. And the ordering of a moving B against static A is decided at interval midpoints: the honest approximations are listed, not hidden.

§4 · Live simulator

Sphere B sweeps across the frame while the pupil opens. Numeric jitters (time × aperture); symbolic stratifies the aperture and integrates B's visibility exactly in t. The motion view paints B's time-coverage field; the depth view paints the bokeh law. Both regressions live on sliders.

model 
resolution 
initializing…
NUMERIC — jittered time × aperture (Cook's doctrine)
SYMBOLIC — stratified aperture × exact time coverage
|difference| ×10² — numeric variance mottles the motion trail; the derived side is smooth
numeric: symbolic: RMS |ΔI|: max |ΔI|: surface flips:
hover — pixel → world, surface, B's switching interval [t₋, t₊], bokeh radius, both RGB triples.

Try this: (1) aperture a to 0 and velocity to 0: both panels return Chapter 5's still frame — two regressions on two sliders. (2) a = 0.35, focus f = 2.2: B snaps sharp and A softens; the depth view shows the bokeh radius law per pixel. (3) velocity up, numeric time samples = 1: the motion trail strobes (wagon-wheel); symbolic's exact coverage stays smooth — that smoothness is a closed form, not luck. (4) motion view: B's coverage field is a capsule with exact algebraic edges; the numeric coverage field dithers the same shape.

§5 · The Colab laboratory (Python / sympy)

Cumulative: Cells 1 and 3 replace Chapter 9's; Cells 2, 4–8 are new. Three run paths per cell — 📋 Copy code, ⬇ notebook (File → Upload notebook in Colab), or paste into colab.new.

🐍 Cell 1 (REPLACES Ch.9 Cell 1) — numeric baseline WITH aperture and shutter: jittered time × aperture

· · paste into colab.new → Run
import numpy as np
import matplotlib.pyplot as plt
import math, time

SCX = dict(R=1.0, e=4.0, Cb=(0.5, 0.5, 1.3), R2=0.32,
           vel=(-0.55, -0.35, 0.35), b_on=True, floor_on=True,
           lights=(((5., 5., 10.), (1., 1., 1.)),
                   ((-6., 2., 8.), (0.9, 0.65, 0.25))),
           ambient=(0.15, 0.15, 0.16), matA=(0.63, 0.19, 0.16), matB=(0.33, 0.44, 0.62),
           ka=0.1, kd=0.7, ks=0.5, shin=32.0, model='blinn',
           alpha=0.25, ku=8.0, kv=4.0, tex='checker',
           aperture=0.18, focus=1.0, Ka=2, Kt=2, seed=7)

def srgb_encode(c):
    c = np.maximum(np.asarray(c, dtype=float), 0.0)
    return np.where(c <= 0.0031308, 12.92*c, 1.055*np.power(c, 1/2.4) - 0.055)

def chartA1(P, R):
    Nx, Ny, Nz = P[0]/R, P[1]/R, P[2]/R
    return ((math.atan2(Ny, Nx) + math.pi)/(2*math.pi),
            math.acos(max(-1.0, min(1.0, Nz)))/math.pi)

def texA1(u, v, SC=SCX):
    m = SC['matA']
    if SC['tex'] == 'solid': return np.array(m)
    if SC['tex'] == 'stripes':
        b = 1.0 if math.sin(math.pi*u*SC['ku']) >= 0 else 0.2
    elif SC['tex'] == 'marble':
        b = 0.55 + 0.45*(0.5 + 0.5*math.sin(2*math.pi*(SC['ku']*u
                 + 0.35*math.sin(3*math.pi*SC['kv']*v))))
    else:
        b = 1.0 if math.sin(math.pi*u*SC['ku'])*math.sin(math.pi*v*SC['kv']) >= 0 else 0.2
    return np.array((m[0]*b, m[1]*b, m[2]*b))

def floor_tex1(x, y, SC=SCX):
    s = 1.0 if math.sin(math.pi*x)*math.sin(math.pi*y) >= 0 else -1.0
    base = 0.85 if s > 0 else 0.15
    return np.array((base*(1-SC['alpha']), base*(1-SC['alpha']), base))

def shadowed1(P, Q, C, Rr):
    seg = Q - P
    a2 = float(seg @ seg); b2 = float((P - C) @ seg)
    c2 = float((P - C) @ (P - C)) - Rr*Rr
    if a2 < 1e-18: return False
    dsc = b2*b2 - a2*c2
    if dsc >= 0:
        sq = math.sqrt(dsc); s1, s2 = (-b2 - sq)/a2, (-b2 + sq)/a2
        if (0 < s1 < 1) or (0 < s2 < 1): return True
    return False

def trace_ray(qx, qy, x, y, t, SC=SCX):
    """One ray of the pupil camera at time t. Returns (rgb, kind, z_hit, fB)."""
    e, f = SC['e'], SC['focus']
    sF = (e - f)/e
    Fx, Fy = x*sF, y*sF
    Dx, Dy, Dz = Fx - qx, Fy - qy, f - e
    R, R2 = SC['R'], SC['R2']
    Cb = np.array(SC['Cb']) + t*np.array(SC['vel'])
    Aq = Dx*Dx + Dy*Dy + Dz*Dz
    # sphere A (static)
    bA = qx*Dx + qy*Dy + e*Dz
    cA = qx*qx + qy*qy + e*e - R*R
    dA = bA*bA - Aq*cA
    tA = (-bA - math.sqrt(dA))/Aq if dA >= 0 else math.inf
    # sphere B (moving)
    tB = math.inf
    if SC['b_on']:
        Ex, Ey, Ez = qx - Cb[0], qy - Cb[1], e - Cb[2]
        bB = Ex*Dx + Ey*Dy + Ez*Dz
        cB = Ex*Ex + Ey*Ey + Ez*Ez - R2*R2
        dB = bB*bB - Aq*cB
        if dB >= 0: tB = (-bB - math.sqrt(dB))/Aq
    # floor z=0
    tF = -e/Dz if (SC['floor_on'] and Dz < 0) else math.inf
    tm = min(tA, tB, tF)
    if tm == math.inf: return np.zeros(3), 0, math.inf, 0.0
    P = np.array([qx + tm*Dx, qy + tm*Dy, e + tm*Dz])
    if tm == tA:
        N = P/R; alb = texA1(*chartA1(P, R), SC)
        blockers = [(Cb, R2)] if SC['b_on'] else []; kind = 1
    elif tm == tB:
        N = (P - Cb)/R2; alb = np.array(SC['matB'])
        blockers = [(np.zeros(3), R)]; kind = 2
    else:
        N = np.array([0., 0., 1.]); alb = floor_tex1(P[0], P[1], SC)
        blockers = [(np.zeros(3), R)] + ([(Cb, R2)] if SC['b_on'] else []); kind = 3
    v_hat = -np.array([Dx, Dy, Dz]); v_hat /= np.linalg.norm(v_hat)
    col = SC['ka']*np.array(SC['ambient'])
    for pos, color in SC['lights']:
        if color is None: continue
        Lp = np.array(pos)
        if any(shadowed1(P, Lp, C, Rr) for C, Rr in blockers): continue
        w = Lp - P; w_hat = w/np.linalg.norm(w)
        mu = float(N @ w_hat)
        if SC['model'] == 'phong':
            r = 2.0*mu*N - w_hat; spec = max(0.0, float(r @ v_hat))**SC['shin']
        else:
            h = w_hat + v_hat; h = h/np.linalg.norm(h)
            spec = max(0.0, float(N @ h))**SC['shin']
        col += np.array(color)*(SC['kd']*alb*max(0.0, mu) + SC['ks']*spec)
    return col, kind, P[2], 1.0 if kind == 2 else 0.0

def render_numeric_md(width, height, SC=SCX):
    """Numeric: jittered (aperture Ka^2) x (time Kt) -- distributed ray tracing."""
    rng = np.random.default_rng(SC['seed'])
    img = np.zeros((height, width, 3)); code = np.zeros((height, width), np.uint8)
    fB = np.zeros((height, width)); zz = np.full((height, width), np.nan)
    half = 1.25*SC['e']*SC['R']/np.sqrt(SC['e']**2 - SC['R']**2)
    Ka, Kt = SC['Ka'], SC['Kt']
    for j in range(height):
        y = half*(1 - 2*(j + 0.5)/height) - 0.15*half
        for i in range(width):
            x = half*(2*(i + 0.5)/width - 1)
            acc = np.zeros(3); fb = 0.0; ns = 0
            for sj in range(Ka):
                for si in range(Ka):
                    for sk in range(Kt):
                        u = (si + rng.random())/Ka; v = (sj + rng.random())/Ka
                        r = SC['aperture']*math.sqrt(u); th = 2*math.pi*v
                        qx, qy = r*math.cos(th), r*math.sin(th)
                        t = (sk + rng.random())/Kt
                        c, kd_, z, fb_ = trace_ray(qx, qy, x, y, t, SC)
                        acc += c; fb += fb_; ns += 1
                        if si == 0 and sj == 0 and sk == 0:
                            code[j, i] = kd_; zz[j, i] = z
            img[j, i] = acc/ns; fB[j, i] = fb/ns
    return np.clip(img, 0, 1), code, fB, zz

t0 = time.perf_counter()
img_num, code_num, fB_num, zz_num = render_numeric_md(256, 256)
t1 = time.perf_counter()
print(f"numeric render (aperture {SCX['Ka']}x{SCX['Ka']} x time {SCX['Kt']}): {t1-t0:.2f} s")
plt.figure(figsize=(4, 4)); plt.imshow(srgb_encode(img_num))
plt.title("Numeric: moving B, open pupil"); plt.axis('off'); plt.show()
Expect: B smeared along its velocity with a soft defocus halo; speckle in the trail at low sample counts.

🐍 Cell 2 (NEW) — the two theorems: the switching-time quadratic, and the bokeh scaling law

· · paste into colab.new → Run
import sympy as sp

# --- (a) Delta(t) for a moving sphere is QUADRATIC in t ---------------------------
t = sp.symbols('t', real=True)
x, y, e = sp.symbols('x y e', real=True)
bx, by, bz = sp.symbols('b_x b_y b_z', real=True)
vx, vy, vz = sp.symbols('v_x v_y v_z', real=True)
R2 = sp.Symbol('R_2', positive=True)
D = sp.Matrix([x, y, -e])                     # pinhole ray (aperture a = 0)
E0 = sp.Matrix([0, 0, e])
Ct = sp.Matrix([bx + vx*t, by + vy*t, bz + vz*t])
Aq = D.dot(D)
bb = (E0 - Ct).dot(D)
cc = (E0 - Ct).dot(E0 - Ct) - R2**2
Delta = sp.expand(bb**2 - Aq*cc)
poly = sp.Poly(Delta, t)
print("Delta(t) coefficients:")
for k in (2, 1, 0):
    print(f"  t^{k}:", sp.simplify(poly.nth(k)))
print("degree in t:", sp.Poly(Delta, t).degree(), "  <- switching times solve a QUADRATIC")

# verify the hit interval against a brute scan
def hit_interval(xv, yv, SC=SCX):
    C0 = np.array(SC['Cb']); vv = np.array(SC['vel'])
    Dv = np.array([xv, yv, -SC['e']]); Aq2 = float(Dv @ Dv)
    b0 = float(-(C0) @ Dv + np.array([0,0,SC['e']]) @ Dv)
    vd = float(-vv @ Dv)
    w0 = np.array([0, 0, SC['e']]) - C0
    alpha = vd*vd - Aq2*float(vv @ vv)
    beta = -2*b0*vd + 2*Aq2*float(w0 @ vv)
    gamma = b0*b0 - Aq2*(float(w0 @ w0) - SC['R2']**2)
    dsc = beta*beta - 4*alpha*gamma
    if dsc < 0 or abs(alpha) < 1e-14: return None
    sq = math.sqrt(dsc)
    lo, hi = sorted(((-beta - sq)/(2*alpha), (-beta + sq)/(2*alpha)))
    if alpha > 0:  # Delta >= 0 outside [lo, hi] -- grazing geometry; take overlap pieces
        iv = []
        if lo > 0: iv.append((0, min(1, lo)))
        if hi < 1: iv.append((max(0, hi), 1))
        return iv
    lo, hi = max(0.0, lo), min(1.0, hi)
    return [(lo, hi)] if hi > lo else []

rng = np.random.default_rng(1)
worst = 0.0
for _ in range(300):
    xv, yv = rng.uniform(-1, 1, 2)
    iv = hit_interval(xv, yv)
    ts = np.linspace(0, 1, 2000)
    Dv = np.array([xv, yv, -SCX['e']])
    def hit(tt):
        C0 = np.array(SCX['Cb']) + tt*np.array(SCX['vel'])
        w0 = np.array([0, 0, SCX['e']]) - C0
        bb2 = float(w0 @ Dv); cc2 = float(w0 @ w0) - SCX['R2']**2
        return bb2*bb2 - float(Dv @ Dv)*cc2 >= 0
    scan = np.mean([hit(tt) for tt in ts])
    exact = sum(hi - lo for lo, hi in iv) if iv else 0.0
    worst = max(worst, abs(scan - exact))
print(f"\nswitching-time coverage vs 2000-pt scan, 300 random pixels: "
      f"worst |delta| = {worst:.2e}")

# --- (b) THE BOKEH LAW ------------------------------------------------------------
qx = sp.symbols('q_x', real=True)
z0, f, aa = sp.symbols('z_0 f a', positive=True)
Xx, Xz = sp.symbols('X_x', real=True), z0
# sensor position of ray from aperture point (qx) through X (1-D section):
Sq = (e*Xx - z0*qx)/(e - z0)           # derived in section 3 by similar triangles
spread = sp.simplify(Sq - Sq.subs(qx, 0))
print("\nbokeh spread from aperture offset q_x:", spread)
print("=> |S(q)-S(0)| = a * z0/(e - z0) at the sensor for focus at the sensor;")
print("   with focus at f the footprint radius is  rho = a*e*|f - z0| / ((e - z0)*(e - f))")
# verify by tracing a point through a grid of aperture samples:
def coc_measured(X, f_v, a_v, SC=SCX):
    e_v = SC['e']
    pts = []
    for qxv in np.linspace(-a_v, a_v, 25):
        for qyv in np.linspace(-a_v, a_v, 25):
            if qxv**2 + qyv**2 > a_v*a_v: continue
            # sensor pixel p whose focus-conjugate ray from q passes through X:
            # chief point F = p*(e-f)/e; collinearity of q, F, X:
            lam = (e_v - X[2])/(f_v - X[2])
            F = np.array(X) + ((e_v - X[2])/(f_v - X[2]))**-1*(np.array([qxv, qyv, e_v]) - np.array(X))
            p = F[:2]*e_v/(e_v - f_v)
            pts.append(p)
    pts = np.array(pts)
    return np.abs(pts - pts.mean(axis=0)).max()
for z0v in (0.8, 1.5, 2.5):
    fv, av = 1.0, 0.25
    meas = coc_measured(np.array([0.3, -0.2, z0v]), fv, av)
    pred = av*SCX['e']*abs(fv - z0v)/((SCX['e'] - z0v)*(SCX['e'] - fv))
    print(f"  z0={z0v}: bokeh radius measured {meas:.4f} vs law {pred:.4f}  "
          f"(ratio {meas/pred:.3f} -- grid quantization bounds the measure)")
Expect: α, β, γ printed (Δ quadratic in t); interval coverage agreeing with the scan to ~1e-3 (scan resolution); bokeh law matching traced footprints within grid quantization.

🐍 Cell 3 (REPLACES Ch.9 Cell 3) — the symbolic renderer: stratified aperture × exact time coverage; the census

· · paste into colab.new → Run
def render_symbolic_md(width, height, SC=SCX, time_mode='exact'):
    """Symbolic: aperture strata (centroids, deterministic). Time: EXACT visibility
       for sphere B via switching times (shading at interval midpoint, confessed);
       static surfaces get Kt=2 time strata for B's moving shadow (quartic -- Cell 8)."""
    img = np.zeros((height, width, 3)); code = np.zeros((height, width), np.uint8)
    fB = np.zeros((height, width)); zz = np.full((height, width), np.nan)
    half = 1.25*SC['e']*SC['R']/np.sqrt(SC['e']**2 - SC['R']**2)
    Ka = SC['Ka']
    for j in range(height):
        y = half*(1 - 2*(j + 0.5)/height) - 0.15*half
        for i in range(width):
            x = half*(2*(i + 0.5)/width - 1)
            acc = np.zeros(3); fb = 0.0; ns = 0
            for sj in range(Ka):
                for si in range(Ka):
                    u = (si + .5)/Ka; v = (sj + .5)/Ka
                    r = SC['aperture']*math.sqrt(u); th = 2*math.pi*v
                    qx, qy = r*math.cos(th), r*math.sin(th)
                    if time_mode == 'exact' and SC['b_on']:
                        # static background at 2 time strata (moving shadow)
                        c0, k0, z0, _ = trace_ray(qx, qy, x, y, 0.25, SC)
                        c1, k1, z1, _ = trace_ray(qx, qy, x, y, 0.75, SC)
                        # B's exact interval for this ray
                        e_, f_ = SC['e'], SC['focus']
                        sF = (e_ - f_)/e_
                        D = np.array([x*sF - qx, y*sF - qy, f_ - e_])
                        C0 = np.array(SC['Cb']); vv = np.array(SC['vel'])
                        w0 = np.array([qx, qy, e_]) - C0
                        Aq2 = float(D @ D)
                        b0 = float(w0 @ D); vd = float(-vv @ D)
                        alpha2 = vd*vd - Aq2*float(vv @ vv)
                        beta2 = -2*b0*vd + 2*Aq2*float(w0 @ vv)
                        gamma2 = b0*b0 - Aq2*(float(w0 @ w0) - SC['R2']**2)
                        dsc = beta2*beta2 - 4*alpha2*gamma2
                        cov = 0.0; cBcol = np.zeros(3); kw = k0; zw = z0
                        if dsc > 0 and abs(alpha2) > 1e-14:
                            sq = math.sqrt(dsc)
                            lo, hi = sorted(((-beta2 - sq)/(2*alpha2),
                                             (-beta2 + sq)/(2*alpha2)))
                            if alpha2 < 0:
                                lo, hi = max(0.0, lo), min(1.0, hi)
                                if hi > lo:
                                    tm = 0.5*(lo + hi)
                                    cb_, kb_, zb_, _ = trace_ray(qx, qy, x, y, tm, SC)
                                    if kb_ == 2:      # B wins at the midpoint
                                        cov = hi - lo
                                        cBcol = cb_; kw = 2; zw = zb_
                        cst = 0.5*(np.array(c0) + np.array(c1))
                        acc += (1 - cov)*cst + cov*cBcol
                        fb += cov
                        if si == 0 and sj == 0: code[j, i] = kw; zz[j, i] = zw
                    else:
                        for sk in range(SC['Kt']):
                            tt = (sk + .5)/SC['Kt']
                            c, kd_, z, fb_ = trace_ray(qx, qy, x, y, tt, SC)
                            acc += c; fb += fb_
                            if si == 0 and sj == 0 and sk == 0:
                                code[j, i] = kd_; zz[j, i] = z
                    ns += 1
            img[j, i] = acc/ns; fB[j, i] = fb/ns
    return np.clip(img, 0, 1), code, fB, zz

t0 = time.perf_counter()
img_sym, code_sym, fB_sym, zz_sym = render_symbolic_md(256, 256)
t1 = time.perf_counter()
print(f"symbolic render (stratified aperture, exact time): {t1-t0:.2f} s")

same = code_num == code_sym
d = np.abs(img_num - img_sym)[same]
print(f"interior max  |num - sym| = {d.max():.3e}")
print(f"interior RMS  |num - sym| = {np.sqrt((d**2).mean()):.3e}")
print(f"surface flips: {(~same).sum()} px   (the trail's edges, where t-sampling lives)")
dfB = np.abs(fB_num - fB_sym)
print(f"motion coverage: mean |dfB| = {dfB.mean():.4f}  max {dfB.max():.4f}")

fig, axs = plt.subplots(1, 3, figsize=(13, 4))
axs[0].imshow(srgb_encode(img_num)); axs[0].set_title("Numeric (jittered)")
axs[1].imshow(srgb_encode(img_sym)); axs[1].set_title("Symbolic (exact time)")
axs[2].imshow(np.clip(np.abs(img_num - img_sym)*30, 0, 1))
axs[2].set_title("|delta| x30: the trail's edge mottles")
for ax in axs: ax.axis('off')
plt.show()
Expect: the symbolic trail smooth where the numeric one dithers; flips concentrated on the swept outline; mean coverage agreement with numeric variance visible around it.

🐍 Cell 4 (NEW) — honest timings: the 5-D product, priced

· · paste into colab.new → Run
print(f"{'Ka':>4} {'Kt':>4} {'numeric 64^2':>14} {'symbolic 64^2':>15}")
for Ka, Kt in ((1, 1), (2, 2), (2, 4), (4, 4)):
    t0 = time.perf_counter(); render_numeric_md(64, 64, {**SCX, 'Ka': Ka, 'Kt': Kt})
    t1 = time.perf_counter(); render_symbolic_md(64, 64, {**SCX, 'Ka': Ka, 'Kt': Kt})
    t2 = time.perf_counter()
    print(f"{Ka:>4} {Kt:>4} {t1-t0:>13.2f}s {t2-t1:>14.2f}s")
print("""
Reading: numeric scales as Ka^2 x Kt (every sample is a full trace). Symbolic
scales as Ka^2 x (2 static strata + one quadratic solve + at most one extra
trace for B's midpoint): exact time coverage is CHEAPER than sampling past Kt=2
for this scene -- the closed form pays rent. The product integral's cost is now
visible in one table: pixel^2 (frame) x aperture^2 (Ka) x time (Kt) -- and the
time dimension is the one where derivation replaced sampling.""")
Expect: numeric time growing with Ka²·Kt; symbolic flatter in Kt — the closed-form time dimension, priced.

🐍 Cell 5 (NEW) — superpowers: the motion footprint's algebraic outline, drawn; bokeh shape traced

· · paste into colab.new → Run
# (a) THE SWEPT SILHOUETTE IS ALGEBRAIC: eliminate t from Delta(x, y, t) = 0.
#     Delta is quadratic in t: envelope = discriminant-in-t = 0.
xv, yv = sp.symbols('x y', real=True)
sub = {e: SCX['e'], bx: SCX['Cb'][0], by: SCX['Cb'][1], bz: SCX['Cb'][2],
       vx: SCX['vel'][0], vy: SCX['vel'][1], vz: SCX['vel'][2], R2: SCX['R2']}
Dxy = sp.Matrix([xv, yv, -SCX['e']])
E0v = sp.Matrix([0, 0, SCX['e']])
Ctv = sp.Matrix([SCX['Cb'][0] + SCX['vel'][0]*t,
                 SCX['Cb'][1] + SCX['vel'][1]*t,
                 SCX['Cb'][2] + SCX['vel'][2]*t])
Dt2 = sp.expand(((E0v - Ctv).dot(Dxy))**2 - Dxy.dot(Dxy)*((E0v - Ctv).dot(E0v - Ctv) - SCX['R2']**2))
pt = sp.Poly(Dt2, t)
env = sp.expand(pt.nth(2)**2 - 4*pt.nth(1)*pt.nth(0))   # beta^2 - 4 alpha gamma... wait:
# for Delta = a2 t^2 + a1 t + a0, envelope over t is a1^2 - 4 a2 a0 = 0
env = sp.expand(pt.nth(1)**2 - 4*pt.nth(2)*pt.nth(0))
print("swept-silhouette polynomial: total degree in (x,y) =",
      sp.Poly(env, xv, yv).total_degree(), " (quartic, as promised)")
envf = sp.lambdify((xv, yv), env, 'numpy')
W = 256
half = 1.25*SCX['e']*SCX['R']/np.sqrt(SCX['e']**2 - SCX['R']**2)
xs = np.linspace(-half, half, W); ys = np.linspace(half, -half, W) - 0.15*half
Xg, Yg = np.meshgrid(xs, ys)
fig, ax = plt.subplots(figsize=(4.5, 4))
ax.imshow(srgb_encode(img_sym), extent=[-half, half, -half - 0.3*half, -0.3*half + 2*half][2:] if False else [-half, half, -half, half])
ax.contour(Xg, Yg + 0.15*half, envf(Xg, Yg + 0*Yg), levels=[0], colors='cyan', linewidths=1.0)
ax.set_title("the quartic outline of B's swept silhouette (cyan)")
ax.axis('off'); plt.show()

# (b) THE BOKEH SHAPE: trace a defocused point through the aperture grid.
def bokeh_footprint(X, f_v, a_v, SC=SCX, n=41):
    e_v = SC['e']; pts = []
    for qxv in np.linspace(-a_v, a_v, n):
        for qyv in np.linspace(-a_v, a_v, n):
            if qxv**2 + qyv**2 > a_v*a_v: continue
            lam = (e_v - X[2])/(f_v - X[2])
            F = np.array(X) + (1/lam)*(np.array([qxv, qyv, e_v]) - np.array(X))
            pts.append(F[:2]*e_v/(e_v - f_v))
    return np.array(pts)
pts = bokeh_footprint(np.array([0.2, -0.1, 2.3]), 1.0, 0.3)
plt.figure(figsize=(4, 4))
plt.scatter(pts[:, 0], pts[:, 1], s=3, color='#1f3a5f')
th = np.linspace(0, 2*np.pi, 200)
pred = 0.3*SCX['e']*abs(1.0 - 2.3)/((SCX['e'] - 2.3)*(SCX['e'] - 1.0))
cen = pts.mean(axis=0)
plt.plot(cen[0] + pred*np.cos(th), cen[1] + pred*np.sin(th), color='#7a1f1f', lw=1.5)
plt.gca().set_aspect('equal'); plt.title("bokeh footprint = scaled aperture (law in red)")
plt.show()
print("The footprint is the aperture, scaled. A square aperture would draw a square.")
Expect: degree-4 outline hugging the trail; the traced bokeh disk matching the red law circle.

🐍 Cell 6 (NEW) — the regressions: a → 0 returns the pinhole; v → 0 returns the still frame

· · paste into colab.new → Run
# Two limits, both machine-checked:
img_a0, code_a0, _, _ = render_symbolic_md(128, 128, {**SCX, 'aperture': 1e-6, 'Ka': 1})
img_still, _, _, _     = render_symbolic_md(128, 128, {**SCX, 'vel': (0., 0., 0.),
                                                       'aperture': 1e-6, 'Ka': 1, 'Kt': 1})
img_both, _, _, _      = render_symbolic_md(128, 128, {**SCX, 'vel': (0., 0., 0.),
                                                       'aperture': 1e-6, 'Ka': 1,
                                                       'Kt': 1, 'time_mode': 'strata'})
d1 = np.abs(img_a0 - img_still)
print(f"aperture->0 vs v->0 (symbolic): max |delta| = {d1.max():.3e}")
print("Both limits land on the same still pinhole frame -- the extensions are")
print("one continuous family closing on Chapter 5.")
# and the numeric pipeline agrees at its center sample:
img_n0, code_n0, _, _ = render_numeric_md(128, 128, {**SCX, 'aperture': 1e-6,
                                                     'Ka': 1, 'Kt': 1,
                                                     'vel': (0., 0., 0.)})
same = code_n0 == code_a0
d2 = np.abs(img_n0 - img_still)[same]
print(f"numeric still/pinhole vs symbolic: interior max |delta| = {d2.max():.3e}, "
      f"flips {(~same).sum()}")
print("Regression contract, final form: every extension of this series degenerates")
print("to its predecessor in its documented limit. The chain is unbroken.""")
Expect: ~1e-12 or better between the two limits; the numeric still frame matching at machine precision.

🐍 Cell 7 (NEW) — pitfalls, live: wagon-wheel strobing; midpoint shading tax; focus breathing of the blur law

· · paste into colab.new → Run
# (a) WAGON-WHEEL: numeric Kt=1 strobes the trail into hard ghosts.
img_k1, _, fB_k1, _ = render_numeric_md(128, 128, {**SCX, 'Kt': 1, 'seed': 3})
img_k8, _, fB_k8, _ = render_numeric_md(128, 128, {**SCX, 'Kt': 8, 'seed': 3})
_, _, fB_ex, _      = render_symbolic_md(128, 128, SCX)
fig, axs = plt.subplots(1, 3, figsize=(13, 4))
axs[0].imshow(fB_k1, cmap='magma'); axs[0].set_title("numeric coverage, Kt=1: ghosts")
axs[1].imshow(fB_k8, cmap='magma'); axs[1].set_title("numeric coverage, Kt=8")
axs[2].imshow(fB_ex, cmap='magma'); axs[2].set_title("symbolic: exact interval")
for ax in axs: ax.axis('off')
plt.show()
print(f"coverage RMS error: Kt=1 {np.sqrt(((fB_k1 - fB_ex)**2).mean()):.4f}, "
      f"Kt=8 {np.sqrt(((fB_k8 - fB_ex)**2).mean()):.4f}, symbolic exact by construction")

# (b) THE MIDPOINT TAX: symbolic freezes shading at the interval midpoint.
#     Measure against dense numeric truth (Kt=16):
img_t16, code_t16, _, _ = render_numeric_md(128, 128, {**SCX, 'Kt': 16, 'Ka': 4, 'seed': 11})
img_sx, code_sx, _, _   = render_symbolic_md(128, 128, {**SCX, 'Ka': 4})
same = code_t16 == code_sx
d = np.abs(img_t16 - img_sx)[same]
print(f"\nsymbolic vs numeric Kt=16 truth: interior max {d.max():.3e}, "
      f"RMS {np.sqrt((d**2).mean()):.3e}")
print("The residual IS the midpoint tax (plus numeric variance): bounded, visible,")
print("and removable by splitting the interval -- at the price of more traces.")

# (c) FOCUS BREATHING OF THE LAW: rho_CoC is nonlinear in z0; near z0 -> e the
#     law diverges (points at the pupil). Print the working range:
print("\nbokeh radius law along depth (a=0.18, f=1.0, e=4):")
for z0v in (0.2, 0.6, 1.0, 1.6, 2.2, 3.0, 3.6):
    print(f"  z0={z0v:>4}: rho = {0.18*4*abs(1.0 - z0v)/((4 - z0v)*3):.4f}")
print("The law has a pole at the pupil: defocus is unbounded as z0 -> e. Nobody")
print("focuses through their own lens; the slider won't let you.""")
Expect: ghost trails at Kt=1 melting by Kt=8; the midpoint tax measured (~1e-2 territory); the bokeh law's pole printed and fenced.

🐍 Cell 8 (NEW) — the frontier, updated: quartic shadow switching times, and the stack of integrals complete

· · paste into colab.new → Run
# (a) B's MOVING SHADOW has quartic switching times. The shadow quadratic's
#     discriminant in t: b2(t), c2(t) quadratic => disc = b2^2 - a2 c2 is QUARTIC.
tt = sp.symbols('t', real=True)
Pvx, Pvy, Pvz = sp.symbols('P_x P_y P_z', real=True)
Llx, Lly, Llz = sp.symbols('L_x L_y L_z', real=True)
Pv = sp.Matrix([Pvx, Pvy, Pvz]); Ll = sp.Matrix([Llx, Lly, Llz])
Ct2 = sp.Matrix([SCX['Cb'][0] + SCX['vel'][0]*tt,
                 SCX['Cb'][1] + SCX['vel'][1]*tt,
                 SCX['Cb'][2] + SCX['vel'][2]*tt])
seg = Ll - Pv
a2e = seg.dot(seg)
b2e = (Pv - Ct2).dot(seg)
c2e = (Pv - Ct2).dot(Pv - Ct2) - SCX['R2']**2
sdisc = sp.expand(b2e**2 - a2e*c2e)
print("shadow-mask discriminant in t: degree", sp.Poly(sdisc, tt).degree(),
      " (quartic -- no general closed form; stratified in time on both pipelines)")
print("The visibility mask was quadratic; the SHADOW mask is quartic. The exact")
print("frontier moved one degree; the method did not change.")

# (b) THE STACK, COMPLETE:
print("""
The product integral, final form:
   pixel^2 (Ch.6)  x  source^2 (Ch.7, resting)  x  lobe^2 (Ch.8, resting)
   x  aperture^2   x  time
Seven dimensions of quadrature over one chambered, differentiable, mostly-
algebraic integrand. What is exact: the checker's box integral, the on-axis
source anchor, the lobe's Lambert weights, B's temporal visibility interval,
the bokeh footprint. What is sampled: everything the boundary algebra cannot
reach -- bitmaps, quartic shadow masks, general occlusion in time.
What remains: path space -- where the integrand's chamber structure itself
recurses. The symbolic object's job, settled at last: ground truth you can
hold, differentiate (Ch.9), and sample (Ch.6-8, 10), whose boundaries carry
polynomial passports (Ch.3-5, 10). Not the answer: the thing answers are
estimates OF. The Appendix (next) writes the contract down.""")
Expect: the quartic degree printed by sympy; the seven-dimensional stack tallied with its exact and sampled provinces named.

§6 · The honest ledger — Chapter-10 deltas

DimensionChapter 9Chapter 10
Camerapinholepupil: aperture disk + focus plane; bokeh law in closed form (§3)
Timeshutter interval; B moves linearly; visibility switching times are a quadratic in t (§3)
Integralnone (rest stop)5-D: pixel² × aperture² × time (source and lobe rest; Cell 8 tallies all seven)
NumericFD gradientjittered time × aperture — Cook's distributed doctrine (Cell 1)
Symbolicexact gradientstratified aperture × exact time coverage; cheaper than sampling past Kt=2 (Cell 4)
New boundarykink locithe swept silhouette: algebraic, degree ≤ 4, drawn (Cell 5)
Regressionsβ → 0a → 0 and v → 0 — two limits, one still pinhole frame (Cell 6)
New pitfallsU-curve, creaseswagon-wheel strobing; midpoint shading tax; bokeh pole at z₀ → e (Cell 7)
Frontiernormal maps, displacementshadow switching times are quartic; path space beyond (Cell 8)

§7 · Pitfalls gallery, continued

  1. Time undersampling strobes. Kt=1 ghosts the trail into hard echoes — the wagon-wheel effect, reproduced on purpose in Cell 7a. The exact interval never strobes; that is what a closed form buys.
  2. Exact visibility ≠ exact shading. The midpoint tax (Cell 7b) is the price of freezing light inside a time interval; split the interval to pay less.
  3. The bokeh law has a pole. As z₀ → e the CoC diverges — points at the pupil blur without bound. Fences belong in the UI and in the prose (Cell 7c).
  4. Ordering a moving object against static ones is time-dependent. We decide at interval midpoints; a pixel where B overtakes A mid-shutter is a temporal branch boundary — stratify time there or confess the residual (we confess).
  5. Shadows of moving objects are quartic. One degree past the closed form; both pipelines stratify the shadow in time, and the ledger stops pretending otherwise (Cell 8).
  6. Determinism vs the seed, one last time. The numeric trail dithers with the seed; the symbolic trail is the scene's pure function. At the end of the arc, the argument hasn't changed — only the dimension count.

§8 · A brief history, continued

Motion blur and depth of field entered the computer image together: Cook, Porter & Carpenter's distributed ray tracing (1984) is this chapter's numeric panel, forty years early — jitter the lens, jitter the clock, average. Potmesil & Chakravarty (1981–83) had already priced the lens models; the thin-lens bokeh law is photographic optics from the 1850s (Petzval's lenses knew the disk). Temporal antialiasing by analytic visibility is the rarest bird in this book's aviary — Catmull's 1978 analytic hidden-surface work and the 1990s beam-tracing literature hunted it; for a linearly-moving sphere the shutter mask closes, as §3 proves. In the other world: a quadratic's roots are the oldest closed form in this entire series — older than Eudoxus, older than notation. The camera acquired a clock, and the clock's mathematics turned out to be ancient Babylonian. That is the right note to end the main arc on.

§9 · Roadmap

Set by the Press · single HTML5 file · the expression is the ground truth.
Section ids are stable (s1…s9, sim, lab, cell1…cell8) — cite the id when requesting revisions.