codex/rc-finite-pulse-threshold.py

rc-finite-pulse-threshold.py
"""Original ideal RC pulse-threshold calculation; times normalized by RC."""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def response(time, duration):
    return np.where(time <= duration, -np.expm1(-time),
                    -np.expm1(-duration) * np.exp(-(time - duration)))


def main():
    fraction = 0.5
    first = -np.log1p(-fraction)
    duration = 2.0
    last = duration + np.log(-np.expm1(-duration) / fraction)
    width = last - first
    assert np.isclose(response(first, duration), fraction)
    assert np.isclose(response(last, duration), fraction)
    assert np.isclose(response(first, first), fraction)
    assert response(0.4, 0.4) < fraction
    assert np.isclose(last, 2.5477337226910866)
    assert np.isclose(width, 1.8545865421311413)
    epsilon = 1e-6
    near = first + epsilon
    near_width = near + np.log(-np.expm1(-near) * (1 - fraction) / fraction)
    assert np.isclose(near_width / epsilon, 1 / fraction, rtol=2e-6)

    fig, (traces, widths) = plt.subplots(1, 2, figsize=(10, 4.3), dpi=100,
                                       layout="constrained")
    fig.set_facecolor("white")
    time = np.sort(np.r_[np.linspace(0, 5, 2001), 0.4, first, duration, last])
    for pulse, label, color in [
        (0.4, "T = 0.40τ: never reaches threshold", "#8a8a8a"),
        (first, "T = 0.693τ: touches at peak", "#7d903b"),
        (duration, "T = 2τ: finite interval above threshold", "#286f99"),
    ]:
        traces.plot(time, response(time, pulse), color=color, label=label)
        traces.scatter([pulse], [response(pulse, pulse)], color=color, s=20, zorder=5)
    traces.axhline(fraction, color="#9f4b27", linestyle="--", label="Threshold = 0.5 V₀")
    traces.scatter([first, last], [fraction, fraction], color="#286f99", s=32, zorder=6)
    traces.annotate("Time above threshold", xy=((first + last) / 2, fraction),
                    xytext=(1.6, 0.68), ha="center", fontsize=9,
                    arrowprops={"arrowstyle": "->", "color": "#444444"})
    traces.set(xlim=(0, 5), ylim=(0, 1), xlabel="Time / τ", ylabel="Output / input pulse height",
               title="A short pulse can be entirely rejected")
    traces.legend(fontsize=8, loc="upper right")
    pulse_widths = np.linspace(first, 4, 1001)
    high_widths = pulse_widths + np.log(-np.expm1(-pulse_widths) * (1 - fraction) / fraction)
    assert np.all(high_widths >= -1e-14)
    widths.plot(np.r_[0, pulse_widths], np.r_[0, high_widths], color="#286f99",
                label="Ideal time above threshold")
    widths.plot([0, 4], [0, 4], "--", color="#888888", label="Input pulse duration")
    widths.axvline(first, color="#7d903b", linestyle=":", label="Minimum duration: 0.693τ")
    widths.scatter([duration], [width], color="#286f99", zorder=5)
    widths.annotate("T = 2τ → W = 1.855τ", xy=(duration, width), xytext=(0.9, 2.8),
                    fontsize=9, arrowprops={"arrowstyle": "->", "color": "#444444"})
    widths.set(xlim=(0, 4), ylim=(0, 4.15), xlabel="Input pulse duration T / τ",
               ylabel="Above-threshold duration W / τ", title="The comparator pulse width also changes")
    widths.legend(fontsize=8, loc="upper left")
    for ax in (traces, widths):
        ax.grid(alpha=0.15)
    fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()