r"""Electronic circuits: ideal operational amplifiers and the junction diode.
Ragazzini, Randall and Russell named the operational amplifier: a
high-gain amplifier whose negative feedback through resistors and
capacitors makes it add, scale and integrate voltages. Shockley's theory of
the p-n junction gave the diode its exponential law, which a load line
turns into an operating point.
"""
from __future__ import annotations
import numpy as np
from scipy.integrate import cumulative_trapezoid
from scipy.optimize import brentq
from engineeringkit._validation import finite as _finite
from engineeringkit._validation import positive as _positive
__all__ = [
"inverting_amplifier_gain",
"noninverting_amplifier_gain",
"op_amp_integrator",
"thermal_voltage",
"shockley_diode_current",
"diode_operating_point",
]
_BOLTZMANN = 1.380649e-23
_ELEMENTARY_CHARGE = 1.602176634e-19
[docs]
def inverting_amplifier_gain(r_feedback, r_input):
r"""Voltage gain :math:`v_o/v_i = -R_f/R_{in}` of an ideal inverting amplifier.
The ideal op amp draws no input current and, with negative feedback,
holds its inverting input at the grounded non-inverting one (a virtual
ground), so the input current :math:`v_i/R_{in}` all flows through
:math:`R_f`.
Parameters
----------
r_feedback : float or array_like
Feedback resistance :math:`R_f` [ohm], positive.
r_input : float or array_like
Input resistance :math:`R_{in}` [ohm], positive.
Returns
-------
float or ndarray
Voltage gain [-], negative.
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`.
References
----------
J. R. Ragazzini, R. H. Randall and F. A. Russell, "Analysis of problems
in dynamics by electronic circuits," *Proc. IRE* 35 (1947) 444-452.
Examples
--------
>>> inverting_amplifier_gain(10e3, 1e3)
-10.0
"""
_finite(r_feedback=r_feedback, r_input=r_input)
_positive(r_feedback=r_feedback, r_input=r_input)
return -r_feedback / r_input
[docs]
def noninverting_amplifier_gain(r_feedback, r_ground):
r"""Voltage gain :math:`v_o/v_i = 1 + R_f/R_g` of an ideal non-inverting amplifier.
Parameters
----------
r_feedback : float or array_like
Feedback resistance :math:`R_f` from the output to the inverting
input [ohm], nonnegative (zero gives the unity-gain follower).
r_ground : float or array_like
Resistance :math:`R_g` from the inverting input to ground [ohm],
positive.
Returns
-------
float or ndarray
Voltage gain [-], at least one.
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`.
Examples
--------
>>> noninverting_amplifier_gain(9e3, 1e3)
10.0
"""
_finite(r_feedback=r_feedback, r_ground=r_ground)
_positive(r_ground=r_ground)
if np.any(np.asarray(r_feedback) < 0):
raise ValueError("r_feedback must be nonnegative")
return 1 + r_feedback / r_ground
[docs]
def op_amp_integrator(v_in, t, R, C, v0=0.0):
r"""Output :math:`v_o(t) = v_0 - \frac{1}{RC}\int_0^t v_i\,dt'` of an ideal inverting integrator.
The virtual ground makes the input current :math:`v_i/R`, and it can
only go into the feedback capacitor. The integral of the sampled input
is taken by the trapezoidal rule.
Parameters
----------
v_in : array_like
Input voltage at each sample [V].
t : array_like
Sample times [s], strictly increasing, same length as ``v_in``
(at least two).
R : float
Input resistance [ohm], positive.
C : float
Feedback capacitance [F], positive.
v0 : float, optional
Output voltage at ``t[0]`` [V].
Returns
-------
ndarray
Output voltage [V] at each sample.
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`. A real integrator saturates at
its supply rails and drifts on its input offset; neither is modelled.
References
----------
J. R. Ragazzini, R. H. Randall and F. A. Russell, "Analysis of problems
in dynamics by electronic circuits," *Proc. IRE* 35 (1947) 444-452.
Examples
--------
>>> t = np.linspace(0, 1e-3, 11)
>>> round(float(op_amp_integrator(np.ones(11), t, 1e3, 1e-6)[-1]), 6)
-1.0
"""
v_in, t = np.asarray(v_in, dtype=float), np.asarray(t, dtype=float)
_finite(v_in=v_in, t=t, R=R, C=C, v0=v0)
_positive(R=R, C=C)
if t.ndim != 1 or t.size < 2 or np.any(np.diff(t) <= 0):
raise ValueError("t must be a strictly increasing time vector of at least two samples")
if v_in.shape != t.shape:
raise ValueError("v_in must have one sample per time")
return v0 - cumulative_trapezoid(v_in, t, initial=0.0) / (R * C)
[docs]
def thermal_voltage(temperature=300.0):
r"""Thermal voltage :math:`V_T = k_B T/q`.
Parameters
----------
temperature : float or array_like, optional
Junction temperature [K], positive.
Returns
-------
float or ndarray
Thermal voltage [V]; about 25.85 mV at 300 K.
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`.
Examples
--------
>>> round(thermal_voltage(300.0) * 1e3, 3)
25.852
"""
_finite(temperature=temperature)
_positive(temperature=temperature)
return _BOLTZMANN * temperature / _ELEMENTARY_CHARGE
[docs]
def shockley_diode_current(v, saturation_current, ideality=1.0, temperature=300.0):
r"""Shockley's diode equation :math:`i = I_S\left(e^{v/nV_T} - 1\right)`.
Parameters
----------
v : float or array_like
Voltage across the diode, anode to cathode [V], at most
:math:`40\,nV_T` (beyond that the exponential overflows any real
device's limits).
saturation_current : float
Reverse saturation current :math:`I_S` [A], positive.
ideality : float, optional
Ideality factor :math:`n` [-], positive (1 for Shockley's ideal
junction, up to about 2 for real silicon diodes).
temperature : float, optional
Junction temperature [K], positive.
Returns
-------
float or ndarray
Diode current [A]; tends to :math:`-I_S` under reverse bias.
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`. Reverse breakdown, series
resistance and high injection are not modelled.
References
----------
W. Shockley, "The theory of p-n junctions in semiconductors and p-n
junction transistors," *Bell Syst. Tech. J.* 28 (1949) 435-489.
Examples
--------
>>> round(float(shockley_diode_current(0.7, 1e-14)) * 1e3, 4)
5.7475
"""
_finite(v=v, saturation_current=saturation_current, ideality=ideality, temperature=temperature)
_positive(saturation_current=saturation_current, ideality=ideality)
scale = ideality * thermal_voltage(temperature)
v = np.asarray(v, dtype=float)
if np.any(v > 40 * scale):
raise ValueError("v must be at most 40 n V_T; limit the current with a series resistance")
return saturation_current * np.expm1(v / scale)
[docs]
def diode_operating_point(v_source, resistance, saturation_current, ideality=1.0, temperature=300.0):
r"""Operating point of a diode in series with a resistor: the load-line solution.
The resistor fixes the load line :math:`i = (V_s - v)/R`, and the
operating point is where it crosses the diode curve of
:func:`shockley_diode_current`. The crossing is found by bracketed root
finding (Brent's method), on the current for forward bias, where the
diode voltage :math:`nV_T\ln(1 + i/I_S)` stays bounded, and on the
voltage for reverse bias.
Parameters
----------
v_source : float
Source voltage driving the anode through the resistor [V].
resistance : float
Series resistance [ohm], positive.
saturation_current : float
Reverse saturation current [A], positive.
ideality : float, optional
Ideality factor [-], positive.
temperature : float, optional
Junction temperature [K], positive.
Returns
-------
tuple of float
``(v_diode, i_diode)``: diode voltage [V] and current [A].
Notes
-----
Inputs are raw SI numbers with no dimensional metadata; see
:doc:`/guides/electrical_engineering`. All parameters are scalar-only:
each operating point is one root find.
Examples
--------
>>> v, i = diode_operating_point(5.0, 1e3, 1e-14)
>>> round(v, 4), round(i * 1e3, 4)
(0.6925, 4.3075)
"""
_finite(v_source=v_source)
_positive(resistance=resistance, saturation_current=saturation_current, ideality=ideality, temperature=temperature)
scale = ideality * thermal_voltage(temperature)
if v_source > 0:
current = brentq(
lambda i: v_source - i * resistance - scale * np.log1p(i / saturation_current), 0.0, v_source / resistance, xtol=1e-18
)
return float(scale * np.log1p(current / saturation_current)), float(current)
if v_source < 0:
voltage = brentq(lambda v: saturation_current * np.expm1(v / scale) - (v_source - v) / resistance, v_source, 0.0, xtol=1e-15)
return float(voltage), float((v_source - voltage) / resistance)
return 0.0, 0.0