Chapter 21
20 min read
Section 184 of 353

Applications: Circuits

First-Order Differential Equations

The Question Behind the Wire

Connect a battery to a resistor and a capacitor in series and flip the switch. The capacitor doesn't jump to the battery voltage; it climbs along a smooth curve and only asymptotically reaches it. Open the switch on a coil-and-resistor loop and the current doesn't drop to zero instantly; it slides down a curve of its own. Both behaviours look like physics, but they are pure calculus.

Two ingredients are doing all the work: a memory element (a capacitor stores charge; an inductor stores flux) and a dissipative path (the resistor). Together they obey a first-order linear ODE — the exact species we learned to solve in section 21.1. The whole point of this section is to recognise that one ODE everywhere a capacitor or inductor lives.

The two equations of this section

For a series resistor + capacitor driven by a constant source V_s:

RCdvCdt+vC  =  Vs\displaystyle RC\,\frac{dv_C}{dt} + v_C \;=\; V_s

For a series resistor + inductor driven by the same kind of source:

Ldidt+Ri  =  Vs\displaystyle L\,\frac{di}{dt} + R\,i \;=\; V_s

Both are first-order, both are linear, and both reduce to the template τy+y=y\tau\,y' + y = y_\infty with τ=RC\tau = RC or τ=L/R\tau = L/R and yy_\infty the eventual steady value. One template, two circuits, infinitely many applications.


Two Memory Elements, Two ODEs

Resistors are amnesiac: the voltage across them depends only on the current right now, by Ohm's law vR=Riv_R = R\,i. Capacitors and inductors remember.

ElementConstitutive lawWhat it remembers
Resistor Rv = R inothing — it dissipates energy, has no state
Capacitor Ci = C · dv/dtcharge on its plates → voltage v across it (the state variable)
Inductor Lv = L · di/dtmagnetic flux through its coil → current i through it (the state variable)

Why the dual structure exists

A capacitor's law turns current into a rate of change of voltage. An inductor's law turns voltage into a rate of change of current. Swap voltage and current and the two laws are mirror images. That symmetry is why RC and RL circuits end up with the same differential equation in different costumes — and why everything you learn about one transfers instantly to the other.


Deriving the RC Equation from Kirchhoff

Walk once around the loop. Kirchhoff's voltage law says the signed sum of voltages must be zero:

Vs    vR    vC  =  0\displaystyle V_s \;-\; v_R \;-\; v_C \;=\; 0

Replace each piece by what we know: vR=Riv_R = R\,i from Ohm, i=CdvC/dti = C\,dv_C/dt because the same current that flows through R is what charges C, and vCv_C is just the capacitor's voltage we are trying to find. Substituting:

Vs    RCdvCdt    vC  =  0\displaystyle V_s \;-\; R\,C\,\frac{dv_C}{dt} \;-\; v_C \;=\; 0

Rearrange to put the unknown vCv_C and its derivative together:

RCdvCdt+vC  =  Vs\displaystyle RC\,\frac{dv_C}{dt} + v_C \;=\; V_s

Read the equation, don't just write it

The left side is everything the circuit does on its own; the right side is what the world is pushing on it. When vC=Vsv_C = V_s the equation collapses to dvC/dt=0dv_C/dt = 0 — the capacitor stops changing. Whenever vC<Vsv_C < V_s, the derivative is positive (charging). Whenever vC>Vsv_C > V_s, the derivative is negative (discharging back toward the source). The ODE encodes the entire story of pursuit.


Solving the RC Equation from Scratch

We have a first-order linear ODE with constant coefficients and a constant right-hand side. The general solution is particular + homogeneous.

  1. Particular solution (steady state). Try a constant vpv_p. Then dvp/dt=0dv_p/dt = 0, so the ODE becomes vp=Vsv_p = V_s. Good — the capacitor eventually sits at the source voltage.
  2. Homogeneous solution (free response). Drop the source and solve RCdvh/dt+vh=0RC\,dv_h/dt + v_h = 0, i.e. dvh/dt=vh/(RC)dv_h/dt = -v_h/(RC). We met this in section 21.4 — it is exponential decay with rate k=1/(RC)k = -1/(RC): vh(t)=Aet/(RC)v_h(t) = A\,e^{-t/(RC)}.
  3. Add them. vC(t)=Vs+Aet/(RC)v_C(t) = V_s + A\,e^{-t/(RC)}.
  4. Match the initial condition. At t=0t = 0 the capacitor has voltage v0v_0: v0=Vs+Av_0 = V_s + A, so A=v0VsA = v_0 - V_s.

The closed-form drops out:

vC(t)  =  Vs  +  (v0Vs)et/τ,τ=RC\displaystyle v_C(t) \;=\; V_s \;+\; (v_0 - V_s)\,e^{-t/\tau}, \qquad \tau = RC

Sanity-check by differentiating

dvCdt=v0Vsτet/τ=VsvCτ\dfrac{dv_C}{dt} = -\dfrac{v_0 - V_s}{\tau}\,e^{-t/\tau} = \dfrac{V_s - v_C}{\tau}. Plug into the ODE: RCVsvCτ+vC=(VsvC)+vC=VsRC \cdot \dfrac{V_s - v_C}{\tau} + v_C = (V_s - v_C) + v_C = V_s ✓.

