#!/usr/bin/env python3 """ setup_diagrams.py ================= Labelled schematics of the LCoS fringe-projection setups used in lcos_fpp_error_sim.py, ftp_walkthrough.py and psp_walkthrough.py. setup_A_amplitude_lcos_projector.png Approach A: amplitude-mode LCoS projector + camera in Takeda's crossed-axes geometry setup_B_phase_slm_interferometer.png Approach B: phase-only LCoS grating, 4f Fourier filter, +/-1 orders interfere setup_C1_VAN_cell_amplitude_projector.png vertically aligned cell (goes with A) setup_C2_PAN_cell_phase_slm.png parallel-aligned / ECB cell (goes with B, C) Schematics, not to scale. Requires numpy and matplotlib. """ import textwrap import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import (Rectangle, Polygon, Ellipse, Circle, FancyBboxPatch, FancyArrowPatch) INK = "#222222" LIGHT = "#f2b705" # illumination PROJ = "#d1495b" # projector ray CAM = "#2e86ab" # camera ray GREEN = "#2a9d4b" # laser / SLM beams # ---------------------------------------------------------------------------------- # helpers # ---------------------------------------------------------------------------------- def new_canvas(w=17, h=9.6, xlim=(0, 215), ylim=(0, 100)): fig, ax = plt.subplots(figsize=(w, h)) ax.set_xlim(*xlim) ax.set_ylim(*ylim) ax.set_aspect("equal") ax.axis("off") return fig, ax def tag(ax, x, y, n, r=2.0, fs=9): ax.add_patch(Circle((x, y), r, fc="white", ec=INK, lw=1.2, zorder=20, clip_on=False)) ax.text(x, y, str(n), ha="center", va="center", fontsize=fs, weight="bold", zorder=21, clip_on=False) def lead(ax, x0, y0, x1, y1, n, r=2.0): """Tag at (x0,y0) with a leader line to the part at (x1,y1).""" ax.plot([x0, x1], [y0, y1], color=INK, lw=0.7, zorder=19) ax.plot(x1, y1, "o", color=INK, ms=2.5, zorder=19) tag(ax, x0, y0, n, r) def lens_v(ax, x, y, h, w=1.6, fc="#cfe8f3", z=6): """Lens with vertical aperture (optical axis horizontal).""" ax.add_patch(Ellipse((x, y), w, h, fc=fc, ec=INK, lw=1, zorder=z)) def lens_h(ax, x, y, w, h=1.6, fc="#cfe8f3", z=6): """Lens with horizontal aperture (optical axis vertical).""" ax.add_patch(Ellipse((x, y), w, h, fc=fc, ec=INK, lw=1, zorder=z)) def box(ax, x, y, w, h, fc, ec=INK, lw=1.1, r=0.8, z=3, **kw): ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle=f"round,pad=0,rounding_size={r}", fc=fc, ec=ec, lw=lw, zorder=z, **kw)) def dim(ax, p0, p1, label, off=(0, 0), fs=10, color=INK): ax.add_patch(FancyArrowPatch(p0, p1, arrowstyle="<|-|>", mutation_scale=9, color=color, lw=1, zorder=15)) mx, my = (p0[0] + p1[0]) / 2 + off[0], (p0[1] + p1[1]) / 2 + off[1] ax.text(mx, my, label, ha="center", va="center", fontsize=fs, color=color, zorder=16, bbox=dict(fc="white", ec="none", pad=0.6, alpha=0.9)) def arrow(ax, p0, p1, color=INK, lw=1.2, style="-|>", ls="-", z=12, ms=10): ax.add_patch(FancyArrowPatch(p0, p1, arrowstyle=style, mutation_scale=ms, color=color, lw=lw, ls=ls, zorder=z)) def key(ax, items, x0, y0, title, width=58, fs=8.6, dy_line=2.55, gap=1.25): """Numbered key: items = [(n, name, detail), ...].""" ax.text(x0 - 2.5, y0 + 4, title, fontsize=11.5, weight="bold", va="center") y = y0 for n, name, detail in items: tag(ax, x0, y, n, r=1.7, fs=8) lines = textwrap.wrap(detail, width) if detail else [] ax.text(x0 + 3, y, name, fontsize=fs + 0.4, weight="bold", va="center") for i, ln in enumerate(lines): ax.text(x0 + 3, y - dy_line * (i + 1), ln, fontsize=fs, va="center", color="#444444") y -= dy_line * (len(lines) + 1) + gap return y # ---------------------------------------------------------------------------------- # A: amplitude LCoS projector + camera # ---------------------------------------------------------------------------------- def draw_A(out): fig, ax = new_canvas(17.5, 11.5, (0, 222), (-36, 108)) # table, stage, reference plane ax.add_patch(Rectangle((8, -1), 134, 3, fc="#bdbdbd", ec=INK, lw=1, hatch="////", zorder=1)) ax.add_patch(Rectangle((38, 2), 88, 5.5, fc="#e3e3e3", ec=INK, lw=1, zorder=2)) ax.text(116, 4.6, "z-stage", fontsize=7.5, ha="center", va="center", color="#555") arrow(ax, (122, 3.0), (122, 7.0), style="<|-|>", lw=0.9, ms=7) ax.add_patch(Rectangle((33, 7.5), 98, 2.5, fc="white", ec=INK, lw=1.2, zorder=4)) # object xs = np.linspace(70, 110, 120) ys = 10 + 10 * np.exp(-(xs - 90) ** 2 / (2 * 6.0 ** 2)) ax.add_patch(Polygon(np.c_[np.r_[70, xs, 110], np.r_[10, ys, 10]], closed=True, fc="#e9c46a", ec=INK, lw=1.2, zorder=8)) Ep, Ec = np.array([42.0, 66.0]), np.array([120.0, 66.0]) # projected fringe fan (behind object) xe = np.arange(44, 124, 2.6) for i in range(len(xe) - 1): ax.add_patch(Polygon([Ep, (xe[i], 10), (xe[i + 1], 10)], closed=True, fc="#7a1f2b" if i % 2 else "#fff3b0", ec="none", alpha=0.22 if i % 2 else 0.5, zorder=3)) # fringes on the object surface (projected from Ep, approximate) for i in range(len(xe) - 1): if i % 2: continue seg = [] for xx in np.linspace(xe[i], xe[i + 1], 6): # intersect ray Ep->(xx,10) with the bump surface by marching t = np.linspace(0, 1, 400) px = Ep[0] + (xx - Ep[0]) * t py = Ep[1] + (10 - Ep[1]) * t surf = 10 + 10 * np.exp(-(px - 90) ** 2 / 72) hit = np.argmax(py <= surf) if hit and 70 < px[hit] < 110: seg.append((px[hit], py[hit])) if len(seg) > 1: seg = np.array(seg) ax.plot(seg[:, 0], seg[:, 1], color="#7a1f2b", lw=3.2, alpha=0.55, solid_capstyle="butt", zorder=9) # camera field of view ax.add_patch(Polygon([Ec, (40, 10), (128, 10)], closed=True, fc=CAM, ec="none", alpha=0.05, zorder=2)) ax.plot([Ec[0], 40], [Ec[1], 10], color=CAM, lw=0.6, ls=":", zorder=3) ax.plot([Ec[0], 128], [Ec[1], 10], color=CAM, lw=0.6, ls=":", zorder=3) # triangulation rays P = np.array([86.0, 10 + 10 * np.exp(-(86 - 90) ** 2 / 72)]) A = Ep + (P - Ep) * (Ep[1] - 10) / (Ep[1] - P[1]) B = Ec + (P - Ec) * (Ec[1] - 10) / (Ec[1] - P[1]) ax.plot(*zip(Ep, P), color=PROJ, lw=1.8, zorder=11) ax.plot(*zip(P, A), color=PROJ, lw=1.2, ls="--", zorder=11) ax.plot(*zip(Ec, P), color=CAM, lw=1.8, zorder=11) ax.plot(*zip(P, B), color=CAM, lw=1.2, ls="--", zorder=11) ax.plot([P[0], P[0]], [10, P[1]], color=INK, lw=1, zorder=12) ax.plot(*P, "o", color=INK, ms=4, zorder=13) ax.text(P[0] - 1.5, P[1] + 2.2, "P", fontsize=10, weight="bold", zorder=13) ax.text(P[0] + 0.8, 13.0, "h", fontsize=10, style="italic", zorder=13) for pt, lab in [(A, "A"), (B, "B")]: ax.plot(*pt, "|", color=INK, ms=9, mew=1.5, zorder=13) ax.text(pt[0], 11.4, lab, ha="center", fontsize=10, weight="bold", zorder=13) dim(ax, (B[0], 5.0), (A[0], 5.0), "", fs=8) ax.text(A[0] + 1.5, 5.0, "AB", fontsize=9, weight="bold", va="center", zorder=13) # projector housing and internals box(ax, 9, 63.5, 52, 30, "#f4f4f4", z=2) ax.text(11, 91.2, "LCoS projector", fontsize=10.5, weight="bold", va="center") ax.add_patch(Polygon([(17, 76), (38, 76), (38, 84), (17, 84)], fc=LIGHT, alpha=0.35, ec="none", zorder=4)) ax.add_patch(Polygon([(40, 76), (44, 76), (44, 86), (40, 86)], fc=LIGHT, alpha=0.35, ec="none", zorder=4)) ax.add_patch(Polygon([(40, 66.5), (44, 66.5), (44, 76), (40, 76)], fc="#ffd27f", alpha=0.45, ec="none", zorder=4)) # LED + heat sink ax.add_patch(Rectangle((12.0, 77), 3.2, 6, fc="#ffe066", ec=INK, lw=1, zorder=6)) for k in range(5): ax.add_patch(Rectangle((10.2, 77.2 + k * 1.25), 1.8, 0.7, fc="#888", ec="none", zorder=6)) lens_v(ax, 19, 80, 8) for xa in (23.0, 25.6): # fly's-eye arrays ax.add_patch(Rectangle((xa - 0.45, 75.8), 0.9, 8.4, fc="#cfe8f3", ec=INK, lw=0.8, zorder=6)) for k in range(6): ax.add_patch(Ellipse((xa + 0.45, 76.5 + k * 1.4), 0.7, 1.3, fc="#cfe8f3", ec=INK, lw=0.5, zorder=6)) lens_v(ax, 30, 80, 8.6) ax.add_patch(Rectangle((33.6, 75.6), 0.9, 8.8, fc="#9e9e9e", ec=INK, lw=0.8, hatch="||||", zorder=6)) ax.add_patch(Rectangle((38, 76), 8, 8, fc="#d9f0ff", ec=INK, lw=1.1, zorder=6, alpha=0.9)) ax.plot([38, 46], [76, 84], color=INK, lw=1.2, zorder=7) ax.add_patch(Rectangle((37.6, 85.0), 8.8, 1.6, fc="#3d405b", ec=INK, lw=1, zorder=7)) ax.add_patch(Rectangle((47.5, 84.4), 11.5, 5.4, fc="#457b3b", ec=INK, lw=0.9, zorder=6)) ax.plot([46.4, 47.5], [85.8, 86.5], color=INK, lw=1.5, zorder=7) ax.text(53.2, 87.1, "driver", fontsize=7, color="white", ha="center", va="center", zorder=8) ax.add_patch(Rectangle((37.6, 72.4), 8.8, 0.9, fc="#9e9e9e", ec=INK, lw=0.8, hatch="----", zorder=6)) ax.add_patch(Rectangle((37.2, 66.2), 9.6, 5.0, fc="#555b6e", ec=INK, lw=1, zorder=6)) lens_h(ax, 42, 66.6, 9.0, 1.8) ax.text(42, 64.2, "$E_p$", fontsize=9.5, ha="center", weight="bold", zorder=14) # polarization notes ax.text(27.7, 73.0, "s-pol", fontsize=6.8, color="#7a5c00", ha="center") ax.text(47.2, 79.5, "p-pol\n(bright pixels)", fontsize=6.4, color="#7a5c00", ha="left", va="center") # camera box(ax, 108, 70, 24, 19, "#e8eef3", z=2) ax.text(120, 84.6, "camera", fontsize=10.5, weight="bold", ha="center") ax.add_patch(Rectangle((112.5, 79.6), 15, 1.4, fc="#264653", ec=INK, lw=0.8, zorder=6)) ax.text(120, 77.4, "sensor", fontsize=7.5, ha="center", color="#264653") ax.add_patch(Rectangle((114.5, 66.2), 11, 3.8, fc="#555b6e", ec=INK, lw=1, zorder=6)) lens_h(ax, 120, 66.6, 10.2, 1.8) ax.text(120, 64.2, "$E_c$", fontsize=9.5, ha="center", weight="bold", zorder=14) # controller and wiring box(ax, 66, 89, 36, 10, "#fff4e0", z=2) ax.text(84, 95.6, "PC / FPGA controller", fontsize=9.8, weight="bold", ha="center") ax.text(84, 91.8, "patterns · sequencing · phase → height", fontsize=7.6, ha="center", color="#555") arrow(ax, (66, 92.5), (59.2, 88.0), lw=1.3) ax.text(53.5, 95.2, "pattern video\n(DP / HDMI)", fontsize=7, ha="right", va="center", color="#555") arrow(ax, (108, 86.5), (102.2, 92.0), lw=1.3) ax.text(104.5, 84.0, "images\n(USB3 /\nCoaXPress)", fontsize=7, ha="right", va="center", color="#555") # trigger tx = [55, 55, 129, 129] ty = [89.8, 101.8, 101.8, 89.0] ax.plot(tx, ty, color="#e76f51", lw=1.6, ls=(0, (4, 2)), zorder=4) arrow(ax, (129, 92), (129, 89.0), color="#e76f51", lw=1.6) ax.text(70, 102.8, "VSYNC → camera trigger (+ settle delay)", fontsize=7.5, color="#c4512f") # dimensions ax.plot([Ec[0] + 5, 142], [Ec[1], Ec[1]], color="#777", lw=0.7, ls="--") ax.plot([131, 142], [10, 10], color="#777", lw=0.7, ls="--") dim(ax, (139.5, 10), (139.5, 66), "L", fs=11) ax.plot([Ep[0], Ep[0]], [66, 59.5], color="#777", lw=0.7, ls="--") ax.plot([Ec[0], Ec[0]], [66, 59.5], color="#777", lw=0.7, ls="--") dim(ax, (Ep[0], 60.5), (Ec[0], 60.5), "d (baseline)", fs=10) # tags on the drawing lead(ax, 13.5, 70.0, 13.6, 77.0, 1) lead(ax, 21.5, 70.0, 23.0, 75.8, 2) lead(ax, 34.0, 70.0, 34.0, 75.6, 3) lead(ax, 34.5, 89.5, 38.6, 83.0, 4) lead(ax, 30.0, 89.5, 37.9, 85.8, 5) lead(ax, 51.5, 72.8, 46.4, 72.85, 6) lead(ax, 30.5, 64.5, 37.6, 67.0, 7) lead(ax, 56.5, 40.0, 59.5, 28.0, 8) lead(ax, 101.5, 27.0, 96.0, 17.0, 9) lead(ax, 28.0, 13.5, 33.5, 9.0, 10) lead(ax, 136.0, 72.5, 125.0, 67.0, 11) lead(ax, 136.0, 81.5, 127.5, 80.3, 12) lead(ax, 137.0, 99.0, 129.0, 96.0, 13) lead(ax, 64.5, 99.5, 68.0, 97.0, 14) lead(ax, 28.0, 5.0, 38.0, 4.5, 15) items = [ (1, "Light source", "high-power LED (450–530 nm) on a heat sink, constant-current driver; or laser + despeckler"), (2, "Condenser + fly's-eye integrator", "makes the LCoS illumination uniform (flat field)"), (3, "Pre-polarizer", "sends s-polarized light into the PBS"), (4, "Polarizing beam splitter (PBS)", "cube or wire-grid; reflects s to the panel, passes the p light from bright pixels"), (5, "Reflective LCoS panel + driver", "VAN amplitude mode (see the VAN cell diagram); VA-polyimide alignment at moderate brightness; 8–10 bit; pitch 3.7–6.4 µm; digital PWM (camera synced) or analog"), (6, "Clean-up polarizer (analyzer)", "sets the on/off contrast"), (7, "Projection lens, exit pupil $E_p$", "focus or slight defocus on the object; low distortion"), (8, "Projected sinusoidal fringes", "frequency f₀ on the reference plane; phase-shifted for PSP"), (9, "Object under test", "surface point P at height h"), (10, "Reference plane", "flat, matte white; measured once for φ_ref and for calibration"), (11, "Camera lens, entrance pupil $E_c$", "at the same height L as $E_p$; views the object"), (12, "Camera", "global-shutter CMOS, 10–12 bit, hardware trigger"), (13, "Trigger line", "panel VSYNC → camera: wait ≥ 2τ, expose for whole PWM periods"), (14, "PC / FPGA controller", "pattern generation, capture, FTP / PSP, unwrapping, height"), (15, "Translation stage", "moves the reference plane in z for φ → h calibration"), ] key(ax, items, 152, 99, "Components", width=52) gx, gy = 10, -9 ax.text(gx, gy, "Geometry and relations", fontsize=11.5, weight="bold", va="center") geo = ["L : height of both pupils above the reference plane; d : baseline between $E_p$ and $E_c$", "A : where the projector ray through P would have hit the plane; B : where the camera sees P on the plane", "AB = d·h / (L − h) → Δφ = 2π f₀ · AB → h = L·Δφ / (Δφ − 2π f₀ d) (sign depends on which side the projector is)", "small heights: Δφ ≈ k·h with k = 2π f₀ d / L Takeda slope limit for FTP: |∂h/∂x| < L / (3d)", "Schematic, not to scale (real systems: d/L ≈ 0.2–0.5)."] for i, g in enumerate(geo): ax.text(gx, gy - 4.3 - i * 3.6, g, fontsize=9, color="#333" if i < 4 else "#777", style="normal" if i < 4 else "italic") # inset: camera view ins = ax.inset_axes([1.0, 18.0, 30, 30], transform=ax.transData) n = 220 yy, xx = np.mgrid[0:n, 0:n] / n * 32 hh = 2.5 * np.exp(-((xx - 12) ** 2 + (yy - 16) ** 2) / 30) + 1.2 * np.exp(-((xx - 23) ** 2 + (yy - 10) ** 2) / 12) ins.imshow(0.5 + 0.5 * np.cos(2 * np.pi * xx / 1.6 + 1.9 * hh), cmap="gray") ins.set_xticks([]) ins.set_yticks([]) ins.set_title("camera image: fringes bent by the object", fontsize=8) ax.text(111, 107, "Approach A — amplitude-mode LCoS fringe projector + camera (crossed optical axes)", fontsize=14, weight="bold", ha="center", va="center") fig.savefig(out, dpi=150, bbox_inches="tight") plt.close(fig) # ---------------------------------------------------------------------------------- # B: phase-only SLM grating interferometer # ---------------------------------------------------------------------------------- def draw_B(out): fig, ax = new_canvas(17.5, 11.5, (0, 222), (-56, 68)) f = 23.0 # L1 and L2 focal length (unfolded 4f) s1 = 0.2 # +/-1 order slope (exaggerated) hw = 4.0 # beam half-width xa, ya = 55.0, 40.0 # BS centre / fold point zfold = 11.0 # SLM -> fold distance def to_xy(z, u): z = np.asarray(z, float) u = np.asarray(u, float) x = np.where(z <= zfold, 66 - z, xa + u) y = np.where(z <= zfold, ya + u, ya - (z - zfold)) return x, y def ray(u0, s, zend): zs = [0, zfold, f, 2 * f, 3 * f, zend] us = [u0, u0 + s * zfold, u0 + s * f] sp = s - us[2] / f us.append(us[2] + sp * f) # mask us.append(us[3] + sp * f) # L2 spp = sp - us[4] / f us.append(us[4] + spp * (zend - 3 * f)) n = len(zs) if zend > 3 * f else 4 return np.array(zs[:n]), np.array(us[:n]) # illumination train ax.add_patch(Rectangle((1.5, 37), 7, 6, fc="#a7e8bd", ec=INK, lw=1, zorder=6)) ax.plot([8.5, 13], [40, 40], color=GREEN, lw=2, zorder=5) lens_v(ax, 13, 40, 3.4, 1.2) ax.add_patch(Rectangle((16.5, 41.0), 0.8, 3.2, fc=INK, zorder=6)) ax.add_patch(Rectangle((16.5, 35.8), 0.8, 3.2, fc=INK, zorder=6)) ax.add_patch(Polygon([(13, 40.8), (16.9, 40), (13, 39.2)], fc=GREEN, alpha=0.45, ec="none", zorder=4)) ax.add_patch(Polygon([(16.9, 40), (30, 40 + hw), (30, 40 - hw)], fc=GREEN, alpha=0.25, ec="none", zorder=4)) lens_v(ax, 30, 40, 11) ax.add_patch(Polygon([(30, 40 + hw), (66, 40 + hw), (66, 40 - hw), (30, 40 - hw)], fc=GREEN, alpha=0.18, ec="none", zorder=3)) ax.add_patch(Rectangle((35.6, 34.2), 0.9, 11.6, fc="#9e9e9e", ec=INK, lw=0.8, hatch="||||", zorder=6)) lens_v(ax, 40.5, 40, 11.6, 1.0, fc="#e6d5f5") # half-wave plate (drawn thin) # BS ax.add_patch(Rectangle((47, 32), 16, 16, fc="#d9f0ff", ec=INK, lw=1.1, alpha=0.75, zorder=7)) ax.plot([47, 63], [32, 48], color=INK, lw=1.2, zorder=8) ax.add_patch(Polygon([(55 - hw, 48), (55 + hw, 48), (55 + hw, 53), (55 - hw, 53)], fc=GREEN, alpha=0.15, ec="none", zorder=3)) ax.add_patch(Rectangle((50.5, 53), 9, 2.2, fc=INK, zorder=6)) ax.text(49.5, 54.1, "beam dump", fontsize=7.5, va="center", ha="right", color="#555") # SLM ax.add_patch(Rectangle((66, 32.5), 1.8, 15, fc="#3d405b", ec=INK, lw=1, zorder=8)) for k in range(10): ax.add_patch(Rectangle((65.4, 33 + k * 1.45), 0.6, 0.72, fc="#e0e0e0", ec="none", zorder=9)) ax.add_patch(Rectangle((69, 35), 9, 10, fc="#457b3b", ec=INK, lw=0.9, zorder=6)) ax.text(73.5, 40, "SLM\ndriver", fontsize=7, color="white", ha="center", va="center", zorder=8) # diffracted orders through the unfolded 4f zend = 4 * f + 3.5 cols = {-1: GREEN, 0: "#9e9e9e", 1: GREEN} for k in (-1, 0, 1): s = k * s1 ze = zend if k else 2 * f za, ua = ray(-hw, s, ze) zb, ub = ray(hw, s, ze) zc, uc = ray(0, s, ze) # refine sampling between nodes for i in range(len(za) - 1): zz = np.linspace(za[i], za[i + 1], 12) A_ = np.interp(zz, za, ua) B_ = np.interp(zz, zb, ub) xa_, ya_ = to_xy(zz, A_) xb_, yb_ = to_xy(zz, B_) ax.add_patch(Polygon(np.r_[np.c_[xa_, ya_], np.c_[xb_, yb_][::-1]], closed=True, fc=cols[k], ec="none", alpha=0.17 if k else 0.22, zorder=4)) zz = np.linspace(zc[0], zc[-1], 200) xc, yc = to_xy(zz, np.interp(zz, zc, uc)) ax.plot(xc, yc, color=cols[k], lw=1.1, zorder=5) ax.text(70.0, 47.8, "+1", fontsize=8, color=GREEN) ax.text(70.0, 31.0, "−1", fontsize=8, color=GREEN) ax.text(57.0, 22.5, "0", fontsize=8, color="#777") # lenses, mask yL1, yM, yL2, yI = ya - (f - zfold), ya - (2 * f - zfold), ya - (3 * f - zfold), ya - (4 * f - zfold) lens_h(ax, xa, yL1, 22, 2.0) lens_h(ax, xa, yL2, 22, 2.0) ax.text(xa + 12.5, yL1, "L1 (f)", fontsize=8.5, va="center") ax.text(xa + 12.5, yL2, "L2 (f)", fontsize=8.5, va="center") sp = s1 * f for x0, x1 in [(40, xa - sp - 1.0), (xa - sp + 1.0, xa + sp - 1.0), (xa + sp + 1.0, 70)]: ax.add_patch(Rectangle((x0, yM - 0.7), x1 - x0, 1.4, fc=INK, zorder=9)) ax.plot(xa, yM + 0.1, "x", color="#e63946", ms=9, mew=2.2, zorder=10) ax.text(71, yM, "Fourier plane", fontsize=8.5, va="center") # fringe overlap region at the image plane + object zz = np.array([3 * f, 4 * f, zend]) poly_p = [] # overlap diamond of the two beams: |u - c(z)| < hw for both, c(z)=+/-s1*(4f - z) zs = np.linspace(3 * f + 2, zend, 60) lo, hi = [], [] for z in zs: c = s1 * (4 * f - z) a1, b1 = c - hw, c + hw a2, b2 = -c - hw, -c + hw lo.append(max(a1, a2)) hi.append(min(b1, b2)) lo, hi = np.array(lo), np.array(hi) ok = hi > lo xl, yl = to_xy(zs[ok], lo[ok]) xh, yh = to_xy(zs[ok], hi[ok]) clip = Polygon(np.r_[np.c_[xl, yl], np.c_[xh, yh][::-1]], closed=True, fc="none", ec="none") ax.add_patch(clip) for j in range(-12, 13): ln, = ax.plot([xa + j * 0.55, xa + j * 0.55], [yI - 3.5, yI + 16], color="#14532d" if j % 2 else "#a7f3c0", lw=2.2, zorder=6) ln.set_clip_path(clip) # object xo = np.linspace(43, 67, 100) yo = yI - 2.5 + 2.2 * np.exp(-(xo - 56) ** 2 / 18) ax.add_patch(Polygon(np.c_[np.r_[43, xo, 67], np.r_[yI - 9, yo, yI - 9]], closed=True, fc="#e9c46a", ec=INK, lw=1.1, zorder=7, alpha=0.9)) ax.add_patch(Rectangle((40, yI - 11), 30, 2, fc="#bdbdbd", ec=INK, lw=0.9, hatch="////", zorder=6)) ax.text(23, yI + 13, "fringe period Λ = λ / (2 sin θ)", fontsize=8.5, va="center") # camera box(ax, 84, yI - 2, 15, 9, "#e8eef3", z=6) ax.text(91.5, yI + 3.6, "camera", fontsize=9, weight="bold", ha="center", zorder=8) ax.add_patch(Polygon([(84, yI), (80.5, yI - 2.2), (80.5, yI + 2.6), (84, yI + 4)], fc="#555b6e", ec=INK, zorder=7)) ax.plot([80.5, 58], [yI + 1, yI - 1.5], color=CAM, lw=0.8, ls=":", zorder=5) ax.plot([80.5, 49], [yI, yI - 4.0], color=CAM, lw=0.8, ls=":", zorder=5) # controller box(ax, 86, 50, 40, 9, "#fff4e0", z=2) ax.text(106, 56.3, "PC / FPGA controller", fontsize=9.6, weight="bold", ha="center") ax.text(106, 52.4, "grating shift Δx · period p · capture · PSP", fontsize=7.4, ha="center", color="#555") arrow(ax, (86, 52), (78.2, 43.5), lw=1.2) ax.plot([126, 133, 133, 99.5], [54, 54, yI + 4, yI + 4], color="#e76f51", lw=1.5, ls=(0, (4, 2))) ax.text(103, yI + 5.8, "SLM frame sync → camera trigger", fontsize=7.5, color="#c4512f", va="center") # tags lead(ax, 5.0, 49.0, 5.0, 43.0, 1) lead(ax, 15.0, 30.0, 16.8, 35.8, 2) lead(ax, 27.0, 50.0, 30.0, 45.4, 3) lead(ax, 36.0, 51.0, 36.0, 45.8, 4) lead(ax, 43.0, 51.5, 40.6, 45.6, 5) lead(ax, 41.0, 28.5, 48.0, 33.0, 6) lead(ax, 72.0, 52.0, 67.0, 47.6, 7) lead(ax, 34.0, yL1, 44.0, yL1, 8) lead(ax, 34.0, yM, 40.5, yM, 9) lead(ax, 34.0, yL2, 44.0, yL2, 10) lead(ax, 33.0, yI + 4.0, 52.5, yI + 3.0, 11) lead(ax, 33.0, yI - 7.0, 44.5, yI - 5.5, 12) lead(ax, 104.0, yI - 7.0, 96.0, yI - 2.0, 13) lead(ax, 131.0, 61.0, 125.0, 58.0, 14) # inset: grating shift ins = ax.inset_axes([84, 14.5, 44, 21], transform=ax.transData) xg = np.arange(64) p = 16 for row, dx in enumerate([0, 2]): g = (((xg - dx) % p) < p / 2).astype(float) ins.imshow(g[None, :], extent=[0, 64, row * 1.4, row * 1.4 + 1], cmap="Greys_r", aspect="auto", vmin=-0.3, vmax=1.3) ins.set_xlim(0, 64) ins.set_ylim(-0.6, 3.4) ins.annotate("", xy=(16, 2.62), xytext=(0, 2.62), arrowprops=dict(arrowstyle="<|-|>", lw=0.9)) ins.text(8, 2.95, "p (SLM px)", fontsize=7, ha="center") ins.annotate("", xy=(2, -0.25), xytext=(0, -0.25), arrowprops=dict(arrowstyle="-|>", lw=0.9, color="#c4512f")) ins.text(18, -0.33, "Δx = p/8 → fringe step δ = 4πΔx/p = π/2", fontsize=7, color="#c4512f", va="center") ins.text(65, 0.5, "frame n+1", fontsize=6.6, va="center") ins.text(65, 1.9, "frame n", fontsize=6.6, va="center") ins.set_xticks([]) ins.set_yticks([]) ins.set_title("binary phase grating on the SLM (0 / π)", fontsize=8) items = [ (1, "Light source", "superluminescent diode or narrow-band LED (low speckle), or laser; λ sets 2π phase depth"), (2, "Spatial filter", "objective + pinhole: clean, point-like source"), (3, "Collimating lens", "plane wave onto the SLM"), (4, "Linear polarizer", "set along the LC director so the SLM is phase-only"), (5, "Half-wave plate (optional)", "rotates polarization to the director without losing light"), (6, "Non-polarizing beam splitter", "separates SLM input and output (50 % lost to the dump); or tilt the SLM off-axis"), (7, "Phase-only reflective LCoS SLM", "parallel-aligned PAN / ECB (see the PAN cell diagram), ≥ 2π at λ; pitch 8–12.5 µm; analog drive preferred (low phase flicker); Δx grating shift = exact phase step"), (8, "Fourier lens L1", "focal length f; SLM at its front focal plane"), (9, "Fourier-plane mask", "blocks the 0 order (and higher), passes only ±1"), (10, "Lens L2", "4f relay: images the SLM onto the object, ±1 beams cross"), (11, "Interference fringe field", "Λ = λ/(2 sin θ); pure cos², non-localized (huge depth of field); common SLM phase flicker cancels"), (12, "Object", "anywhere inside the beam-overlap volume"), (13, "Camera", "off-axis view (triangulation), synced to SLM frames"), (14, "PC / FPGA controller", "writes gratings, triggers the camera, computes phase"), ] key(ax, items, 152, 59, "Components", width=50) ax.text(2, -55, "Schematic, not to scale: diffraction angles are exaggerated; the reflective SLM path is folded by the beam splitter.", fontsize=8, color="#777", style="italic") ax.text(111, 67, "Approach B — phase-only LCoS grating: ±1 diffraction orders interfere to form the fringes", fontsize=14, weight="bold", ha="center", va="center") fig.savefig(out, dpi=150, bbox_inches="tight") plt.close(fig) # ---------------------------------------------------------------------------------- # C: LCoS cell cross-section # ---------------------------------------------------------------------------------- def draw_C(out, mode="pan"): """Reflective LCoS cell cross-section, drawn to a COMMON scale for both modes so the cell gap and pixel pitch can be compared directly. mode='van': vertically aligned, negative-delta-eps LC, VA-polyimide alignment (moderate brightness), thin gap, small pitch -> amplitude projector (A) mode='pan': parallel-aligned (ECB), positive-delta-eps LC, rubbed polyimide (oblique SiO2 as the high-power option), thicker gap, larger pitch -> phase-only SLM (B, C) """ van = mode == "van" SH, SV = 2.6, 5.0 # drawing units per um: horizontal, vertical (LC layer) d_um = 2.5 if van else 4.0 pitch_um = 4.5 if van else 9.2 gap_um = 0.45 if van else 0.40 driven = ([False, True, True, False, False, True, False, True, True, False] if van else [False, True, True, False, True]) fig, ax = new_canvas(17.5, 10, (0, 222), (2, 108)) X0, X1 = 6, 140 pitch, gap = pitch_um * SH, gap_um * SH npx = len(driven) xstart = X0 + 4 + (X1 - X0 - 8 - npx * pitch) / 2 + gap / 2 xs = [xstart + i * pitch for i in range(npx)] ySi0, ySi1, yMet1 = 8, 26, 33 yMir0, yMir1 = 33, 35.2 yAl0, yAl1 = 35.2, 37.0 yLC0 = yAl1 yLC1 = yLC0 + d_um * SV yAt0, yAt1 = yLC1, yLC1 + 1.8 yITO1 = yAt1 + 1.6 yG1 = yITO1 + 17 PI_FC = "#f3dfb0" # backplane + pixel circuits ax.add_patch(Rectangle((X0, ySi0), X1 - X0, ySi1 - ySi0, fc="#9fb3c8", ec=INK, lw=1, zorder=2)) ax.add_patch(Rectangle((X0, ySi1), X1 - X0, yMet1 - ySi1, fc="#dde5ec", ec=INK, lw=0.8, zorder=2)) w = pitch - gap for x, on in zip(xs, driven): if van: # SRAM bit cell (digital) drawn as two cross-coupled inverters ax.add_patch(Rectangle((x + 0.12 * w, 14.5), 0.3 * w, 3.0, fc="#5c6f82", ec=INK, lw=0.5, zorder=3)) ax.add_patch(Rectangle((x + 0.55 * w, 14.5), 0.3 * w, 3.0, fc="#5c6f82", ec=INK, lw=0.5, zorder=3)) ax.plot([x + 0.42 * w, x + 0.55 * w], [16.6, 15.4], color="#d4a373", lw=0.9, zorder=4) ax.plot([x + 0.42 * w, x + 0.55 * w], [15.4, 16.6], color="#d4a373", lw=0.9, zorder=4) via_x = x + 0.27 * w via_y0 = 17.5 else: # transistor + storage capacitor (analog) ax.add_patch(Rectangle((x + 0.1 * w, 15), 0.3 * w, 3.0, fc="#5c6f82", ec=INK, lw=0.6, zorder=3)) ax.add_patch(Rectangle((x + 0.17 * w, 18), 0.16 * w, 1.3, fc="#d4a373", ec=INK, lw=0.5, zorder=3)) ax.add_patch(Rectangle((x + 0.5 * w, 15.2), 0.3 * w, 0.8, fc="#d4a373", ec=INK, lw=0.5, zorder=3)) ax.add_patch(Rectangle((x + 0.5 * w, 17.6), 0.3 * w, 0.8, fc="#d4a373", ec=INK, lw=0.5, zorder=3)) via_x = x + 0.25 * w via_y0 = 19.3 ax.plot([via_x, via_x], [via_y0, yMir0], color="#6d597a", lw=1.6, zorder=3) ax.add_patch(Rectangle((x, yMir0), w, yMir1 - yMir0, fc="#c0c0c0", ec=INK, lw=0.8, zorder=5)) ax.text(x + w / 2, 29.5, ("on" if on else "0") if van else ("V_on" if on else "V = 0"), fontsize=7.2, ha="center", color="#9d0208" if on else "#555", zorder=6) # alignment layers (polyimide in both default builds) for (y0, y1), top in [((yAl0, yAl1), False), ((yAt0, yAt1), True)]: ax.add_patch(Rectangle((X0 + 4, y0), X1 - X0 - 8, y1 - y0, fc=PI_FC, ec=INK, lw=0.6, zorder=4)) surf = y0 if top else y1 sgn = -1 if top else 1 for xx in np.arange(X0 + 5, X1 - 4.5, 0.85): if van: # vertical side chains ax.plot([xx, xx], [surf, surf + sgn * 0.75], color="#a07a2c", lw=0.7, zorder=5) else: # rubbing micro-grooves ax.plot([xx, xx + 0.55], [surf, surf + sgn * 0.45], color="#a07a2c", lw=0.7, zorder=5) ax.add_patch(Rectangle((X0, yAt1), X1 - X0, yITO1 - yAt1, fc="#c9b6e4", ec=INK, lw=0.8, zorder=4)) ax.add_patch(Rectangle((X0, yITO1), X1 - X0, yG1 - yITO1, fc="#e0f4fb", ec=INK, lw=1, zorder=2, alpha=0.9)) ax.add_patch(Rectangle((X0, yG1), X1 - X0, 0.9, fc="#9be7c4", ec=INK, lw=0.6, zorder=3)) ax.text(X0 + 58, yITO1 + 1.3, "ITO at V_com", fontsize=7.5, ha="left", va="center", color="#4a3b6b", zorder=6) # seals + spacer beads (bead diameter = cell gap) for xa in (X0, X1 - 4): ax.add_patch(Rectangle((xa, yMir0), 4, yAt1 - yMir0, fc="#f4d58d", ec=INK, lw=0.8, zorder=6)) nb = max(1, int((yLC1 - yLC0) // 4.2)) for k in range(nb): ax.add_patch(Circle((xa + 2, yLC0 + 2.1 + k * (yLC1 - yLC0 - 4.2) / max(nb - 1, 1)), 1.6, fc="#fefae0", ec=INK, lw=0.5, zorder=7)) # director field d = yLC1 - yLC0 def w_of_x(x): ww = 0.0 for xp, on in zip(xs, driven): if on: ww = max(ww, 0.5 * (np.tanh((x - xp - 0.4) / (0.08 * d + 0.6)) - np.tanh((x - xp - w + 0.4) / (0.08 * d + 0.6)))) return ww nrow = 4 if van else 6 for x in np.arange(X0 + 6.5, X1 - 5.5, 2.9): ww = w_of_x(x) for y in np.linspace(yLC0 + 1.5, yLC1 - 1.5, nrow): xi = (y - yLC0) / d if van: th0 = np.deg2rad(84) th = th0 - ww * (th0 - np.deg2rad(14)) * np.sin(np.pi * xi) ** 0.7 else: tp = np.deg2rad(5) th = tp + ww * (np.deg2rad(80) - tp) * np.sin(np.pi * xi) ** 0.7 L = 2.3 ax.plot([x - L / 2 * np.cos(th), x + L / 2 * np.cos(th)], [y - L / 2 * np.sin(th), y + L / 2 * np.sin(th)], color="#264653", lw=3.0, solid_capstyle="round", zorder=6, alpha=0.85) # fringing field at the right edge of the 2-pixel driven block xe = xs[2] + w for k, frac in enumerate([0.3, 0.55, 0.8]): t = np.linspace(0, np.pi, 50) span = (1.2 + 1.4 * k) * (d / 12.5) ax.plot(xe - span + 2 * span * (1 - np.cos(t)) / 2 + gap / 2, yMir1 + 0.2 + frac * d * np.sin(t), color="#e63946", lw=0.9, ls="--", zorder=7) # light arrow(ax, (38, 100), (46, yMir1 + 0.4), color="#f4a261", lw=2.2, z=9, ms=14) arrow(ax, (46, yMir1 + 0.4), (54, 100), color="#e76f51", lw=2.2, z=9, ms=14) if van: ax.text(36, 97, "incident\n(s-pol from PBS)", fontsize=8, ha="right", color="#b5651d") ax.text(57, 97, "reflected: unchanged at 0 V (dark)\nrotated toward p when driven (bright)", fontsize=8, ha="left", color="#c4512f") else: ax.text(36, 97, "incident\n(polarized ∥ director)", fontsize=8, ha="right", color="#b5651d") ax.text(57, 97, "reflected (double pass)\nphase delayed, polarization unchanged", fontsize=8, ha="left", color="#c4512f") # dimensions dim(ax, (X1 + 4, yLC0), (X1 + 4, yLC1), "d", fs=11) ax.text(X1 + 6.5, (yLC0 + yLC1) / 2 - 1.0, ("cell gap\nd ≈ 2–3 µm\n(drawn 2.5)" if van else "cell gap\nd ≈ 3–5 µm\n(drawn 4.0)"), fontsize=7.6, va="center") ip = 3 if van else 3 dim(ax, (xs[ip], 23.0), (xs[ip + 1], 23.0), "", fs=7.6) ax.text((xs[ip] + xs[ip + 1]) / 2, 20.6, ("pitch ≈ 3.7–6.4 µm (drawn 4.5) → resolution" if van else "pitch ≈ 8–12.5 µm (drawn 9.2) → less fringing, phase accuracy"), fontsize=7.4, ha="center", color=INK, zorder=8, bbox=dict(fc="white", ec="none", pad=0.5, alpha=0.9)) ax.text(X1 + 6.5, yMir0 - 4.5, f"d / pitch ≈ {d_um / pitch_um:.2f}", fontsize=7.8, weight="bold") # tags lead(ax, 3.0, yG1 + 6, 12, yG1 + 0.5, 1) lead(ax, 3.0, (yITO1 + yG1) / 2, 10, (yITO1 + yG1) / 2, 2) lead(ax, 3.0, yAt1 + 3.5, 10, (yAt1 + yITO1) / 2, 3) lead(ax, 3.0, yAt0 + 0.5, 11, (yAt0 + yAt1) / 2, 4) lead(ax, 3.0, (yLC0 + yLC1) / 2, 13.5, (yLC0 + yLC1) / 2, 5) lead(ax, 3.0, yAl0 - 0.5, 11, (yAl0 + yAl1) / 2, 6) lead(ax, 3.0, yMir0 - 4.0, xs[0] + 1, (yMir0 + yMir1) / 2, 7) lead(ax, 3.0, 20.0, xs[0] + 0.25 * w, 16.0, 8) lead(ax, 3.0, 11.0, 10, 11.0, 9) lead(ax, 146.0, yLC1 + 9, X1 - 2, (yLC0 + yLC1) / 2 + 1, 10) lead(ax, xe + 5, yITO1 + 4.2, xe + 1.0, yLC0 + 0.45 * d, 11) # inset: alignment detail ins = ax.inset_axes([84, yITO1 + 1.6, 52, 13.0], transform=ax.transData) ins.set_xlim(0, 52) ins.set_ylim(0, 13) ins.set_aspect("equal") ins.axis("off") ins.add_patch(Rectangle((0, 0), 52, 1.6, fc="#c0c0c0", ec=INK, lw=0.6)) ins.add_patch(Rectangle((0, 1.6), 52, 1.3, fc=PI_FC, ec=INK, lw=0.6)) if van: for xx in np.arange(1, 51, 1.5): ins.plot([xx, xx + 0.15], [2.9, 4.6], color="#a07a2c", lw=1.2) xm, ym = 36, 7.8 th = np.deg2rad(84) ins.plot([xm - 3.6 * np.cos(th), xm + 3.6 * np.cos(th)], [ym - 3.6 * np.sin(th), ym + 3.6 * np.sin(th)], color="#264653", lw=5, solid_capstyle="round") ins.plot([xm - 3.6 * np.cos(th), xm - 3.6 * np.cos(th)], [ym - 3.6 * np.sin(th), ym + 4.2], color="#555", lw=0.6, ls=":") ins.text(1, 10.6, "VA polyimide: side chains anchor LC vertically;", fontsize=6.5, color="#7a5c00") ins.text(1, 9.2, "light rubbing / photo-alignment sets 1–3° tilt", fontsize=6.5, color="#7a5c00") ins.text(1, 7.8, "from normal (θₚ ≈ 87–89°)", fontsize=6.5, color="#7a5c00") ins.set_title("detail: vertical-alignment polyimide (no oblique SiO₂ needed at moderate brightness)", fontsize=7.6) else: xx = np.arange(0, 52.01, 1.3) ins.plot(xx, 2.9 + 0.35 * (np.arange(len(xx)) % 2), color="#a07a2c", lw=0.9) ins.annotate("", xy=(20, 5.2), xytext=(4, 5.2), arrowprops=dict(arrowstyle="-|>", color="#c4512f", lw=1.1)) ins.text(1, 6.2, "rubbing direction", fontsize=6.5, color="#c4512f") xm, ym = 37, 6.2 th = np.deg2rad(8) ins.plot([xm - 6 * np.cos(th), xm + 6 * np.cos(th)], [ym - 6 * np.sin(th), ym + 6 * np.sin(th)], color="#264653", lw=5, solid_capstyle="round") ins.plot([xm - 7, xm + 7], [ym - 6 * np.sin(th), ym - 6 * np.sin(th)], color="#555", lw=0.6, ls=":") ins.text(xm + 7.3, ym - 0.9, "θₚ ≈ 2–5°", fontsize=7, color="#555") ins.text(1, 10.6, "inorganic option for high power / blue-UV:", fontsize=6.5, color="#4a4e69") ins.text(1, 9.2, "two-step oblique SiO₂ tuned for low pretilt", fontsize=6.5, color="#4a4e69") ins.set_title("detail: rubbed polyimide, antiparallel top/bottom → low pretilt", fontsize=7.6) if van: items = [ (1, "AR coating", "suppresses the front-surface reflection"), (2, "Cover glass", "front substrate, 0.7–1.1 mm"), (3, "ITO common electrode", "transparent; held at V_com"), (4, "Top alignment layer", "vertical-alignment (VA) polyimide, antiparallel pretilt; oblique SiOx only needed for high-flux projection"), (5, "Liquid crystal, VAN mode", "negative-Δε nematic (Δn ≈ 0.08–0.13); vertical at 0 V → dark between crossed polarizers; driven pixels tilt over at 45° to the polarizer → bright; reflectance ≈ sin²(Γ/2)"), (6, "Bottom alignment layer", "VA polyimide; rubbing or photo-alignment sets a 1–3° tilt from the normal so all pixels tilt the same way (no domains)"), (7, "Aluminium pixel mirrors", "small pitch for resolution; fill factor ≈ 90 %"), (8, "Pixel circuit", "SRAM bit cell (digital PWM drive) shown; analog 1T1C also used"), (9, "CMOS silicon backplane", "addresses all pixels"), (10, "Edge seal + spacer beads", "bead diameter sets the thin cell gap"), (11, "Fringing field", f"d/pitch ≈ {d_um / pitch_um:.2f}: spreads tilt into neighbouring pixels, risk of reverse-tilt disclinations; limits the shortest usable fringe period"), ] title = "Vertically aligned (VAN) LCoS cell — amplitude fringe projector (approach A)" else: items = [ (1, "AR coating", "for the design wavelength (avoids ghost interference)"), (2, "Cover glass", "front substrate; flatness matters (wavefront)"), (3, "ITO common electrode", "transparent; held at V_com"), (4, "Top alignment layer", "rubbed polyimide, antiparallel to the bottom one; two-step oblique SiO₂ for high-power use"), (5, "Liquid crystal, PAN / ECB mode", "positive-Δε, high-Δn nematic (Δn ≈ 0.2–0.3); in-plane at 0 V, tilts up when driven; light ∥ director → pure phase, Δφ = 4π d Δn_eff / λ"), (6, "Bottom alignment layer", "rubbed polyimide, pretilt ≈ 2–5°; pretilt uniformity sets how the phase-vs-voltage curve varies across the panel"), (7, "Aluminium pixel mirrors", "larger pitch; dielectric mirror on top in high-power SLMs"), (8, "Pixel circuit", "transistor + storage capacitor (analog drive, low phase flicker) shown"), (9, "CMOS silicon backplane", "addresses all pixels; flatness corrected by calibration"), (10, "Edge seal + spacer beads", "bead diameter sets the thicker cell gap; uniformity → phase uniformity"), (11, "Fringing field", f"d/pitch ≈ {d_um / pitch_um:.2f}: larger pitch keeps fringing small despite the thick cell → accurate phase gratings"), ] title = "Parallel-aligned (PAN / ECB) LCoS cell — phase-only SLM setups (approaches B and C)" key(ax, items, 160, 97, "Layers and parts", width=48) ax.text(111, 107, title, fontsize=14, weight="bold", ha="center", va="center") ax.text(73, 103.4, "drawn to the same scale as the companion cell diagram: compare cell gap and pixel pitch directly", fontsize=9.5, ha="center", va="center", color="#555") ax.text(6, 3.5, "Scale (both cell diagrams): pixel pitch and LC gap to scale (LC layer vertically exaggerated ×2); " "glass, electrode, alignment and silicon layers not to scale; pretilt exaggerated.", fontsize=8, color="#777", style="italic") fig.savefig(out, dpi=150, bbox_inches="tight") plt.close(fig) if __name__ == "__main__": import os os.makedirs("setup_diagrams", exist_ok=True) draw_A("setup_diagrams/setup_A_amplitude_lcos_projector.png") draw_B("setup_diagrams/setup_B_phase_slm_interferometer.png") draw_C("setup_diagrams/setup_C1_VAN_cell_amplitude_projector.png", mode="van") draw_C("setup_diagrams/setup_C2_PAN_cell_phase_slm.png", mode="pan") print("written to", os.path.abspath("setup_diagrams"))