codex/perrin-brownian-displacement-diffusion.py

perrin-brownian-displacement-diffusion.py
"""Original independent horizontal-displacement readout, not historical data.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; output is perrin-brownian-displacement-diffusion.png in CWD.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    k_b, temperature, viscosity, radius = 1.380649e-23, 298.0, 1.00e-3, .250e-6
    diffusivity = k_b * temperature / (6 * np.pi * viscosity * radius) * 1e12
    rng = np.random.default_rng(1609)
    plt.rcParams.update({'font.size': 11, 'axes.spines.top': False, 'axes.spines.right': False})
    fig, (tracks, msd) = plt.subplots(1, 2, figsize=(11.6, 5.3), facecolor='white',
                                    gridspec_kw={'width_ratios': [1, 1.35]})
    fig.subplots_adjust(left=.08, right=.97, bottom=.20, top=.74, wspace=.32)
    fig.suptitle('A second readout: horizontal displacement', fontsize=18, y=.97)
    fig.text(.5, .86, 'Chosen bulk model: T = 298 K, η = 1.00 mPa s, a = 0.250 μm; D = %.4f μm²/s' % diffusivity,
             ha='center', fontsize=11)
    colors = ['#007c91', '#dd7223', '#6956b3']
    for j, color in enumerate(colors):
        steps = rng.normal(size=(20, 2)) * np.sqrt(2 * diffusivity)
        positions = np.vstack([np.zeros(2), np.cumsum(steps, axis=0)])
        tracks.plot(positions[:, 0], positions[:, 1], '.-', color=color, lw=1.1, markersize=4,
                    alpha=.85, label=f'Track {j + 1}')
    tracks.scatter([0], [0], marker='*', c='#253746', s=100, zorder=5)
    tracks.set(xlabel='Horizontal x (μm)', ylabel='Horizontal y (μm)')
    tracks.set_aspect('equal', adjustable='datalim')
    tracks.set_title('Three samples: 20 s, positions every 1 s', fontsize=11.5, pad=13)
    tracks.grid(alpha=.2)
    tracks.legend(frameon=False, fontsize=9, loc='upper left')

    tau = np.linspace(0, 20, 300)
    msd.plot(tau, 2 * diffusivity * tau, color=colors[0], lw=2, label='One coordinate: 2Dτ')
    msd.plot(tau, 4 * diffusivity * tau, color=colors[1], lw=2, label='Two coordinates: 4Dτ')
    lags = np.array([1., 2., 5., 10., 15., 20.])
    sample_x, sample_xy = [], []
    # Separate ensembles at each lag; independent of the three tracks and height counts.
    for lag in lags:
        displacement = rng.normal(size=(16000, 2)) * np.sqrt(2 * diffusivity * lag)
        sample_x.append(np.mean(displacement[:, 0]**2))
        sample_xy.append(np.mean(np.sum(displacement**2, axis=1)))
    msd.scatter(lags, sample_x, s=32, facecolor='white', edgecolor=colors[0], zorder=3)
    msd.scatter(lags, sample_xy, s=32, marker='s', facecolor='white', edgecolor=colors[1], zorder=3)
    msd.axvline(10, color='#718096', ls=':', lw=1)
    msd.annotate('At 10 s: 17.46 and 34.92 μm²', xy=(10, 4 * diffusivity * 10),
                 xytext=(1.5, 59), fontsize=10, arrowprops={'arrowstyle': '->', 'color': '#536170'})
    msd.set(xlabel='Lag τ (s)', ylabel='Mean squared displacement (μm²)', xlim=(0, 21), ylim=(0, 77))
    msd.set_title('Displacement statistics determine D', fontsize=12, pad=13)
    msd.legend(frameon=False, fontsize=10, loc='upper left')
    msd.grid(alpha=.2)
    fig.text(.5, .06, 'Lines: exact model laws. Open markers: separate simulated ensembles, 16,000 displacements per lag.\nJoined sample positions do not resolve the intervening motion. No historical data are plotted.',
             ha='center', fontsize=10, color='#47535e')
    output = Path.cwd() / f'{Path(__file__).stem}.png'
    fig.savefig(output, dpi=110, facecolor='white')
    plt.close(fig)
    print(output)


if __name__ == '__main__':
    main()