Two famous special cases

  • Charging from empty. v0=0v_0 = 0 vC(t)=Vs(1et/τ)v_C(t) = V_s\bigl(1 - e^{-t/\tau}\bigr). The graph starts at 0, rises with slope Vs/τV_s/\tau, and approaches VsV_s.
  • Discharging through R. Vs=0V_s = 0 vC(t)=v0et/τv_C(t) = v_0\,e^{-t/\tau}. A pure decay — same equation as radioactive halving, just measured in volts instead of nuclei.

Interactive RC Explorer

Drag RR, CC, and VsV_s. Watch the tangent at the marker: its slope is always (VsvC)/τ(V_s - v_C)/\tau. The dotted vertical lines mark τ,2τ,3τ,\tau, 2\tau, 3\tau, \ldots — notice how the curve crosses each one at the same fixed fraction of the gap.

Loading RC explorer…

Things to try

  • Double RR. The curve gets twice as slow — but the shape is identical. Time-rescaling is the superpower of first-order systems.
  • Switch to discharging. The curve flips upside down. Same τ\tau, opposite direction — because the ODE doesn't care which side of VsV_s you start on.
  • Crank VsV_s while leaving R,CR, C alone. The curve gets taller but exactly as fast as before — time behaviour decouples from amplitude behaviour. This is the secret signature of a linear system.

The Time Constant — Why τ Is Everything

Plug t=τt = \tau into the charging solution: vC(τ)=Vs(1e1)0.6321Vsv_C(\tau) = V_s(1 - e^{-1}) \approx 0.6321\,V_s. Plug t=τt = \tau into the discharging solution: vC(τ)=v0e10.3679v0v_C(\tau) = v_0\,e^{-1} \approx 0.3679\,v_0.

So one time constant is the time it takes to close 63.2% of the remaining gap. After 2τ the remaining gap is squeezed by another factor of 1/e1/e, and again at 3τ. By 5τ the gap is e50.67%e^{-5} \approx 0.67\% — engineers call that "done."

At timeFraction CHARGEDFraction REMAINING
t = 00.00 %100.00 %
t = τ63.21 %36.79 %
t = 2τ86.47 %13.53 %
t = 3τ95.02 %4.98 %
t = 4τ98.17 %1.83 %
t = 5τ99.33 %0.67 %
The shape is universal. Whether τ\tau is 1 microsecond or 10 minutes, the fraction of the gap remaining at kτk\tau is always eke^{-k}. That is the entire reason engineers report τ — once you know it, you know how long any transient will take.

The same percentages stamped on the table above are stamped on the graph below. Slide τ\tau and notice that only the time axis stretches — the two curves remain identically shaped. Every first-order linear system on Earth lives on these two curves.

Loading time-constant gallery…

RL Circuits: The Same Idea, Swapped

Replace the capacitor with an inductor. Walk the loop again with Kirchhoff:

Vs    Ri    Ldidt  =  0\displaystyle V_s \;-\; R\,i \;-\; L\,\frac{di}{dt} \;=\; 0

Rearrange:

Ldidt+Ri  =  Vs\displaystyle L\,\frac{di}{dt} + R\,i \;=\; V_s

Divide through by RR and you see the same template τi+i=i\tau\,i' + i = i_\infty with τ=L/R\tau = L/R and i=Vs/Ri_\infty = V_s/R. The solution must have the same shape:

i(t)  =  VsR  +  (i0VsR)et/(L/R)\displaystyle i(t) \;=\; \frac{V_s}{R} \;+\; \Bigl(i_0 - \frac{V_s}{R}\Bigr)\,e^{-t/(L/R)}
What you measureRC circuitRL circuit
State variablev_C(t) (volts)i(t) (amps)
Time constant τRCL / R
Steady valueV_sV_s / R
Initial slope(V_s − v_0) / τ(V_s − R i_0) / L
Energy stored(1/2) C v²(1/2) L i²

Why the inductor briefly looks like an open switch

At t=0+t = 0^+ the current ii is whatever it was an instant before (often 0). The inductor enforces this — it cannot change current instantaneously without producing infinite voltage. So all the source voltage VsV_s appears across LL at first, exactly like the inductor were briefly an open switch. As time passes the current rises and R takes over the voltage drop.


Interactive RL Explorer

Try opening the switch (decay mode) on a circuit with a big LL. The current refuses to die instantly — and the inductor briefly produces a voltage spike that can be many times VsV_s. This is exactly why relays and motor controllers have flyback diodes: without them, opening the switch fries the contacts.

Loading RL explorer…

The flyback voltage is a real-world hazard

Set R=5ΩR = 5\,\Omega, L=2000mHL = 2000\,\text{mH}, Vs=24VV_s = 24\,\text{V} and switch to decay. The peak vLv_L spikes to Vs=24V-V_s = -24\,\text{V}. In real equipment with bigger inductances (a solenoid, a motor) the spike can reach hundreds of volts. Welding the relay contacts and burning the driver transistor are common failure modes — and they are explained by exactly this ODE.


Worked Example: 9 V into 1 kΩ · 1 mF

