codex/fluorescence-decay-instrument-convolution.py

fluorescence-decay-instrument-convolution.py
"""Plot a stipulated fluorescence decay, timing response and background.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
No experimental data; writes the matching PNG to the current directory.
"""
from math import erfc
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def model(t, tau=4.0, sigma=0.6):
    """Exact convolution of exp(-t/tau)H(t) with a unit-area Gaussian."""
    intrinsic = np.exp(-np.maximum(t, 0)/tau)*(t >= 0)
    response = np.exp(-0.5*(t/sigma)**2)/(sigma*np.sqrt(2*np.pi))
    complement = np.array([erfc(float((sigma*sigma/tau-x)/(np.sqrt(2)*sigma))) for x in t])
    convolved = 0.5*np.exp(sigma*sigma/(2*tau*tau)-t/tau)*complement
    return intrinsic, response, convolved


def main():
    plt.rcParams.update({'font.size': 11, 'font.family': 'DejaVu Sans'})
    t = np.linspace(-2.5, 24, 2651)
    intrinsic, response, convolved = model(t)
    background = 0.025
    fig, axs = plt.subplots(1, 2, figsize=(10, 4.5), dpi=130, facecolor='white')
    axs[0].plot(t, intrinsic, '--', color='#18845b', label='Intrinsic decay')
    axs[0].plot(t, convolved, color='#2460a7', lw=2, label='Decay ∗ timing response')
    axs[0].plot(t, response/response.max(), ':', color='#7753a6', label='Response (unit peak)')
    axs[0].set(xlim=(-2.5, 15), ylim=(-0.02, 1.1), ylabel='Common signal units', title='Finite timing broadens the onset')
    axs[0].legend(loc='upper right', fontsize=9)
    axs[1].semilogy(t, convolved, color='#2460a7', lw=2, label='Background removed')
    axs[1].semilogy(t, convolved+background, color='#b65e0a', lw=2, label='With background')
    axs[1].axhline(background, color='#777777', ls=':', label='Background level')
    axs[1].set(xlim=(0, 24), ylim=(0.001, 1.1), ylabel='Common signal units (log scale)', title='Background flattens the tail')
    axs[1].legend(loc='upper right', fontsize=9)
    for ax in axs:
        ax.set_xlabel('Delay from excitation reference (ns)')
        ax.grid(alpha=0.2)
        ax.spines[['top', 'right']].set_visible(False)
    fig.suptitle('Synthetic model: lifetime 4 ns; Gaussian timing width σ = 0.6 ns', fontsize=14)
    fig.text(0.5, 0.025, 'No measured data. Background = 0.025; response centre = 0 ns. Only response display is peak-normalized.',
             ha='center', fontsize=9)
    fig.subplots_adjust(left=0.075, right=0.98, top=0.82, bottom=0.19, wspace=0.28)
    fig.savefig(Path(__file__).with_suffix('.png').name, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()