#!/usr/bin/env python3 """Create the 9 missing SpiceBook notebooks for Mims library integration. Uses the SpiceBook API to PUT notebooks with specific IDs matching the Mims frontmatter references. After creation, runs each SPICE cell and generates schematics. Usage: python3 scripts/create-spicebook-notebooks.py [--api-url http://localhost:8099] """ import json import sys import time import urllib.request import urllib.error from datetime import datetime, timezone API_BASE = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8099" NOW = datetime.now(timezone.utc).isoformat() NOTEBOOKS = { "555-astable-blinker": { "metadata": { "title": "555 Astable LED Blinker", "engine": "ngspice", "tags": ["555", "timer", "astable", "beginner"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# 555 Astable LED Blinker\n\n" "The 555 timer in astable mode produces a continuous square wave " "without any external trigger. Two resistors (Ra, Rb) and a capacitor (C1) " "set the frequency and duty cycle.\n\n" "- **Frequency:** f = 1.44 / ((Ra + 2*Rb) * C1)\n" "- **Duty cycle:** D = (Ra + Rb) / (Ra + 2*Rb)\n\n" "With Ra = 1k, Rb = 10k, C1 = 10uF:\n" "f = 1.44 / ((1k + 20k) * 10u) = 6.86 Hz (~7 blinks/second)" ), "outputs": [], }, { "id": "cell-astable", "type": "spice", "source": ( "555 Astable Multivibrator\n" "* Power supply\n" "VCC vcc 0 DC 9\n" "* Timing resistors\n" "Ra vcc dis 1k\n" "Rb dis thr 10k\n" "* Timing capacitor\n" "C1 thr 0 10u IC=0\n" "* 555 timer modeled with behavioral sources\n" "* Comparator thresholds: 2/3 Vcc (upper), 1/3 Vcc (lower)\n" "* SR flip-flop drives output\n" "Bcomp out 0 V = V(vcc) * (V(thr) < V(vcc)*2/3 ? 1 : 0) * (V(thr) > V(vcc)/3 ? 1 : V(vcc) > 0 ? 1 : 0)\n" "* Discharge transistor\n" "Sdis dis 0 out 0 SWMOD\n" ".model SWMOD SW VT=4 VH=0.5 RON=10 ROFF=1G\n" ".tran 10u 0.5\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## How It Works\n\n" "The capacitor C1 charges through Ra + Rb and discharges through Rb alone. " "The 555's internal comparators trip at 1/3 and 2/3 of Vcc, toggling the " "output flip-flop and the discharge transistor. The result is a square wave " "at the output pin.\n\n" "The LED (not shown in the SPICE model) would connect from the output " "through a 330 ohm current-limiting resistor to ground." ), "outputs": [], }, ], }, "555-monostable-pulse": { "metadata": { "title": "555 Monostable Pulse Generator", "engine": "ngspice", "tags": ["555", "timer", "monostable", "beginner"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# 555 Monostable Pulse Generator\n\n" "In monostable (one-shot) mode, the 555 produces a single timed pulse " "when triggered by a falling edge on the trigger pin.\n\n" "- **Pulse width:** T = 1.1 * R * C\n\n" "With R = 100k and C = 10uF: T = 1.1 * 100k * 10u = 1.1 seconds" ), "outputs": [], }, { "id": "cell-mono", "type": "spice", "source": ( "555 Monostable One-Shot\n" "* Power supply\n" "VCC vcc 0 DC 9\n" "* Trigger pulse (brief low pulse at t=0.1s)\n" "Vtrig trig 0 PULSE(9 0 0.1 1n 1n 1u 10)\n" "* Timing components\n" "R1 vcc thr 100k\n" "C1 thr 0 10u IC=0\n" "* Simplified 555 monostable behavior\n" "* Output goes high on trigger, stays high until C1 charges to 2/3 Vcc\n" "Bout out 0 V = V(vcc) * ( V(thr) < V(vcc)*2/3 ? (V(trig) < V(vcc)/3 ? 1 : 0) : 0 )\n" "* Discharge switch\n" "Sdis thr 0 out 0 SWMOD\n" ".model SWMOD SW VT=4 VH=0.5 RON=1G ROFF=10\n" ".tran 1m 3\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## How It Works\n\n" "At rest, the output is low and the discharge transistor holds the " "timing capacitor at 0V. When the trigger input drops below 1/3 Vcc, " "the output goes high and the capacitor starts charging through R.\n\n" "When the capacitor voltage reaches 2/3 Vcc, the threshold comparator " "resets the flip-flop, the output goes low, and the discharge transistor " "drains the capacitor. The circuit then waits for the next trigger." ), "outputs": [], }, ], }, "inverting-op-amp": { "metadata": { "title": "Inverting Op-Amp Amplifier", "engine": "ngspice", "tags": ["op-amp", "amplifier", "analog", "beginner"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# Inverting Op-Amp Amplifier\n\n" "The inverting amplifier is a fundamental op-amp circuit. The input signal " "connects through R1 to the inverting (-) input, and feedback resistor Rf " "sets the gain.\n\n" "- **Gain:** Av = -Rf / R1\n" "- **Input impedance:** Zin = R1\n\n" "With R1 = 10k and Rf = 47k: Av = -47k/10k = -4.7" ), "outputs": [], }, { "id": "cell-ac", "type": "spice", "source": ( "Inverting Op-Amp Amplifier\n" "* Dual power supply\n" "VCC vcc 0 DC 12\n" "VEE vee 0 DC -12\n" "* Input signal (1kHz sine, 100mV amplitude)\n" "Vin in 0 SIN(0 0.1 1k)\n" "* Input and feedback resistors\n" "R1 in inv 10k\n" "Rf out inv 47k\n" "* LM741 op-amp (subcircuit)\n" ".include /usr/share/ngspice/spice3/LM741.MOD\n" "XU1 0 inv vcc vee out LM741\n" ".tran 10u 5m\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## Reading the Waveform\n\n" "The output (red) should be an inverted and amplified version of the " "input (blue). With a gain of -4.7:\n" "- 100mV input peak becomes ~470mV output peak\n" "- The output is 180 degrees out of phase (inverted)\n\n" "The virtual ground principle keeps the inverting input at ~0V. " "All the input current (Vin/R1) flows through Rf, creating the " "output voltage: Vout = -Vin * (Rf/R1)." ), "outputs": [], }, ], }, "op-amp-comparator": { "metadata": { "title": "Op-Amp Voltage Comparator", "engine": "ngspice", "tags": ["op-amp", "comparator", "digital", "beginner"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# Op-Amp Voltage Comparator\n\n" "An op-amp with no feedback acts as a comparator: the output swings " "to the positive or negative rail depending on which input is higher.\n\n" "- If V(+) > V(-): output goes to +Vcc\n" "- If V(+) < V(-): output goes to -Vcc\n\n" "A reference voltage on one input sets the threshold. " "This is the basis for level detectors, zero-crossing circuits, and " "analog-to-digital conversion." ), "outputs": [], }, { "id": "cell-comp", "type": "spice", "source": ( "Op-Amp Voltage Comparator\n" "* Dual power supply\n" "VCC vcc 0 DC 12\n" "VEE vee 0 DC -12\n" "* Slowly rising input (ramp)\n" "Vin inp 0 PULSE(-5 5 0 10m 10m 1n 20m)\n" "* Reference voltage (voltage divider)\n" "R1 vcc ref 10k\n" "R2 ref 0 10k\n" "* Op-amp in open-loop (comparator mode)\n" ".include /usr/share/ngspice/spice3/LM741.MOD\n" "XU1 inp ref vcc vee out LM741\n" ".tran 10u 40m\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## Interpreting the Output\n\n" "The triangular input ramps between -5V and +5V. The reference is " "set at Vcc * R2/(R1+R2) = 6V (from the 12V supply divided by equal " "resistors). When the input crosses the reference, the output snaps " "between the rails.\n\n" "In practice, a dedicated comparator IC (like the LM339) switches " "faster than a general-purpose op-amp, but the principle is identical." ), "outputs": [], }, ], }, "am-radio-receiver": { "metadata": { "title": "AM Envelope Detector", "engine": "ngspice", "tags": ["radio", "am", "diode", "communications"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# AM Envelope Detector\n\n" "The simplest AM radio receiver: a diode rectifies the RF carrier, " "and an RC filter extracts the audio envelope. This is the circuit " "used in crystal radios and the detector stage of superheterodyne " "receivers.\n\n" "- **Carrier:** 1 MHz (AM broadcast band)\n" "- **Modulating signal:** 1 kHz audio tone\n" "- **Modulation depth:** 50%" ), "outputs": [], }, { "id": "cell-detector", "type": "spice", "source": ( "AM Envelope Detector\n" "* AM modulated source: carrier 1MHz, modulated at 1kHz\n" "* Vam = (1 + m*sin(2*pi*fm*t)) * sin(2*pi*fc*t)\n" "Vam rf 0 AM(1 1e6 0 0 0.5 1e3)\n" "* Detector diode\n" "D1 rf det DMOD\n" ".model DMOD D IS=1e-14 N=1 BV=100\n" "* Load + filter\n" "R1 det 0 10k\n" "C1 det 0 1n\n" ".tran 0.1u 3m\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## How It Works\n\n" "The RF input is an amplitude-modulated carrier: a 1 MHz sine wave " "whose amplitude varies at the 1 kHz audio rate. The diode conducts " "only on positive half-cycles, charging C1 through the diode. " "Between peaks, C1 slowly discharges through R1.\n\n" "The RC time constant (R1 * C1 = 10us) is chosen to be:\n" "- Much longer than the carrier period (1us) to filter out RF\n" "- Much shorter than the audio period (1ms) to follow the envelope\n\n" "The result at the detector output is the recovered audio envelope." ), "outputs": [], }, ], }, "colpitts-oscillator": { "metadata": { "title": "Colpitts RF Oscillator", "engine": "ngspice", "tags": ["oscillator", "rf", "bjt", "communications"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# Colpitts RF Oscillator\n\n" "The Colpitts oscillator uses a capacitive voltage divider (C1, C2) " "with an inductor (L1) to form the resonant tank circuit. The BJT " "provides gain to sustain oscillation.\n\n" "- **Oscillation frequency:** f = 1 / (2*pi * sqrt(L * Cs))\n" "- Where Cs = C1*C2 / (C1+C2) (series combination)\n\n" "With L1 = 1uH, C1 = 100pF, C2 = 100pF:\n" "Cs = 50pF, f = 1 / (2*pi * sqrt(1u * 50p)) = ~22.5 MHz" ), "outputs": [], }, { "id": "cell-osc", "type": "spice", "source": ( "Colpitts RF Oscillator\n" "VCC vcc 0 DC 9\n" "* Bias network\n" "Rb1 vcc base 47k\n" "Rb2 base 0 10k\n" "Re emitter 0 1k\n" "Ce emitter 0 10n\n" "Rc vcc collector 1k\n" "* BJT\n" "Q1 collector base emitter 2N2222\n" ".model 2N2222 NPN(IS=14.34f BF=255.9 VAF=74.03 IKF=0.2847 ISE=14.34f NE=1.307 BR=6.092 VAR=28 IKR=0 ISC=0 NC=2 RB=10 RC=1 CJE=22.01p CJC=7.306p TF=0.4115n TR=46.91n)\n" "* Tank circuit: L + capacitive divider\n" "L1 vcc collector 1u\n" "C1 collector base 100p\n" "C2 base 0 100p\n" "* Coupling cap to output\n" "Cout collector out 10p\n" "Rload out 0 50\n" ".tran 0.5n 500n UIC\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## What to Look For\n\n" "The collector voltage should show oscillation building up from noise. " "The frequency is set by the LC tank — the capacitive divider (C1, C2) " "provides the feedback path from collector to base.\n\n" "The Colpitts topology is popular in RF circuits because the capacitive " "divider provides good frequency stability and the inductor can be a " "simple air-core coil at VHF frequencies." ), "outputs": [], }, ], }, "thermistor-bridge": { "metadata": { "title": "Thermistor Bridge Circuit", "engine": "ngspice", "tags": ["sensor", "thermistor", "bridge", "measurement"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# Thermistor Wheatstone Bridge\n\n" "A Wheatstone bridge converts small resistance changes into a " "measurable voltage. Here, an NTC thermistor replaces one bridge arm. " "As temperature changes, the thermistor resistance shifts and " "unbalances the bridge.\n\n" "The bridge is balanced (Vout = 0) when R_therm = R3 * R2/R1.\n\n" "We simulate temperature change by sweeping the thermistor value." ), "outputs": [], }, { "id": "cell-bridge", "type": "spice", "source": ( "Thermistor Wheatstone Bridge\n" "* Bridge excitation\n" "V1 vcc 0 DC 5\n" "* Fixed bridge arms\n" "R1 vcc a 10k\n" "R2 a 0 10k\n" "R3 vcc b 10k\n" "* Thermistor arm (swept from 5k to 20k to simulate temperature)\n" "Rtherm b 0 10k\n" ".dc Rtherm 5k 20k 100\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## Reading the Bridge Output\n\n" "The differential voltage V(a) - V(b) is the bridge output. At balance " "(Rtherm = 10k), both nodes sit at Vcc/2 = 2.5V and the difference is " "zero.\n\n" "As the thermistor resistance decreases (temperature rises for NTC), " "V(b) increases and the bridge output goes negative. As resistance " "increases (temperature falls), V(b) decreases and output goes positive.\n\n" "An instrumentation amplifier (like the INA128) would typically amplify " "this small differential signal for measurement." ), "outputs": [], }, ], }, "photodiode-amplifier": { "metadata": { "title": "Photodiode Transimpedance Amplifier", "engine": "ngspice", "tags": ["sensor", "photodiode", "op-amp", "transimpedance"], }, "cells": [ { "id": "cell-intro", "type": "markdown", "source": ( "# Photodiode Transimpedance Amplifier\n\n" "A transimpedance amplifier (TIA) converts the tiny current from a " "photodiode into a usable voltage. The op-amp holds the photodiode " "at virtual ground, and the feedback resistor Rf sets the conversion " "gain.\n\n" "- **Output voltage:** Vout = -Iphoto * Rf\n" "- **Bandwidth** is limited by Rf and the feedback capacitance Cf\n\n" "With Rf = 1M and a 1uA photocurrent: Vout = 1V" ), "outputs": [], }, { "id": "cell-tia", "type": "spice", "source": ( "Photodiode Transimpedance Amplifier\n" "* Dual supply\n" "VCC vcc 0 DC 12\n" "VEE vee 0 DC -12\n" "* Photodiode modeled as current source (light pulses)\n" "Iphoto 0 inv PULSE(0 1u 1m 0.1m 0.1m 2m 5m)\n" "* Feedback network\n" "Rf out inv 1MEG\n" "Cf out inv 1p\n" "* Op-amp\n" ".include /usr/share/ngspice/spice3/LM741.MOD\n" "XU1 0 inv vcc vee out LM741\n" ".tran 10u 15m\n" ".end" ), "outputs": [], }, { "id": "cell-explain", "type": "markdown", "source": ( "## Understanding the Response\n\n" "The photocurrent pulse (simulated as a current source) flows through " "the feedback resistor Rf. Since the op-amp keeps its inverting input " "at virtual ground, all the photocurrent must flow through Rf.\n\n" "The output voltage is simply Vout = Iphoto * Rf (inverted). With " "Rf = 1M ohm and Iphoto = 1uA, the output should reach ~1V.\n\n" "The small feedback capacitor Cf (1pF) prevents oscillation by " "rolling off the gain at high frequencies. Without it, the parasitic " "capacitance of the photodiode can cause the TIA to ring or oscillate." ), "outputs": [], }, ], }, } def api_put(path: str, data: dict) -> dict: """PUT JSON to the API and return parsed response.""" url = f"{API_BASE}{path}" body = json.dumps(data).encode() req = urllib.request.Request( url, data=body, method="PUT", headers={"Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: err_body = e.read().decode() if e.fp else "" print(f" ERROR {e.code}: {err_body}", file=sys.stderr) return {} def api_post(path: str, data: dict = None) -> dict: """POST JSON to the API and return parsed response.""" url = f"{API_BASE}{path}" body = json.dumps(data).encode() if data else b"{}" req = urllib.request.Request( url, data=body, method="POST", headers={"Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: err_body = e.read().decode() if e.fp else "" print(f" ERROR {e.code}: {err_body}", file=sys.stderr) return {} def main(): print(f"Creating {len(NOTEBOOKS)} notebooks via {API_BASE}") print() for nb_id, nb_data in NOTEBOOKS.items(): print(f"--- {nb_id} ---") # PUT the full notebook (inject required timestamps) metadata = {**nb_data["metadata"], "created": NOW, "modified": NOW} notebook = { "spicebook_version": "2026-02-13", "metadata": metadata, "cells": nb_data["cells"], } result = api_put(f"/api/notebooks/{nb_id}", notebook) if result: print(f" Created: {nb_data['metadata']['title']}") else: print(f" FAILED to create {nb_id}") continue # Run each SPICE cell and generate schematics for cell in nb_data["cells"]: if cell["type"] != "spice": continue cell_id = cell["id"] # Generate schematic print(f" Generating schematic for {cell_id}...") api_post(f"/api/notebooks/{nb_id}/cells/{cell_id}/schematic") # Run simulation print(f" Running simulation for {cell_id}...") sim_result = api_post(f"/api/notebooks/{nb_id}/cells/{cell_id}/run") if sim_result and sim_result.get("success"): elapsed = sim_result.get("elapsed_seconds", 0) print(f" Simulation OK ({elapsed:.3f}s)") else: error = sim_result.get("error", "unknown error") if sim_result else "no response" print(f" Simulation issue: {error}") # Small delay between cells to avoid overwhelming ngspice time.sleep(0.5) print() print("Done! All notebooks created.") if __name__ == "__main__": main()