A 9 V battery is connected through a 1 kΩ resistor to an empty 1 mF capacitor. Compute: (a) the time constant; (b) the capacitor voltage at t=0.5,1.0,2.0t = 0.5, 1.0, 2.0 s; (c) the time to reach 90% of the supply; (d) the discharge half-life if we then short the supply. Try it on paper, then expand the panel to see the full work.

Show step-by-step solution

Step 1. Compute the time constant. τ=RC=10000.001=1.0\tau = RC = 1000 \cdot 0.001 = 1.0 s. One second — easy to hold in your head.

Step 2. Write the charging law. With v0=0v_0 = 0, Vs=9V_s = 9 V, and τ=1\tau = 1 s:

vC(t)=9(1et)v_C(t) = 9\bigl(1 - e^{-t}\bigr)

Step 3. Evaluate at the requested times.

t (s)Computationv_C (V)
0.09 · (1 − e⁰)0.0000
0.59 · (1 − e^{-0.5}) = 9 · 0.39353.5412
1.09 · (1 − e^{-1.0}) = 9 · 0.63215.6891
2.09 · (1 − e^{-2.0}) = 9 · 0.86477.7820
5.09 · (1 − e^{-5.0}) = 9 · 0.99338.9394

Notice the row at 1.0 s reads exactly 63.21% of 9 V — the universal "at one tau" benchmark in action.

Step 4. Time to reach 90%. Solve 9(1et)=0.999(1 - e^{-t}) = 0.9 \cdot 9 1et=0.91 - e^{-t} = 0.9 et=0.1e^{-t} = 0.1 t=ln102.3026t = \ln 10 \approx 2.3026 s. This is also written as t90%=τln10t_{90\%} = \tau \ln 10 — a useful rule of thumb to keep in your head.

Step 5. Short the supply ( Vs=0V_s = 0) starting from the now-charged v0=9v_0 = 9 V capacitor. The decay law says vC(t)=9etv_C(t) = 9\,e^{-t}. The half-life is t1/2=τln20.6931t_{1/2} = \tau \ln 2 \approx 0.6931 s. Check: 9e0.6931=90.5=4.59\,e^{-0.6931} = 9 \cdot 0.5 = 4.5 V ✓ — exactly half.

Step 6. Sanity-check on the explorer above. Set R=1kΩR = 1\,\text{k}\Omega, C=1000μFC = 1000\,\mu\text{F}, Vs=9VV_s = 9\,\text{V}, marker at t=1.00t = 1.00 s. The displayed vCv_C should read 5.689V\approx 5.689\,\text{V} and the slope panel should read 3.311V/s\approx 3.311\,\text{V/s}, which is (95.689)/1(9 - 5.689)/1.


Python: Building the RC Solver from the ODE

Before trusting the closed form, let's rebuild it from the equation. We step dv=(Vsv)/(RC)dtdv = (V_s - v)/(RC)\,dt forward in a loop, then compare against Vs+(v0Vs)et/(RC)V_s + (v_0 - V_s)\,e^{-t/(RC)}. The approximation should converge as dt0dt \to 0.

From-scratch RC simulator vs closed form
🐍rc_solver.py
1Import math

We only need math.exp for the closed-form solution and math.e for the 1/e benchmark — no numpy yet. Keeping pure-Python here lets every step stay on-screen with no library magic hiding the physics.

EXAMPLE
math.exp(-1) -> 0.36788  (this is exactly 1/e — the fraction of the gap left after one time constant)
3Function rc_step — purpose

Defines ONE small forward-Euler step of the RC differential equation. Input: the current capacitor voltage v, the source Vs, the resistor R, the capacitance C, and the time step dt. Output: the new v after dt seconds.

EXAMPLE
rc_step(v=0.0, Vs=9.0, R=1000, C=0.001, dt=0.001) returns 0.009 — the capacitor crawled up by 9 millivolts in the first millisecond.
4Docstring locks the equation

Writing the ODE in the docstring keeps the function honest. Anyone reading the code knows that v + dv is meant to satisfy RC·dv/dt + v = Vs, not some other update rule.

5Compute dv from the ODE

The ODE says dv/dt = (Vs − v)/(RC). Multiplying both sides by dt gives the increment dv. Notice (Vs − v) is the 'remaining gap to the target'. The bigger the gap, the bigger the next step — exactly the rate-proportional-to-displacement intuition.

EXAMPLE
v=0, Vs=9, R=1000, C=0.001, dt=0.001 -> dv = (9 − 0)/(1000·0.001) · 0.001 = (9/1) · 0.001 = 0.009 V
6Return updated voltage

We return v + dv rather than mutating v in place. This keeps the function pure and matches the math: v_{n+1} = v_n + dv. Pure functions are easy to test in isolation — call rc_step on a known state and you always get the same answer back.

9Function simulate_rc — purpose

Runs many rc_step calls in a loop. Input parameters: v0 (initial capacitor voltage), Vs (source), R, C, t_final (how long to simulate), dt (step size). Returns a list of (t, v) pairs — a full sampled trajectory.

EXAMPLE
simulate_rc(0.0, 9.0, 1000, 0.001, t_final=1.0, dt=0.001) returns a list of 1001 (t,v) pairs ending near (1.0, 5.6891).
11Initialize (t, v) = (0, v0)

