codextunnel-diode-toy-load-line-stability.py
tunnel-diode-toy-load-line-stability.py
"""Original chosen cubic I-V and quasistatic load-line dynamics, not device data.
Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Run from _media.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
def current(x):
return x**3 - 6*x**2 + 10*x
def slope(x):
return 3*x**2 - 12*x + 10
def trajectory(x0, r, xs):
# tau = t I0 / (C V0). RK4, dimensionless time, no SciPy.
t = np.linspace(0, 8, 4001)
x = np.empty_like(t)
x[0] = x0
dt = t[1]-t[0]
def f(y):
return (xs-y)/r-current(y)
for k in range(len(t)-1):
a = f(x[k])
b = f(x[k]+dt*a/2)
c = f(x[k]+dt*b/2)
d = f(x[k]+dt*c)
x[k+1] = x[k]+dt*(a+2*b+2*c+d)/6
return t, x
def main():
plt.rcParams.update({'font.size': 11, 'figure.facecolor': 'white',
'axes.facecolor': 'white'})
fig, axes = plt.subplots(2, 2, figsize=(11, 7.8), gridspec_kw={'height_ratios': [1.3, 1]})
x = np.linspace(0, 3.6, 1801)
ndr = (slope(x) < 0)
cases = [(1., 6., [1., 2., 3.], r'$R=100\,\Omega,\ V_s=0.60\,\mathrm{V}$'),
(.25, 3., [2.], r'$R=25\,\Omega,\ V_s=0.30\,\mathrm{V}$')]
for col, (r, xs, roots, title) in enumerate(cases):
ax = axes[0, col]
ax.plot(.1*x, current(x), color='#1d5e99', lw=2.5, label='chosen device $I(V)$')
ax.plot(.1*x[ndr], current(x)[ndr], color='#ba502c', lw=3, label='$dI/dV<0$ segment')
ax.plot(.1*x, (xs-x)/r, '--', color='#574779', lw=1.8, label='supply line $(V_s-V)/R$')
for root in roots:
stable = 1/r+slope(root) > 0
ax.plot(.1*root, current(root), 'o', ms=8, mec='#168253' if stable else '#bb2525',
mfc='#168253' if stable else 'white', mew=2, zorder=5)
ax.annotate('stable' if stable else 'unstable', (.1*root, current(root)),
xytext=(-15, 17 if stable else -25), textcoords='offset points',
fontsize=10, color='#168253' if stable else '#bb2525')
ax.set(xlim=(0, .36), ylim=(0, 7), xlabel='Junction voltage $V$ (V)',
ylabel='Current $I$ (mA)', title=title)
ax.grid(alpha=.18)
if col == 0:
ax.legend(loc='upper right', fontsize=9, framealpha=.95)
ax = axes[1, col]
for x0, color in [(1.9, '#1d5e99'), (2.1, '#b76619')]:
t, y = trajectory(x0, r, xs)
ax.plot(t, .1*y, color=color, lw=2, label=f'$V(0)={.1*x0:.2f}$ V')
for root in roots:
ax.axhline(.1*root, color='#999999', ls=':', lw=1)
ax.set(xlim=(0, 8), ylim=(.08, .32), xlabel=r'Time $t$ ($\mu$s)',
ylabel='Junction voltage $V$ (V)')
ax.grid(alpha=.18)
ax.legend(loc='lower right' if col == 0 else 'upper right', fontsize=9)
ax.set_title('Two attracting states' if col == 0 else 'Negative-slope point is stable', fontsize=12)
fig.suptitle('A toy tunnel-diode curve: supply resistance determines local stability', fontsize=15, y=.99)
fig.text(.5, .92, r'$I=I_0(x^3-6x^2+10x),\ x=V/V_0;\quad V_0=0.10$ V, $I_0=1.0$ mA, $C=10$ nF',
ha='center', fontsize=12)
fig.text(.5, .024, r'$C\dot V=(V_s-V)/R-I(V)$; stable when $1/R+I^{\prime}(V)>0$. All values chosen; no measured curve or device fit.',
ha='center', fontsize=10)
fig.tight_layout(rect=(0, .05, 1, .91), h_pad=2.1, w_pad=2.4)
fig.savefig(Path.cwd() / (Path(__file__).stem+'.png'), dpi=120, facecolor='white')
plt.close(fig)
if __name__ == '__main__':
main()