codex/amplifier-clipping-and-integrator-reset.py

amplifier-clipping-and-integrator-reset.py
"""Generate the ideal-model figure for clipping and finite integrator reset."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

t = np.linspace(-0.2, 1.2, 1401)  # milliseconds
active = (t >= 0) & (t < 1)
ideal = 4.0 * active
clipped = np.clip(ideal, -2.0, 2.0)
assert np.all(clipped[active] == 2)
assert np.all(clipped[~active] == 0)
assert 2 / 4 == 0.5

reset_us = np.linspace(0, 900, 1000)
residual_mv = 500 * np.exp(-reset_us / 100)
threshold_us = 100 * np.log(500)
assert abs(500 * np.exp(-threshold_us / 100) - 1) < 1e-12
assert abs(10e-12 / 10e-9 - 1e-3) < 1e-15

fig, ax = plt.subplots(1, 2, figsize=(9.0, 3.8), dpi=100, layout='constrained')
fig.set_facecolor('white')
ax[0].plot(t, ideal, '--', color='#777777', label='Unclipped: gain 10')
ax[0].plot(t, clipped, color='#1769aa', label='Output limited to 2 V')
ax[0].fill_between(t, 0, clipped, color='#1769aa', alpha=0.15)
ax[0].set(xlabel='Time (ms)', ylabel='Output (V)', title='Clipping loses pulse area', ylim=(-0.2, 4.8))
ax[0].text(0.5, 2.8, 'Half the ideal area remains', ha='center', fontsize=9)
ax[0].legend(loc='upper right', fontsize=8)

ax[1].semilogy(reset_us, residual_mv, color='#1769aa', label='Residual: 0.5 V initial value')
ax[1].axhline(1, color='#b24a20', ls='--', label='1 mV injection step (separate)')
ax[1].plot(threshold_us, 1, 'o', color='#1769aa', ms=5)
ax[1].annotate('621 µs to reach 1 mV', (threshold_us, 1), (260, 8),
               arrowprops={'arrowstyle': '->', 'color': '#444444'}, fontsize=9)
ax[1].set(xlabel='Time with reset switch closed (µs)', ylabel='Residual magnitude (mV)',
          title='Reset settling: time constant 100 µs', ylim=(0.05, 2000))
ax[1].legend(loc='upper right', fontsize=8)
for a in ax:
    a.grid(alpha=0.2)
fig.savefig(Path.cwd() / 'amplifier-clipping-and-integrator-reset.png', facecolor='white')
plt.close(fig)