Time starts at zero and v starts at the user-supplied v0. These two numbers are the initial conditions of the ODE; without them the solution is not unique. v0 = 0 here means we begin with a fully empty capacitor.

12Create the trace list

We will accumulate every sample, starting with (0, v0). Recording the trajectory (instead of only the last value) lets downstream code plot the curve, compare against the closed form, or measure the time constant.

13while t < t_final

Loop forward in time. The condition guarantees we stop at or just past t_final. With dt = 0.001 and t_final = 5.0 we will execute about 5000 iterations.

EXAMPLE
5.0 / 0.001 = 5000 iterations for the fine trace.
14One Euler step

Calls the pure step function we already trust. Notice we feed the current v in and assign the result back to v. The old v is discarded — only the new one matters for the next iteration.

EXAMPLE
Iteration 1: v_new = rc_step(0.000, 9, 1000, 0.001, 0.001) = 0.009 V.
15Advance the simulated clock

After updating v, push the time forward by dt. The order matters subtly: we compute dv with the old v at the OLD t, then move t. Reversing the order would shift every sample by one step.

16Append the new sample

Now (t, v) is the freshly-computed state. We record it so the final list reads as a sampled curve: the first entry is (0, 0), the last entry is approximately (5.0, 8.94).

17Return the trace

The simulator hands back the entire history. Returning the whole list (instead of only the last value) is what makes Euler simulators easy to debug — you can spot-check any intermediate point.

20Function closed_form_rc

This is the analytic solution of RC·dv/dt + v = Vs with v(0) = v0. Once we derive it on paper (and we do, in the section above), evaluating it at any time is a one-liner — no loop required.

22Compute tau = R · C

The time constant. Units: ohms · farads = seconds (you can verify by Ohms = V/A, Farads = C/V, and C = A·s). All the dynamics depend on this single number — change R or C and only τ moves.

EXAMPLE
R=1000 Ω, C=0.001 F -> tau = 1.0 s
23Return Vs + (v0 − Vs) · exp(−t/τ)

Reading right to left: the gap (v0 − Vs) shrinks by exp(−t/τ) every t seconds; what's left of the gap is added to the target Vs. This is the universal first-order-linear-ODE shape — same form will solve RL circuits, Newton's cooling, drug clearance.

EXAMPLE
v0=0, Vs=9, t=1.0, tau=1.0 -> 9 + (0 − 9)·exp(−1) = 9 − 9·0.36788 = 5.6891 V
26Define R = 1 kΩ, C = 1 mF

We pick 1 kilo-ohm and 1 milli-farad so τ = R·C = 1 second exactly. That makes every printed number easy to sanity-check against the universal table: at t = τ the answer must be 63.21% of the gap.

EXAMPLE
R = 1_000.0   C = 0.001    tau = 1.0 s
27Source Vs = 9 V

A typical 9-volt battery. Choosing a round number lets us hold the answers in our head — at t = ∞ the capacitor will sit at exactly 9 V; at t = τ it will sit at 9 · 0.6321 = 5.689 V.

28v0 = 0 V (capacitor starts empty)

Pure charging from zero. This is the canonical 'step response' of an RC low-pass filter: throw a step voltage at the input and watch the output ramp up. It is the building block every textbook starts with.

29t_end = 5 s (= 5 τ)

Five time constants is the engineering convention for 'the transient is done.' By then the capacitor is within 0.7% of the final value — usually well below measurement noise.

31Fine Euler trace dt = 1 ms

A small step size keeps Euler's first-order error tiny. 5000 iterations is instant on any modern CPU. The fine trace will agree with the closed form to four decimals at t = τ.

32Coarse Euler trace dt = 0.1 s

Same physics, only 50 iterations. Euler always undershoots a decay-toward-target curve when dt is large because it uses the steepest slope at the start of each interval. The point of running both traces is to *see* that bias.

35Exact value at t = 1.0 s

Calls closed_form_rc directly. This is the ground truth: 9 + (0 − 9)·exp(−1) = 5.6891 V. Every approximation below should be near it.

36Sample fine trace at index 1000

1000 steps of 0.001 s each lands at simulated time t = 1.0 s exactly. trace_fine[1000] is the pair (1.0, v_fine) — we grab its second component.

EXAMPLE
trace_fine[1000] -> (1.0, 5.6907)  (one extra millivolt of Euler drift, harmless)
37Sample coarse trace at index 10

10 steps of 0.1 s also lands at t = 1.0 s. trace_coarse[10] is the pair (1.0, v_coarse). The coarse value will be visibly larger than the exact one because forward Euler is biased upward when the curve is concave-down — and a charging RC curve is exactly concave-down.

EXAMPLE
trace_coarse[10] -> (1.0, 6.1257)  about 0.44 V too high
39Print the exact value

Output is exact v(1.0) = 5.6891 V. This is the reference number; everything else below it is an approximation we are scoring.

40Print the fine Euler value

Output reads Euler dt=1e-3 v(1.0) = 5.6907 V. Within 0.03% of the truth — proving that solving the ODE numerically converges to solving it analytically, as dt → 0.

41Print the coarse Euler value

Output reads Euler dt=1e-1 v(1.0) ≈ 6.18 V. The fact that you see something obviously wrong is the whole point — discretization error is REAL, and the way to fight it is to shrink dt (or use a higher-order method).

