codexrc-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()