codex/optical-tweezers-synthetic-calibration.py

optical-tweezers-synthetic-calibration.py
"""Synthetic harmonic-trap model, never measured data.

Python 3.14 / numpy 2.3.5 / matplotlib 3.10.7. Run in _media.
Stationary exact Ornstein-Uhlenbeck transitions; rectangular, demeaned,
nonoverlapping block periodograms in displacement squared per hertz.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def model():
    k_b, temperature, stiffness = 1.380649e-23, 300.0, 2.0e-5
    radius, viscosity, beta = 0.5e-6, 1.0e-3, 100.0e-9
    drag = 6 * np.pi * viscosity * radius
    tau = drag / stiffness
    sigma2 = k_b * temperature / stiffness
    dt, count = 1 / 20000, 200000
    rho = np.exp(-dt / tau)
    rng = np.random.default_rng(3817)
    x = np.empty(count)
    x[0] = np.sqrt(sigma2) * rng.normal()
    innovations = np.sqrt(sigma2 * (-np.expm1(-2 * dt / tau))) * rng.normal(size=count - 1)
    for j in range(1, count):
        x[j] = rho * x[j - 1] + innovations[j - 1]
    block_length = 4096
    blocks = x[:count // block_length * block_length].reshape(-1, block_length)
    blocks = blocks - blocks.mean(axis=1, keepdims=True)
    transformed = np.fft.rfft(blocks, axis=1)
    psd = dt / block_length * np.abs(transformed) ** 2
    psd[:, 1:-1] *= 2
    frequency = np.fft.rfftfreq(block_length, dt)
    # This is the exact Parseval check for the declared normalization, including endpoints.
    np.testing.assert_allclose(psd.sum(axis=1) * (frequency[1] - frequency[0]), np.mean(blocks ** 2, axis=1), rtol=1e-12)
    return locals()


def main():
    m = model()
    x, dt, sigma2, tau, drag, stiffness = [m[k] for k in ("x", "dt", "sigma2", "tau", "drag", "stiffness")]
    kbt, beta = m["k_b"] * m["temperature"], m["beta"]
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, axes = plt.subplots(2, 2, figsize=(11.2, 7.6), facecolor="white")
    blue, red = "#2367a2", "#bd4635"
    time = np.arange(len(x)) * dt
    axes[0, 0].plot(time[:400] * 1000, x[:400] * 1e9, color=blue, lw=1)
    axes[0, 0].set(xlabel="Time (ms)", ylabel="Displacement x (nm)", title="A. Synthetic equilibrium trajectory (first 20 ms)")
    axes[0, 0].axhline(0, color="0.6", lw=0.7)
    axes[0, 1].hist(x * 1e9, bins=65, density=True, color="#bdd6e8", label="Synthetic samples")
    grid = np.linspace(-60, 60, 500)
    density = np.exp(-(grid * 1e-9) ** 2 / (2 * sigma2)) / np.sqrt(2 * np.pi * sigma2) * 1e-9
    axes[0, 1].plot(grid, density, color=red, lw=2, label="Exact equilibrium density")
    axes[0, 1].set(xlabel="Displacement x (nm)", ylabel="Density (nm⁻¹)", title=f"B. Equipartition: σ = {np.sqrt(sigma2)*1e9:.2f} nm")
    axes[0, 1].legend(fontsize=9)
    f = m["frequency"][1:]
    continuous = 4 * kbt * drag / (stiffness ** 2 + (2 * np.pi * f * drag) ** 2)
    sampled = 2 * sigma2 * dt * (1 - m["rho"] ** 2) / ((1 - m["rho"]) ** 2 + 4 * m["rho"] * np.sin(np.pi * f * dt) ** 2)
    axes[1, 0].loglog(f, m["psd"].mean(axis=0)[1:] * 1e18, color="#8ab4d1", lw=0.9, label="Synthetic block-averaged PSD")
    axes[1, 0].loglog(f, continuous * 1e18, color=red, lw=1.8, label="Continuous one-sided model")
    axes[1, 0].loglog(f, sampled * 1e18, color="0.2", ls="--", lw=1.2, label="Exact sampled model (aliasing)")
    fc = 1 / (2 * np.pi * tau)
    axes[1, 0].axvline(fc, color="0.4", ls=":", lw=1)
    axes[1, 0].set(xlabel="Frequency f (Hz)", ylabel="Position PSD (nm²/Hz)", title=f"C. Corner frequency f_c = {fc:.1f} Hz", ylim=(5e-4, 2))
    axes[1, 0].legend(fontsize=8)
    speed = np.linspace(-100, 100, 101)
    displacement = drag * speed * 1e-6 / stiffness * 1e9
    axes[1, 1].plot(speed, displacement, color=blue, lw=2, label="Exact steady-drag model")
    axes[1, 1].plot([-50, 50], drag * np.array([-50, 50]) * 1e-6 / stiffness * 1e9, "o", color=red)
    axes[1, 1].axhline(0, color="0.7", lw=0.7)
    axes[1, 1].set(xlabel="Fluid speed u (µm/s)", ylabel="Mean displacement ⟨x⟩ (nm)", title="D. Drag calibration cross-check")
    axes[1, 1].text(0.04, 0.94, "At u = 50 µm/s:\n⟨x⟩ = 23.56 nm, V − V₀ = 0.2356 V\nζu = k⟨x⟩ = 0.4712 pN", transform=axes[1, 1].transAxes, va="top", fontsize=10)
    for ax in axes.flat:
        ax.spines[["right", "top"]].set_visible(False)
    fig.suptitle("Synthetic optical-trap calibration — NOT measured data", fontsize=16, y=0.98)
    fig.text(0.5, 0.018, "T = 300 K; a = 0.50 µm; η = 1.00 mPa s; k = 0.020 pN/nm; β = 100 nm/V.\n10 s at 20 kHz; exact stationary OU steps; no detector noise, exposure averaging or walls.", ha="center", fontsize=10)
    fig.subplots_adjust(left=0.085, right=0.985, top=0.9, bottom=0.15, hspace=0.48, wspace=0.28)
    fig.savefig(Path(__file__).stem + ".png", dpi=110, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()