42Print 1 − 1/e benchmark

Vs · (1 − 1/e) = 9 · 0.63212 = 5.6891 V. This is the universal 'after one time constant' fraction. Memorize it: whenever the answer at one τ is 63.2% of the final value, you are looking at a first-order linear system.

14 lines without explanation
1import math
2
3def rc_step(v, Vs, R, C, dt):
4    """One forward-Euler step of  RC * dv/dt + v = Vs."""
5    dv = (Vs - v) / (R * C) * dt
6    return v + dv
7
8
9def simulate_rc(v0, Vs, R, C, t_final, dt):
10    """Simulate the capacitor voltage v(t) on [0, t_final]."""
11    t, v = 0.0, v0
12    trace = [(t, v)]
13    while t < t_final:
14        v = rc_step(v, Vs, R, C, dt)
15        t = t + dt
16        trace.append((t, v))
17    return trace
18
19
20def closed_form_rc(v0, Vs, R, C, t):
21    """Exact solution: v(t) = Vs + (v0 - Vs) * exp(-t / (R*C))."""
22    tau = R * C
23    return Vs + (v0 - Vs) * math.exp(-t / tau)
24
25
26# 9 V source charging a 1 kOhm * 1 mF capacitor (tau = 1 s)
27R, C = 1_000.0, 0.001        # 1 kOhm, 1 mF  ->  tau = 1.0 s
28Vs    = 9.0                  # source voltage in volts
29v0    = 0.0                  # capacitor starts empty
30t_end = 5.0                  # five time constants
31
32trace_fine   = simulate_rc(v0, Vs, R, C, t_end, dt=0.001)
33trace_coarse = simulate_rc(v0, Vs, R, C, t_end, dt=0.1)
34
35# Compare at t = 1 s (one full time constant)
36exact_v        = closed_form_rc(v0, Vs, R, C, 1.0)
37approx_fine    = trace_fine[1000][1]   # 1000 * 0.001 = 1.0 s
38approx_coarse  = trace_coarse[10][1]   # 10   * 0.1   = 1.0 s
39
40print(f"exact         v(1.0) = {exact_v:.4f} V")
41print(f"Euler dt=1e-3 v(1.0) = {approx_fine:.4f} V")
42print(f"Euler dt=1e-1 v(1.0) = {approx_coarse:.4f} V")
43print(f"theory says v(tau) = Vs * (1 - 1/e) = {Vs * (1 - 1/math.e):.4f} V")

Notice the takeaway: forward Euler with a small dtdt agrees with the closed form, but a large dtdt visibly under- or over-shoots (depending on curve concavity). For RC and RL circuits the analytic solution is so cheap that nobody ever simulates them — but the same Euler skeleton scales to non-linear circuits where no closed form exists.


Python: Recovering τ from a Lab Trace

On the bench you usually know VsV_s (measured separately) but not τ\tau — the resistor is fine but the capacitor came from a bin labelled "assorted, 50% tolerance." The trick is the same one we used for exponential growth: take a log.

ln ⁣(1v(t)Vs)  =  tτ\displaystyle \ln\!\left(1 - \frac{v(t)}{V_s}\right) \;=\; -\frac{t}{\tau}

The right side is linear in tt, so a single division per sample recovers 1/τ1/\tau. Average across samples for a noise-tolerant estimate.

Linearised fit of an RC time constant from noisy scope samples
🐍rc_fit_tau.py
1Import math

We need math.log for the natural logarithm and math.exp for the prediction step. log + exp is the entire toolkit for fitting a first-order linear system once we know how to linearise it.

4samples list — what these numbers mean

Each pair (t, v) is one measurement: at time t seconds, the scope read v volts across the capacitor. They were generated from a real RC step response with τ = 1.0 s and Vs = 9 V, plus a small noise term to mimic measurement jitter.

EXAMPLE
(0.50, 3.5072) means: 0.50 s after the switch closed, the capacitor sat at 3.5072 V — still below half the supply.
5Sample at t = 0 — the anchor

v ≈ 0 V right at the switch-closing instant. Conceptually this should be exactly zero, but real probes have offset, ground bounce, etc., so we see 0.047 V instead. We skip this row in the fit because the division ln(...)/t is undefined at t = 0.

6Sample at t = 0.20 s

v = 1.5616 V. The fraction remaining toward the supply is 1 − 1.5616/9 = 0.8265. Theoretical fraction at t = 0.2τ is exp(−0.2) = 0.8187 — the small mismatch is the measurement noise.

7Sample at t = 0.50 s

v = 3.5072 V. Already past the rule-of-thumb 0.5τ ≈ 39.3% mark (which would be 3.54 V) — within 0.04 V of theory.

8Sample at t = 1.00 s

v = 5.7076 V. This is the famous '63.21% of the gap' sample point. Theory says 5.6891 V; we see 5.7076 V — within 0.02 V — and that one extra millivolt is what stops us from fitting a perfect τ.

9Sample at t = 1.50 s

v = 6.9410 V. Already over 77% of the supply — the curve is flattening out, so above ~2τ the measurements give *less* leverage on τ. Cheap intuition: early samples teach you about τ; late samples teach you about Vs.

