spicebook/scripts/populate-advanced-examples.sh
Ryan Malloy 42f4428295 Add 7 advanced example notebooks with real-world circuits
Buck converter, Class AB amplifier, AM radio receiver, Dickson
charge pump, transmission line signal integrity, 4th-order
Sallen-Key filter, and Colpitts RF oscillator. Multi-cell
notebooks with engineering narratives and multiple simulation
views per circuit.
2026-02-13 04:29:33 -07:00

788 lines
35 KiB
Bash
Executable File

#!/bin/bash
# Populate SpiceBook with advanced example notebooks
# Usage: ./populate-advanced-examples.sh [BASE_URL]
#
# These 7 notebooks showcase real-world circuits: power electronics,
# audio amplifiers, RF, signal integrity, and active filters.
# Each notebook is multi-cell, telling an engineering story.
BASE="${1:-https://spicebook.warehack.ing}"
API="$BASE/api"
create_notebook() {
local title="$1"
curl -s -X POST "$API/notebooks" \
-H "Content-Type: application/json" \
-d "{\"title\": \"$title\"}"
}
add_cell() {
local nb_id="$1" type="$2" source="$3" after="$4"
local body="{\"type\": \"$type\", \"source\": $(echo "$source" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}"
if [ -n "$after" ]; then
body=$(echo "$body" | python3 -c "import sys,json; d=json.load(sys.stdin); d['after_cell_id']='$after'; print(json.dumps(d))")
fi
curl -s -X POST "$API/notebooks/$nb_id/cells" \
-H "Content-Type: application/json" \
-d "$body"
}
update_cell() {
local nb_id="$1" cell_id="$2" source="$3"
curl -s -X PUT "$API/notebooks/$nb_id/cells/$cell_id" \
-H "Content-Type: application/json" \
-d "{\"source\": $(echo "$source" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}"
}
run_cell() {
local nb_id="$1" cell_id="$2"
echo -n " Running simulation..."
local result
result=$(curl -s --max-time 120 -X POST "$API/notebooks/$nb_id/cells/$cell_id/run")
echo "$result" | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
success=d.get('success', False)
elapsed=d.get('elapsed_seconds', 0)
wf=d.get('waveform', {}) or {}
pts=wf.get('points', 0)
nvars=len(wf.get('variables', []))
status='OK' if success else 'FAILED'
print(f' {status} ({elapsed:.3f}s, {pts} points, {nvars} variables)')
if not success:
print(f' Error: {d.get(\"error\",\"unknown\")}')
except Exception as e:
print(f' parse error: {e}')
" 2>/dev/null || echo " (run skipped)"
}
get_cell_id() {
echo "$1" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])"
}
get_nb_id() {
echo "$1" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])"
}
get_first_cell_id() {
local nb_id="$1"
curl -s "$API/notebooks/$nb_id" | python3 -c "import sys,json; print(json.load(sys.stdin)['cells'][0]['id'])"
}
echo "=== Populating SpiceBook advanced examples at $BASE ==="
echo
# ─────────────────────────────────────────────────────────────────────
# 1. Synchronous Buck Converter
# ─────────────────────────────────────────────────────────────────────
echo "1/7 Synchronous Buck Converter..."
nb=$(create_notebook "Synchronous Buck Converter")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# Synchronous Buck Converter
Every phone charger, laptop regulator, server VRM, and EV power module uses a buck converter. It is THE circuit of modern power electronics -- converting a higher DC voltage to a lower one with 90%+ efficiency by rapidly switching an inductor between the supply and ground.
**How it works:** A high-side switch connects the inductor to Vin during the ON time, then a low-side switch (synchronous rectifier) connects it to ground during the OFF time. The LC output filter smooths the resulting square wave into DC.
**Key equations:**
- Output voltage: **V_out = D * V_in** where D = duty cycle
- Inductor ripple: **dI = (V_in - V_out) * D / (f_sw * L)**
- Output ripple: **dV = dI / (8 * f_sw * C)**
This design: 12V in, 3.3V out, D = 3.3/12 = 0.275, switching at 500 kHz.' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* Synchronous Buck Converter - Full Startup
.param fsw=500k duty=0.275
.param Tp={1/fsw}
* 12V input supply
Vin supply 0 DC 12
* PWM drive signal (duty cycle sets on-time)
Vpwm pwm 0 PULSE(0 5 0 1n 1n {duty*Tp} {Tp})
* High-side switch (ON when pwm > 2.5V)
S1 supply sw pwm 0 IDEAL_SW
* Complementary low-side drive (inverted PWM)
Binv lsd 0 V = 5 - V(pwm)
S2 sw 0 lsd 0 IDEAL_SW
.model IDEAL_SW SW(Ron=50m Roff=1Meg Vt=2.5 Vh=0.1)
* Output LC filter
L1 sw lx 4.7u
Resr lx out 10m
C1 out 0 47u
* 1 ohm load (~3.3A at 3.3V)
Rload out 0 1
.tran 10n 40u UIC
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## What to look for in the waveforms
- **V(sw)** -- the switch node -- should be a rectangle wave bouncing between 12V and 0V
- **V(out)** -- the output -- ramps up from 0V and settles near 3.3V as the LC filter integrates the switching waveform
- **I(L1)** -- inductor current -- shows triangular ripple riding on a DC offset (the load current)
The startup transient takes roughly L*C * a few time constants to reach steady state. With 4.7 uH and 47 uF, the LC resonant period is about 93 us, so we see the output approaching 3.3V within our 40 us window.
Next, we zoom in on just a few switching cycles at steady state to see the fine detail.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* Synchronous Buck Converter - Steady State Detail
.param fsw=500k duty=0.275
.param Tp={1/fsw}
Vin supply 0 DC 12
Vpwm pwm 0 PULSE(0 5 0 1n 1n {duty*Tp} {Tp})
S1 supply sw pwm 0 IDEAL_SW
Binv lsd 0 V = 5 - V(pwm)
S2 sw 0 lsd 0 IDEAL_SW
.model IDEAL_SW SW(Ron=50m Roff=1Meg Vt=2.5 Vh=0.1)
L1 sw lx 4.7u
Resr lx out 10m
C1 out 0 47u
Rload out 0 1
* Skip output until 35us to see only steady-state
.tran 10n 38u 35u UIC
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 2. Class AB Push-Pull Audio Amplifier
# ─────────────────────────────────────────────────────────────────────
echo "2/7 Class AB Push-Pull Audio Amplifier..."
nb=$(create_notebook "Class AB Push-Pull Audio Amplifier")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# Class AB Push-Pull Audio Amplifier
The output stage of nearly every analog audio amplifier ever built. From vintage hi-fi receivers to guitar amps to PA systems, the complementary push-pull topology drives speakers efficiently while keeping distortion low.
**The crossover problem:** A pure Class B push-pull (NPN handles positive half, PNP handles negative half) creates nasty crossover distortion where the two transistors hand off near zero volts. Class AB solves this by biasing both transistors slightly ON at idle using a pair of diodes that maintain ~1.2V between the bases -- just enough to keep both devices in their active region through the crossover.
**This circuit:**
- Common-emitter input stage (Q1) provides voltage gain (~Rc/Re)
- Two bias diodes (D1, D2) set the quiescent current for the output pair
- Complementary NPN/PNP emitter followers (Q2, Q3) provide current gain to drive an 8-ohm speaker
- Coupling capacitors block DC from the input and output' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* Class AB Push-Pull Amplifier - Frequency Response
VCC vcc 0 DC 12
VEE vee 0 DC -12
* AC input through coupling cap
Vin inp 0 AC 0.2
C1 inp base 10u
* Bias to mid-rail
Rb1 vcc base 100k
Rb2 base vee 100k
* Common-emitter input stage
Q1 col base em1 NPN1
Re1 em1 vee 1k
Ce1 em1 vee 100u
* Collector load with bias diodes (2x Vbe for Class AB)
Rc1 vcc q2b 4.7k
D1 q2b dmid DBIAS
D2 dmid col DBIAS
* Complementary output emitter followers
Q2 vcc q2b out NPN1
Q3 vee col out PNP1
* Output coupling and 8-ohm speaker
Cout out spk 470u
Rload spk 0 8
.model NPN1 NPN(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model PNP1 PNP(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model DBIAS D(Is=1e-14 N=1.0)
.ac dec 100 10 1meg
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## Reading the Bode plot
The frequency response shows the classic bandpass shape of an AC-coupled amplifier:
- **Low-frequency rolloff** (~50 Hz) -- set by the input and output coupling capacitors. Below this, the caps have too much impedance to pass signal.
- **Mid-band flat region** -- the amplifier provides ~20 dB of gain (about 10x voltage amplification). This is set by Rc1/Re1 = 4.7k/1k = 4.7, boosted by the bootstrapping effect.
- **High-frequency rolloff** -- set by transistor parasitic capacitances (CJC, CJE) and the transition frequency of the BJTs.
The bandwidth easily covers the 20 Hz to 20 kHz audio range.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* Class AB Push-Pull Amplifier - Clean 1kHz Sine
VCC vcc 0 DC 12
VEE vee 0 DC -12
Vin inp 0 SIN(0 0.2 1k)
C1 inp base 10u
Rb1 vcc base 100k
Rb2 base vee 100k
Q1 col base em1 NPN1
Re1 em1 vee 1k
Ce1 em1 vee 100u
Rc1 vcc q2b 4.7k
D1 q2b dmid DBIAS
D2 dmid col DBIAS
Q2 vcc q2b out NPN1
Q3 vee col out PNP1
Cout out spk 470u
Rload spk 0 8
.model NPN1 NPN(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model PNP1 PNP(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model DBIAS D(Is=1e-14 N=1.0)
.tran 10u 5m
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
prev_id="$spice2_id"
spice3=$(add_cell "$nb_id" "spice" '* Class AB Push-Pull Amplifier - Overdrive Clipping
VCC vcc 0 DC 12
VEE vee 0 DC -12
* 2V input -- way too hot for this amp
Vin inp 0 SIN(0 2 1k)
C1 inp base 10u
Rb1 vcc base 100k
Rb2 base vee 100k
Q1 col base em1 NPN1
Re1 em1 vee 1k
Ce1 em1 vee 100u
Rc1 vcc q2b 4.7k
D1 q2b dmid DBIAS
D2 dmid col DBIAS
Q2 vcc q2b out NPN1
Q3 vee col out PNP1
Cout out spk 470u
Rload spk 0 8
.model NPN1 NPN(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model PNP1 PNP(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.model DBIAS D(Is=1e-14 N=1.0)
.tran 10u 5m
.end' "$prev_id")
spice3_id=$(get_cell_id "$spice3")
run_cell "$nb_id" "$spice3_id"
prev_id="$spice3_id"
md3=$(add_cell "$nb_id" "markdown" '## Clean signal vs clipping
**1kHz transient (200mV input):** The output is a faithful, amplified reproduction of the input sine wave. The push-pull output stage handles the positive and negative swings symmetrically with no visible crossover artifacts -- the bias diodes are doing their job.
**Overdriven (2V input):** The output clips hard against the supply rails. The sine wave becomes a flat-topped square-ish wave as the output transistors saturate. This is what guitar players call "tube-like breakup" when it happens gently, and what audiophiles call "unacceptable" when it happens to their symphony recordings. The harmonic distortion in the clipped signal is rich in odd harmonics -- the characteristic "crunch" of a solid-state amplifier pushed past its limits.' "$prev_id")
md3_id=$(get_cell_id "$md3")
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 3. AM Radio Receiver (Envelope Detector)
# ─────────────────────────────────────────────────────────────────────
echo "3/7 AM Radio Receiver..."
nb=$(create_notebook "AM Radio Receiver (Envelope Detector)")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# AM Radio Receiver -- Envelope Detection
One of the most elegant circuits in all of electronics. Every AM radio built since the 1920s -- from crystal sets made with a cats whisker and a razor blade to modern software-defined radios -- relies on envelope detection to recover audio from a modulated carrier.
**AM modulation:** The audio signal rides as a varying amplitude on a high-frequency carrier:
**V(t) = V_c * (1 + m * sin(2*pi*f_a*t)) * sin(2*pi*f_c*t)**
where m is the modulation depth (0 to 1), f_a is the audio frequency, and f_c is the carrier frequency.
**Envelope detection:** A diode rectifies the AM signal, letting only positive peaks through. An RC filter smooths out the carrier frequency ripple while preserving the slower audio envelope. The result: the original audio signal, recovered from the radio wave with nothing more than a diode and a capacitor.
This simulation uses f_c = 100 kHz (carrier), f_a = 1 kHz (audio), m = 0.8 (80% modulation depth).' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* AM Radio Receiver - Full Demodulation
.param fc=100k fa=1k m=0.8 Vc=1
* AM modulated antenna signal
* V = Vc*(1 + m*sin(2*pi*fa*t)) * sin(2*pi*fc*t)
Bam ant 0 V = {Vc*(1+m*sin(6.2832*fa*time))*sin(6.2832*fc*time)}
* Schottky diode detector (low forward drop)
D1 ant det SCHOTTKY
.model SCHOTTKY D(Is=1e-8 N=1.05 Rs=10 CJO=1p BV=20)
* Peak detector: RC holds the envelope
* Time constant must be: 1/fc << RC << 1/fa
* 10k * 10n = 100us (10us carrier << 100us << 1ms audio)
C1 det 0 10n
R1 det 0 10k
* Audio output with DC blocking
C2 det audio 100n
R2 audio 0 10k
.tran 1u 3m
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## Three signals tell the story
Look at the waveforms from the simulation:
- **V(ant)** -- the antenna signal -- shows the AM modulated carrier. The 100 kHz carrier appears as a dense waveform whose amplitude varies at the 1 kHz audio rate. The modulation depth (0.8) means the amplitude swings from 0.2V to 1.8V peak.
- **V(det)** -- the detector output -- follows the positive peaks of the carrier. The Schottky diode clips the negative half-cycles, and the RC filter connects the dots between peaks. You can see the carrier ripple riding on the recovered envelope.
- **V(audio)** -- the audio output -- a clean 1 kHz sine wave recovered from the modulated carrier. The DC blocking capacitor removes the DC offset, leaving just the audio signal.
**The RC time constant is critical.** Too short (fast discharge) and the detector can not hold the peak between carrier cycles. Too long (slow discharge) and it can not follow the audio modulation. The sweet spot: much longer than the carrier period, much shorter than the audio period.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* AM Radio Receiver - Carrier Detail (Zoomed)
.param fc=100k fa=1k m=0.8 Vc=1
Bam ant 0 V = {Vc*(1+m*sin(6.2832*fa*time))*sin(6.2832*fc*time)}
D1 ant det SCHOTTKY
.model SCHOTTKY D(Is=1e-8 N=1.05 Rs=10 CJO=1p BV=20)
C1 det 0 10n
R1 det 0 10k
C2 det audio 100n
R2 audio 0 10k
* Narrow time window to see individual RF cycles
.tran 0.1u 100u
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 4. Dickson Charge Pump
# ─────────────────────────────────────────────────────────────────────
echo "4/7 Dickson Charge Pump..."
nb=$(create_notebook "Dickson Charge Pump Voltage Multiplier")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# Dickson Charge Pump Voltage Multiplier
Need 15V from a 5V supply but have no room for an inductor? The Dickson charge pump generates high voltage from low voltage using only capacitors and diodes -- no magnetics, no transformers. Used in EEPROM/Flash memory programming, MEMS actuation, LED drivers, and anywhere you need a higher voltage rail from a low-voltage supply in a tiny footprint.
**How it works:** Two non-overlapping clock phases alternately pump charge up a diode ladder. Each stage adds roughly one clock amplitude (minus a diode drop) to the voltage. A 3-stage pump with 5V clocks can theoretically reach:
**V_out = V_dd + N_stages * V_clk - (N_stages + 1) * V_diode**
**V_out = 5 + 3*5 - 4*0.3 = 18.8V** (theoretical maximum with Schottky diodes)
Real output is lower due to output impedance: **R_out = N / (f * C)**
The output voltage drops under load as current flows through this impedance -- which is why charge pumps are best suited for low-current applications.' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* Dickson Charge Pump - Light Load (10k)
Vdd vdd 0 DC 5
* Two-phase non-overlapping clocks at 100 kHz
Vclk1 clk1 0 PULSE(0 5 0 10n 10n 4.5u 10u)
Vclk2 clk2 0 PULSE(0 5 5u 10n 10n 4.5u 10u)
* 3-stage diode-capacitor ladder
D1 vdd n1 SCHOTTKY
C1 n1 clk1 100n
D2 n1 n2 SCHOTTKY
C2 n2 clk2 100n
D3 n2 n3 SCHOTTKY
C3 n3 clk1 100n
* Output rectifier and filter
D4 n3 out SCHOTTKY
Cout out 0 10u
* Light load: 10k ohm
Rload out 0 10k
.model SCHOTTKY D(Is=1e-8 N=1.05 Rs=0.5 CJO=1p BV=40)
.tran 1u 500u UIC
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## Exponential approach to steady state
Watch V(out) -- it does not jump instantly to the theoretical maximum. Instead, it climbs in a staircase pattern, gaining a bit more voltage with each clock cycle as charge shuttles up the diode ladder into the output capacitor.
The charging follows an exponential approach: fast at first (large voltage difference drives current through the diodes), then progressively slower as the output approaches its limit. This is the same RC charging curve you see everywhere in electronics, but implemented with a switched-capacitor network.
The intermediate nodes V(n1), V(n2), V(n3) show progressively higher DC levels -- each stage adds roughly 5V minus a diode drop.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* Dickson Charge Pump - Heavy Load (1k)
Vdd vdd 0 DC 5
Vclk1 clk1 0 PULSE(0 5 0 10n 10n 4.5u 10u)
Vclk2 clk2 0 PULSE(0 5 5u 10n 10n 4.5u 10u)
D1 vdd n1 SCHOTTKY
C1 n1 clk1 100n
D2 n1 n2 SCHOTTKY
C2 n2 clk2 100n
D3 n2 n3 SCHOTTKY
C3 n3 clk1 100n
D4 n3 out SCHOTTKY
Cout out 0 10u
* Heavy load: 1k ohm (10x more current)
Rload out 0 1k
.model SCHOTTKY D(Is=1e-8 N=1.05 Rs=0.5 CJO=1p BV=40)
.tran 1u 500u UIC
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
prev_id="$spice2_id"
md3=$(add_cell "$nb_id" "markdown" '## Output impedance makes all the difference
Compare V(out) between the two simulations:
- **10k load** (light) -- output reaches close to the theoretical maximum. The pump barely notices the ~1.5 mA load current.
- **1k load** (heavy) -- output voltage drops significantly. The ~15 mA load current causes a large voltage drop across the pump internal impedance.
**R_out = N / (f * C) = 3 / (100k * 100n) = 300 ohm**
The output ripple is also much worse under heavy load -- each clock cycle, the load drains more charge from the output cap before the next pump cycle replenishes it.
This is why charge pumps dominate in low-current applications (EEPROM programming pulses, gate drivers, bias supplies) but lose to inductor-based converters for high-current loads. An inductor-based boost converter can deliver amps at any voltage ratio; a charge pump struggles with milliamps.' "$prev_id")
md3_id=$(get_cell_id "$md3")
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 5. Transmission Line Signal Integrity
# ─────────────────────────────────────────────────────────────────────
echo "5/7 Transmission Line Signal Integrity..."
nb=$(create_notebook "Transmission Line Signal Integrity")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# Transmission Line Signal Integrity
Above ~50 MHz, a wire is no longer just a wire. It becomes a transmission line with characteristic impedance, propagation delay, and reflections. This is THE single biggest cause of signal integrity failures in high-speed PCB design -- and mandatory knowledge for anyone working above a few tens of megahertz.
**When does a wire become a transmission line?** When the signal rise time is shorter than the round-trip propagation delay. A 1 ns rise time on a 15 cm trace (1 ns propagation delay) means the signal arrives at the far end before the source has finished transitioning. The far end "sees" a wave, not a voltage.
**Impedance mismatch causes reflections.** When a wave hits a boundary where Z changes, part of it bounces back:
**Gamma = (Z_load - Z_line) / (Z_load + Z_line)**
- Open end (Z_load = infinity): Gamma = +1 (full positive reflection, voltage doubles)
- Short end (Z_load = 0): Gamma = -1 (full negative reflection)
- Matched (Z_load = Z_line): Gamma = 0 (no reflection, clean signal)
This simulation uses a lumped LC ladder to model a 50-ohm transmission line: 10 sections of L=10nH, C=4pF each. Z0 = sqrt(L/C) = sqrt(10n/4p) = 50 ohm. Total propagation delay ~2 ns.' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* Transmission Line - Unterminated (Massive Ringing)
* Fast pulse with 0.5 ns edges, 10-ohm driver
Vpulse inp 0 PULSE(0 3.3 2n 0.5n 0.5n 15n 30n)
Rdrv inp near 10
* 10-section lumped LC ladder (Z0=50 ohm, ~2ns delay)
L1 near n1 10n
C1 n1 0 4p
L2 n1 n2 10n
C2 n2 0 4p
L3 n2 n3 10n
C3 n3 0 4p
L4 n3 n4 10n
C4 n4 0 4p
L5 n4 n5 10n
C5 n5 0 4p
L6 n5 n6 10n
C6 n6 0 4p
L7 n6 n7 10n
C7 n7 0 4p
L8 n7 n8 10n
C8 n8 0 4p
L9 n8 n9 10n
C9 n9 0 4p
L10 n9 far 10n
C10 far 0 4p
* No termination -- open load
Rload far 0 1meg
.tran 0.1n 30n
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## The staircase of doom
Look at V(far) -- the voltage at the far end of the unterminated line. Instead of a clean step to 3.3V, you see massive overshoot, ringing, and a slow staircase settling pattern. This is what happens to every digital signal on an unterminated trace.
**What is happening, step by step:**
1. The driver launches a wave down the line. With a 10-ohm driver into a 50-ohm line, the initial wave amplitude is 3.3V * 50/(10+50) = 2.75V.
2. The wave arrives at the open far end (Gamma = +1) and reflects back at full amplitude. The far end momentarily sees 2 * 2.75V = 5.5V -- way above 3.3V!
3. The reflection returns to the driver end (Gamma = (10-50)/(10+50) = -0.667) and partially reflects again.
4. Each round trip adds another step to the staircase, with diminishing amplitude.
That overshoot to 5.5V can violate absolute maximum ratings, cause latch-up in CMOS, or trigger false clock edges. This is why termination matters.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* Transmission Line - Series Terminated (Clean)
Vpulse inp 0 PULSE(0 3.3 2n 0.5n 0.5n 15n 30n)
Rdrv inp drv 10
* Series termination: Rdrv + Rterm = Z0
Rterm drv near 40
* Same 10-section lumped LC ladder
L1 near n1 10n
C1 n1 0 4p
L2 n1 n2 10n
C2 n2 0 4p
L3 n2 n3 10n
C3 n3 0 4p
L4 n3 n4 10n
C4 n4 0 4p
L5 n4 n5 10n
C5 n5 0 4p
L6 n5 n6 10n
C6 n6 0 4p
L7 n6 n7 10n
C7 n7 0 4p
L8 n7 n8 10n
C8 n8 0 4p
L9 n8 n9 10n
C9 n9 0 4p
L10 n9 far 10n
C10 far 0 4p
Rload far 0 1meg
.tran 0.1n 30n
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
prev_id="$spice2_id"
md3=$(add_cell "$nb_id" "markdown" '## Series termination: one bounce and done
With the 40-ohm series resistor added (Rdrv + Rterm = 10 + 40 = 50 ohm = Z0), the source impedance now matches the line impedance.
**The clean version:**
1. The driver launches a half-amplitude wave: 3.3V * 50/(50+50) = 1.65V.
2. At the open far end (Gamma = +1), the wave reflects and doubles to 3.3V. The far end sees a clean step.
3. The reflection travels back to the source. Source impedance matches line impedance (Gamma = 0). No re-reflection. Done.
V(near) shows a two-step waveform (1.65V, then 3.3V) because the near end sees the initial half-voltage followed by the returning reflection. V(far) shows a single clean step to 3.3V with no overshoot, no ringing. This is the standard technique for digital PCB design: put a series resistor at the source so Rdrv + Rterm = Z0.' "$prev_id")
md3_id=$(get_cell_id "$md3")
prev_id="$md3_id"
spice3=$(add_cell "$nb_id" "spice" '* Transmission Line - Ideal T Element (ngspice built-in)
* Uses ngspice lossless transmission line model
Vpulse inp 0 PULSE(0 3.3 2n 0.5n 0.5n 15n 30n)
Rdrv inp near 10
* Ideal 50-ohm line, 2ns propagation delay
T1 near 0 far 0 Z0=50 TD=2n
* Open load (unterminated for comparison)
Rload far 0 1meg
.tran 0.1n 30n
.end' "$prev_id")
spice3_id=$(get_cell_id "$spice3")
run_cell "$nb_id" "$spice3_id"
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 6. 4th-Order Sallen-Key Butterworth Filter
# ─────────────────────────────────────────────────────────────────────
echo "6/7 4th-Order Sallen-Key Butterworth Filter..."
nb=$(create_notebook "4th-Order Sallen-Key Butterworth Filter")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# 4th-Order Sallen-Key Butterworth Filter
Passive RC filters roll off at a gentle -20 dB/decade. For serious filtering -- anti-aliasing before an ADC, crossover networks in audio, sensor signal conditioning, biomedical instrumentation -- you need steeper slopes. Active filters using op-amps can achieve -40, -60, -80 dB/decade and beyond.
**Why Butterworth?** The Butterworth response is "maximally flat" in the passband -- no ripple, no peaking, just a smooth transition from passband to stopband. It is the right choice when you need a predictable, well-behaved frequency response without surprises.
**Sallen-Key topology:** Each 2nd-order section uses one op-amp as a unity-gain buffer, with two resistors and two capacitors setting the cutoff frequency and Q factor. Cascading two sections gives a 4th-order filter with -80 dB/decade rolloff.
**Design parameters (fc = 1 kHz):**
- Stage 1: Q = 0.5412 (gentle, overdamped)
- Stage 2: Q = 1.3066 (sharper, slightly underdamped)
- Combined: maximally-flat Butterworth response
This notebook compares a simple 1st-order RC filter against the 4th-order Sallen-Key -- both targeting 1 kHz cutoff.' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* 4th-Order Butterworth vs Simple RC - Frequency Response
* === Simple 1st-order RC lowpass (reference) ===
V1 in1 0 AC 1
R0 in1 rc_out 15.9k
C0 rc_out 0 10n
* fc = 1/(2*pi*15.9k*10n) = 1.0 kHz
*
* === 4th-order Sallen-Key Butterworth ===
V2 in2 0 AC 1
* Ideal op-amp subcircuit (100 dB open-loop gain)
.subckt IDEALOA inp inn out
E1 out 0 inp inn 100000
.ends IDEALOA
* Stage 1: Q=0.5412, fc=1kHz (R=15k, C1=10n, C2=11.7n)
R1a in2 a1 15k
R1b a1 b1 15k
C1a a1 0 10n
C1b b1 s1out 11.7n
X1 b1 s1out s1out IDEALOA
* Stage 2: Q=1.3066, fc=1kHz (R=6.2k, C1=10n, C2=68.3n)
R2a s1out a2 6.2k
R2b a2 b2 6.2k
C2a a2 0 10n
C2b b2 s2out 68.3n
X2 b2 s2out s2out IDEALOA
.ac dec 100 10 100k
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## 60 dB more rejection = 1000x cleaner signal
Compare the two response curves:
- **V(rc_out)** -- the simple RC -- rolls off at -20 dB/decade. At 10 kHz (one decade above cutoff), it provides only 20 dB of attenuation. A 1V interferer at 10 kHz would appear as 100 mV at the output.
- **V(s2out)** -- the 4th-order Butterworth -- rolls off at -80 dB/decade. At 10 kHz, it provides 80 dB of attenuation. That same 1V interferer is reduced to 0.1 mV -- a thousand times better.
Both curves are flat and nearly identical below 100 Hz. The Butterworth passband is "maximally flat" -- no peaking, no ripple. The transition band (around 1 kHz) shows the dramatic difference in steepness. This is why every serious anti-aliasing filter, every audio crossover, and every sensor conditioning circuit uses higher-order active filters.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* 4th-Order Sallen-Key Butterworth - Step Response
V1 in 0 PULSE(0 1 0.1m 1n 1n 10m 20m)
.subckt IDEALOA inp inn out
E1 out 0 inp inn 100000
.ends IDEALOA
* Stage 1: Q=0.5412
R1a in a1 15k
R1b a1 b1 15k
C1a a1 0 10n
C1b b1 s1out 11.7n
X1 b1 s1out s1out IDEALOA
* Stage 2: Q=1.3066
R2a s1out a2 6.2k
R2b a2 b2 6.2k
C2a a2 0 10n
C2b b2 out 68.3n
X2 b2 out out IDEALOA
.tran 10u 5m
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
prev_id="$spice2_id"
md3=$(add_cell "$nb_id" "markdown" '## Step response: the time-domain signature
The step response reveals the filter character in the time domain:
- **Butterworth** shows a smooth rise to the final value with minimal overshoot (~0.4%). This is the hallmark of maximally-flat magnitude design.
- **Chebyshev** filters (not shown) would ring more but have a steeper transition band -- sharper filtering at the cost of passband ripple and more overshoot.
- **Bessel** filters (not shown) would have zero overshoot and linear phase (preserving waveform shape) but a much gentler rolloff -- better for pulse signals where shape matters more than rejection.
The Butterworth is the compromise: flat passband, moderate overshoot, good stopband rejection. It is the default choice unless you have a specific reason to pick Chebyshev (maximum stopband rejection) or Bessel (best transient response).' "$prev_id")
md3_id=$(get_cell_id "$md3")
echo " Created: $nb_id"
# ─────────────────────────────────────────────────────────────────────
# 7. Colpitts RF Oscillator
# ─────────────────────────────────────────────────────────────────────
echo "7/7 Colpitts RF Oscillator..."
nb=$(create_notebook "Colpitts RF Oscillator")
nb_id=$(get_nb_id "$nb")
md_id=$(get_first_cell_id "$nb_id")
update_cell "$nb_id" "$md_id" '# Colpitts RF Oscillator
Radio transmitters, clock generators, frequency synthesizers, local oscillators in every superheterodyne receiver -- all need a stable sinusoidal source. The Colpitts oscillator, invented in 1918 by Edwin Colpitts of Western Electric, uses a capacitive voltage divider for feedback and remains one of the most common RF oscillator topologies a century later.
**Barkhausen criterion:** An oscillator is an amplifier with positive feedback. For sustained oscillation, the loop must satisfy two conditions simultaneously:
- **Loop gain >= 1** (enough amplification to overcome losses)
- **Loop phase = 0 degrees** (or 360) at the oscillation frequency
**Colpitts feedback:** The LC tank (L1, C1, C2) sets the frequency. C1 and C2 form a capacitive voltage divider that feeds a fraction of the collector signal back to the base. The 180-degree phase shift from the common-emitter amplifier plus the 180-degree shift from the capacitive tap gives the required 360 degrees.
**Target frequency:**
**f = 1 / (2*pi*sqrt(L * C1*C2/(C1+C2))) = 1 / (2*pi*sqrt(100u * 235p)) = ~1.04 MHz**' > /dev/null
prev_id="$md_id"
spice=$(add_cell "$nb_id" "spice" '* Colpitts RF Oscillator - Full Startup
VCC vcc 0 DC 12
* RF choke: passes DC bias, blocks RF from supply
Lrfc vcc col 1m
* LC tank with capacitive voltage divider
L1 col tank 100u
C1 tank base 470p
C2 base 0 470p
* DC bias network
Rb1 vcc base 100k
Rb2 base 0 22k
* BJT amplifier
Q1 col base emit NPN1
Re emit 0 1k
Ce emit 0 10n
.model NPN1 NPN(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
* Initial conditions to break symmetry and start oscillation
.ic V(col)=6 V(tank)=6.1
.tran 0.05u 50u UIC
.end' "$prev_id")
spice_id=$(get_cell_id "$spice")
run_cell "$nb_id" "$spice_id"
prev_id="$spice_id"
md2=$(add_cell "$nb_id" "markdown" '## Watching oscillation emerge from nothing
This is one of the most satisfying things to see in analog simulation. The startup sequence:
1. **Perturbation** -- the `.ic` directive sets V(col) and V(tank) to slightly different values, creating a tiny initial imbalance. In a real circuit, thermal noise provides this kick.
2. **Exponential growth** -- each cycle around the feedback loop amplifies the previous one. The waveform grows exponentially: 1 mV, then 2 mV, then 4 mV... visible as an expanding sinusoidal envelope.
3. **Gain compression** -- as the amplitude grows, the BJT hits its nonlinear limits (cutoff and saturation clipping). The effective loop gain drops to exactly 1, and the amplitude stabilizes.
4. **Steady state** -- the oscillator settles into a stable sinusoidal output at ~1 MHz. The amplitude is set by the point where gain compression balances the feedback.
Look at V(tank) for the clearest view of the startup envelope.' "$prev_id")
md2_id=$(get_cell_id "$md2")
prev_id="$md2_id"
spice2=$(add_cell "$nb_id" "spice" '* Colpitts RF Oscillator - Steady State Detail
VCC vcc 0 DC 12
Lrfc vcc col 1m
L1 col tank 100u
C1 tank base 470p
C2 base 0 470p
Rb1 vcc base 100k
Rb2 base 0 22k
Q1 col base emit NPN1
Re emit 0 1k
Ce emit 0 10n
.model NPN1 NPN(BF=200 IS=1e-14 VAF=100 CJC=5p CJE=10p TF=0.5n)
.ic V(col)=6 V(tank)=6.1
* Run full sim but only output the last 5 microseconds
.tran 0.05u 50u 45u UIC
.end' "$prev_id")
spice2_id=$(get_cell_id "$spice2")
run_cell "$nb_id" "$spice2_id"
prev_id="$spice2_id"
md3=$(add_cell "$nb_id" "markdown" '## Frequency verification and harmonic content
In the zoomed steady-state view, count the cycles in the 5 us window. You should see roughly 5 complete cycles, confirming f ~ 1 MHz. The exact frequency:
**f = 1 / (2*pi*sqrt(L * C_series))** where C_series = C1*C2/(C1+C2) = 470p*470p/940p = 235 pF
**f = 1 / (2*pi*sqrt(100u * 235p)) = 1.038 MHz**
The waveform is not a perfect sine -- it is slightly distorted by the BJT nonlinearities that limit the amplitude. The flat-bottomed shape at V(base) shows where the transistor cuts off during part of each cycle. This distortion means the output contains harmonics (2f, 3f, 4f...) in addition to the fundamental.
In a real transmitter, this signal would be fed through a bandpass filter to select the fundamental and reject harmonics before reaching the antenna. For a clock generator, the harmonics actually help sharpen the edges.' "$prev_id")
md3_id=$(get_cell_id "$md3")
echo " Created: $nb_id"
echo
echo "=== Done! Created 7 advanced example notebooks ==="
echo "Visit: $BASE"