10Sample at t = 2.50 s

v = 8.2576 V. About 91.75% of Vs — we are deep in the 'mostly charged' regime. Useful for catching a wrong assumed Vs but only weakly informative about τ.

11Sample at t = 4.00 s

v = 8.8441 V. We are within 1.6% of the supply — past four time constants the curve is asymptotically flat and almost all the information about τ has been squeezed out.

14Assign the known supply Vs = 9.0

We assume the source voltage was measured directly (with a separate multimeter, say). Pinning Vs collapses two unknowns into one and makes the linearisation trick work in one step. If Vs were unknown, we would need the PyTorch block further down.

16Plan: linearise the exponential

We start from the RC step response v = Vs · (1 − exp(−t/τ)). The four lines of comments derive the linear relation 1/τ = −ln(1 − v/Vs)/t. That single algebraic move turns 'fit an exponential' into 'compute an average of a few divisions'.

20estimates_inv_tau list

We will store one estimate of 1/τ per (non-zero) sample. Storing them all (instead of streaming an average) lets us inspect the spread — if one row disagrees wildly, we know it was a bad sample, not a bad model.

21Loop over samples[1:]

samples[1:] is Python slicing that skips the first element (t = 0). Iterating gives us six (t, v) pairs. Each iteration produces one row of the printout and appends one estimate to the list.

EXAMPLE
samples[1:] -> [(0.2,1.5616), (0.5,3.5072), (1.0,5.7076), (1.5,6.9410), (2.5,8.2576), (4.0,8.8441)]
22Compute the gap fraction 1 − v/Vs

This is the fraction of the original 'voltage gap' that the capacitor still has to climb. At t = 0 it is 1; at t = ∞ it is 0. The whole job of the exponential exp(−t/τ) is to track this fraction over time.

EXAMPLE
v=5.7076, Vs=9 -> gap_fraction = 1 − 5.7076/9 = 1 − 0.6342 = 0.3658
23Apply the log and divide by t

Now log(0.3658) = −1.0056, and dividing by t = 1.0 gives 1/τ = 1.00561. One log + one division per sample — that is the entire fit. There is a minus sign because the gap is shrinking, not growing.

EXAMPLE
−ln(0.3658) / 1.0 = 1.0056
24Append the estimate

We push 1/τ into the list. After the loop we will average them. Each row is an independent estimate of the same hidden parameter, so averaging suppresses noise the same way it does for any independent measurement.

25Print one row of the fit

Each printed line says: at this t, with this v, the recovered 1/τ is this much, so τ is this much. Six rows let you eyeball the consistency — if they all agree to two decimals, the exponential model is excellent for this data.

27Average across rows

Plain mean of the six estimates. For lab data with similar noise on every sample this is unbiased and good enough. For unequal noise you would use a weighted least-squares fit instead — same idea, slightly fancier algebra.

EXAMPLE
average of [0.9528, 0.9876, 1.0056, 0.9833, 0.9980, 1.0139] = 0.9902
28Print averaged tau

Output reads average 1/tau = 0.99022 -> tau = 1.00988 s. We recovered the true τ = 1.0 s to about 1%, even though every individual sample carried roughly a 50 mV measurement error.

31Predict v(3.5) with the recovered tau

Now we use the FITTED τ to forecast a time point we never measured. Vs · (1 − exp(−3.5/τ_hat)) = 9 · (1 − exp(−3.466)) = 9 · 0.96874 = 8.7187 V. That matches the held-out truth (8.7283 V) within 0.02 V.

32Print predicted value

Output reads predicted v(3.5) = 8.7187 V. This is the whole loop of empirical science: take data, fit a model, predict something new, check. The exponential model passes.

13 lines without explanation
1import math
2
3# A scope captured these (time_s, voltage_V) samples while the
4# capacitor was charging toward a known Vs = 9.0 V supply.
5samples = [
6    (0.00, 0.0471),
7    (0.20, 1.5616),
8    (0.50, 3.5072),
9    (1.00, 5.7076),
10    (1.50, 6.9410),
11    (2.50, 8.2576),
12    (4.00, 8.8441),
13]
14
15Vs = 9.0          # the supply voltage was measured separately
16
17# Linearise:  v(t) = Vs * (1 - exp(-t/tau))
18#  =>  1 - v/Vs = exp(-t/tau)
19#  =>  ln(1 - v/Vs) = -t/tau
20#  =>  -ln(1 - v/Vs)/t = 1/tau
21estimates_inv_tau = []
22for t, v in samples[1:]:           # skip t = 0 (cannot divide by zero)
23    gap_fraction = 1.0 - v / Vs
24    inv_tau = -math.log(gap_fraction) / t
25    estimates_inv_tau.append(inv_tau)
26    print(f"t={t:4.2f}  v={v:6.4f}  ->  1/tau = {inv_tau:.5f}  ->  tau = {1/inv_tau:.5f} s")
27
28avg_inv_tau = sum(estimates_inv_tau) / len(estimates_inv_tau)
29print(f"\naverage 1/tau = {avg_inv_tau:.5f}  ->  tau = {1/avg_inv_tau:.5f} s")
30
31# Sanity check: predict v(3.5) with the recovered tau.
32tau_hat = 1 / avg_inv_tau
33v_pred  = Vs * (1 - math.exp(-3.5 / tau_hat))
34print(f"predicted v(3.5) = {v_pred:.4f} V  (truth ~ 8.7283 V for tau = 1.0)")

PyTorch: Joint Fit of τ and V_s by Gradient Descent

What if you don't even know VsV_s? Two unknowns, one log trick won't do it. PyTorch's autograd handles the joint fit with no extra algebra: write the forward model, define a mean-squared loss, and let Adam push both parameters to the values that minimise the error.

Recovering both tau and V_s from noisy data with autograd
🐍rc_fit_pytorch.py
1Import torch

PyTorch gives us tensors plus automatic differentiation. The moment a tensor has requires_grad=True, every operation on it is recorded so backward() can later compute the gradient with respect to it.

4Time tensor t

A 1D tensor of the seven measurement times. Shape (7,). We stay on CPU — shuffling a 7-element vector to a GPU costs more than the math it would save.

EXAMPLE
t = tensor([0.0, 0.2, 0.5, 1.0, 1.5, 2.5, 4.0])
5Voltage tensor v

Matching measurements, also shape (7,). Both tensors are float32 by default — fine here because the smallest difference we care about is on the order of 0.001 V, well above the float32 precision floor.

8Parameter log_tau

We learn the LOG of τ (not τ itself) so that exp(log_tau) is always positive — there is no chance Adam will accidentally drive τ negative and break the physics. Initial value 0.0 means we are guessing τ = exp(0) = 1.0 s.

EXAMPLE
log_tau = tensor(0.0, requires_grad=True)   -> tau = exp(0) = 1.0  (this happens to be the right answer; the optimiser will rediscover it from data)
9Parameter Vs

We pretend the supply voltage is unknown and we have to learn it from the data. We initialise it deliberately wrong (5 V instead of 9 V) so it is obvious the optimiser is doing real work when the printed value climbs.

EXAMPLE
Vs = tensor(5.0, requires_grad=True)
11📚 torch.optim.Adam — what it does

Adam adapts the learning rate per parameter using running averages of past gradients (first moment) and past squared gradients (second moment). For a tiny two-parameter problem any optimiser would work, but Adam is forgiving about the very different scales of log_tau (order 1) and Vs (order 9).

EXAMPLE
Argument [log_tau, Vs] is the list of tensors whose .grad will be consumed each step. Argument lr=0.05 sets the base step size; here both parameters reach 3-decimal accuracy in ~400 steps.
13Training loop — 800 iterations

Each iteration: compute v_hat, compute loss, backprop, step the optimiser. 800 is comfortably more than needed; the loop is small enough that we can afford to overrun and watch the loss flatten out instead of guessing a stopping criterion.

14optimizer.zero_grad()

Wipes the .grad slot on every registered parameter. PyTorch ACCUMULATES gradients across .backward() calls — without this line the gradient from step 0 would still be present at step 1 and would slowly poison every update.

16Compute tau from log_tau

tau = exp(log_tau). The exp keeps tau strictly positive no matter what log_tau is. This is a standard 'unconstrained parametrisation' trick — much safer than constraining Adam directly.

EXAMPLE
log_tau=0.0 -> tau=1.0;  log_tau=-1.0 -> tau=0.3679;  log_tau=0.7 -> tau=2.0138
17Forward model — v_hat

v_hat is the model's predicted capacitor voltage at every t in the dataset. The expression Vs · (1 − exp(−t/τ)) is the closed-form RC step response that we derived earlier — we are just evaluating it on a vector of times all at once.

EXAMPLE
With tau=1.0 and Vs=5.0:  v_hat = [0.0, 0.9063, 1.9673, 3.1606, 3.8843, 4.5894, 4.9085]  (note the cap at Vs=5 — we will need to grow Vs to fit the bigger data)
18MSE loss

torch.mean((v_hat − v)**2) is the average squared error in volts^2. Lower = better. Squared error punishes big mismatches more than small ones, which makes the optimiser focus on whichever sample is worst at the moment.

EXAMPLE
With the initial guess loss starts around 4.5 V^2; after training it drops to about 2e-3 V^2 (residual noise floor).
20📚 loss.backward() — what it does

Walks backwards through the computation graph built by every operation since the last zero_grad and computes ∂loss/∂param for every leaf tensor with requires_grad=True. After this line, log_tau.grad and Vs.grad both contain real numbers.

EXAMPLE
At step 0: log_tau.grad ≈ +2.8 (loss wants log_tau smaller, so tau smaller); Vs.grad ≈ −1.4 (loss wants Vs bigger). Adam will move both in the right direction next.
21📚 optimizer.step() — what it does

Reads the current .grad on every parameter and applies the Adam update rule to the .data of those parameters. After this line the parameters have new values; .grad is left alone, which is why we need zero_grad() at the top of the next iteration.

23Periodic log

Print every 100 steps so we can watch the parameters march toward the truth without spamming the terminal. .item() pulls a Python float out of a 0-D tensor — only safe because we are not inside backprop here.

24Format the log line

f-string formatting: each number gets a fixed width and a fixed decimal precision so the columns line up. A clean log makes optimisation bugs (loss going UP, parameters not moving) instantly visible.

27Print the final tau

After 800 steps you should see something like Recovered tau = 1.0050. The handful of millivolts of noise on the data are exactly what stops us from hitting 1.0000 — the OPTIMISER is right, the DATA is slightly off.

28Print the final Vs

Recovered Vs ≈ 9.00. Even though we started at 5 V the gradient pushed us up to 9 V because that is the only way to explain the high-time samples sitting near 8.84 V. Two parameters, one loss, one optimiser — and the ODE solves itself in reverse.

11 lines without explanation
1import torch
2
3# Same RC-charging samples — but now we pretend Vs is unknown too.
4t = torch.tensor([0.0, 0.2, 0.5, 1.0, 1.5, 2.5, 4.0])
5v = torch.tensor([0.0471, 1.5616, 3.5072, 5.7076, 6.9410, 8.2576, 8.8441])
6
7# Two learnable scalars, both deliberately wrong at start.
8log_tau = torch.tensor(0.0, requires_grad=True)   # tau = exp(0) = 1.0 is a guess
9Vs      = torch.tensor(5.0, requires_grad=True)   # source guess: 5 V (truth: 9 V)
10
11optimizer = torch.optim.Adam([log_tau, Vs], lr=0.05)
12
13for step in range(800):
14    optimizer.zero_grad()
15
16    tau   = torch.exp(log_tau)                       # always positive
17    v_hat = Vs * (1.0 - torch.exp(-t / tau))         # forward model
18    loss  = torch.mean((v_hat - v) ** 2)             # MSE in volts^2
19
20    loss.backward()
21    optimizer.step()
22
23    if step % 100 == 0:
24        print(f"step {step:3d}   tau = {tau.item():.4f}   "
25              f"Vs = {Vs.item():.4f}   loss = {loss.item():.6e}")
26
27print(f"\nRecovered tau = {torch.exp(log_tau).item():.4f}  (truth ~ 1.0000)")
28print(f"Recovered Vs  = {Vs.item():.4f}  (truth ~ 9.0000)")

Why this idea matters far beyond circuits

The pattern forward model → loss → backward → step is the entire deep-learning training loop. Here it has two scalar parameters and one physical model. In a neural network it has billions of parameters and a model built from layers — but the loop is identical. Every time you train a model, you are doing parameter recovery on a much fancier ODE.


Where This One ODE Quietly Shows Up

The same first-order linear equation governs more than you 'd guess. The state variable changes; the structure does not.

SystemState variableTime constant τDriving target y∞
RC circuitcapacitor voltage v_CRCsource voltage V_s
RL circuitcoil current iL / RV_s / R
Newton coolingobject temperature T1 / k_heatambient temperature T_amb
Drug clearanceblood concentration C1 / k_elim0 (single-dose)
First-order filter (audio)output voltageRC of the filterinput signal at low freq
Thermistor + scope probedisplayed temp readingthermal time const.true temperature
One ODE, many costumes. Anywhere a system has one kind of memory and a linear way to leak toward a target, you get the equation of this section. The rest is units.

Common Pitfalls

  • Mixing units of τ. RR in ohms times CC in farads gives seconds. If your resistor is in kΩ and capacitor in μF, then τ\tau is in ms — not seconds. The single most common debugging mistake on RC circuits is a missing factor of 1000.
  • Forgetting the initial condition. vC(0)=v0v_C(0) = v_0 is part of the problem; a different v0v_0 gives a different curve. Especially when an experiment is repeated quickly, the capacitor may not have fully discharged between runs.
  • Assuming "steady state" too soon. At t=τt = \tau you are still 36.8% off the target. Always wait at least 5τ before measuring a DC level.
  • Treating an inductor as a passive blob. Open the loop on a charged inductor and you get a voltage spike of Ldi/dtL\,di/dt — which can be huge. Always provide a flyback path (a freewheeling diode, an RC snubber) before unplugging a coil.
  • Reading τ off the wrong axis. On a linear-y plot, τ is the time at which a tangent drawn at t = 0 reaches the final value — not the visual half-height. On a log-y plot of the decay, τ is 1/slope-1/\text{slope}. Pick the right visual and the measurement becomes trivial.

Summary

  1. A series RC loop satisfies the first-order linear ODE RCvC+vC=VsRC\,v_C' + v_C = V_s; a series RL loop satisfies Li+Ri=VsL\,i' + R\,i = V_s. Both reduce to the template τy+y=y\tau y' + y = y_\infty.
  2. The solution is y(t)=y+(y0y)et/τy(t) = y_\infty + (y_0 - y_\infty)\,e^{-t/\tau}. The state pursues the target with characteristic time τ\tau.
  3. The time constant is τ=RC\tau = RC for capacitors and τ=L/R\tau = L/R for inductors. Units are seconds either way.
  4. At kτk\,\tau the remaining gap is eke^{-k} of the original — 63.2% closed after 1τ, 99.3% closed after 5τ. Engineers call 5τ "done."
  5. Fitting τ\tau from data is one log + one division per sample. Fitting both τ\tau and VsV_s simultaneously is two lines of PyTorch.
  6. The same ODE governs Newton's law of cooling, single-compartment drug clearance, audio low-pass filters, and any other system with one kind of memory leaking toward a fixed target.
Loading comments...