main
John Lauer Saif Abdessayed's diode-bridge VCA: KiCad project, Gerbers, BOM, simulations, reports, and the earlier RC filter test 45f3ff6 11d ago
#!/usr/bin/env python3
from pathlib import Path
import csv, re, subprocess

HERE = Path(__file__).resolve().parent
DECKS = HERE / "decks"
RESULTS = HERE / "results"
COMMON = (DECKS / "common.cir.inc").read_text()
RESULTS.mkdir(exist_ok=True)

def run(name, tail, params):
    deck = COMMON
    for key, value in params.items():
        deck = re.sub(rf"\.param {key}=[^ \n]+", f".param {key}={value}", deck)
    deck += "\n" + tail + "\n.end\n"
    path = DECKS / f"{name}.cir"
    log = RESULTS / f"{name}.log"
    path.write_text(deck)
    p = subprocess.run(["ngspice", "-b", "-o", str(log), str(path)], text=True, capture_output=True)
    if p.returncode:
        raise SystemExit(f"{name}: ngspice failed: {p.stderr}")
    return log.read_text(errors="replace")

rows = []
for vc in (0, 0.5, 1, 1.5, 2):
    name = f"small_vc_{str(vc).replace('.', 'p')}"
    text = run(name, """.tran 2u 30m 10m
.measure tran vout_pp PP v(out) from=20m to=30m
.measure tran vin_pp PP v(in) from=20m to=30m
.measure tran gain param='vout_pp/vin_pp'
.four 1k v(out)""", {"VC": vc, "VINPP": 0.1, "FREQ": "1k"})
    vals = dict(re.findall(r"^(vout_pp|vin_pp|gain)\s*=\s*([-+0-9.eE]+)", text, re.M))
    thd = re.search(r"THD:\s*([0-9.eE+-]+) %", text)
    rows.append({"control_v": vc, "vin_vpp": 0.1, "vout_vpp": float(vals["vout_pp"]), "gain": float(vals["gain"]), "thd_percent": float(thd.group(1)) if thd else None})

for vc in (0, 0.5, 1, 1.5, 2):
    name = f"large_vc_{str(vc).replace('.', 'p')}"
    text = run(name, """.tran 1u 30m 10m
.measure tran vout_pp PP v(out) from=20m to=30m
.measure tran vin_pp PP v(in) from=20m to=30m
.measure tran gain param='vout_pp/vin_pp'
.four 1k v(out)""", {"VC": vc, "VINPP": 12, "FREQ": "1k"})
    vals = dict(re.findall(r"^(vout_pp|vin_pp|gain)\s*=\s*([-+0-9.eE]+)", text, re.M))
    thd = re.search(r"THD:\s*([0-9.eE+-]+) %", text)
    rows.append({"control_v": vc, "vin_vpp": 12, "vout_vpp": float(vals["vout_pp"]), "gain": float(vals["gain"]), "thd_percent": float(thd.group(1)) if thd else None})

with (RESULTS / "transient_summary.csv").open("w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=rows[0].keys()); w.writeheader(); w.writerows(rows)

freq_rows = []
for vc in (0, 0.5, 1, 1.5, 2):
    name = f"ac_vc_{str(vc).replace('.', 'p')}"
    text = run(name, """.ac dec 200 20 20k
.print ac vm(out)
.measure ac gain20 FIND vm(out) AT=20
.measure ac gain1k FIND vm(out) AT=1k
.measure ac gain20k FIND vm(out) AT=20k""", {"VC": vc, "VINPP": 0.1, "FREQ": "1k"})
    vals = dict(re.findall(r"^(gain20|gain1k|gain20k)\s*=\s*([-+0-9.eE]+)", text, re.M))
    import math
    freq_rows.append({"control_v": vc, "gain_db_20hz": 20*math.log10(float(vals["gain20"])), "gain_db_1khz": 20*math.log10(float(vals["gain1k"])), "gain_db_20khz": 20*math.log10(float(vals["gain20k"]))})
with (RESULTS / "frequency_summary.csv").open("w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=freq_rows[0].keys()); w.writeheader(); w.writerows(freq_rows)

# Worst-case-style feedthrough model: D4 Is is 2% high and control rises in 1 ms.
mismatch = COMMON.replace("D4 brb brn D4148A", "D4 brb brn D4148B").replace("VCTRL ctrl 0 {VC}", "VCTRL ctrl 0 PWL(0 0 9m 0 10m 2 40m 2)")
mismatch = mismatch.replace("VIN in 0 SIN(0 {VINPP/2} {FREQ}) AC 1", "VIN in 0 0")
tail = """.tran 2u 40m
.print tran v(out)
.measure tran ftmax MAX v(out) from=9m to=25m
.measure tran ftmin MIN v(out) from=9m to=25m"""
path = DECKS / "control_feedthrough_mismatch.cir"
path.write_text(mismatch + "\n" + tail + "\n.end\n")
log = RESULTS / "control_feedthrough_mismatch.log"
subprocess.run(["ngspice", "-b", "-o", str(log), str(path)], check=True)
feed_text = log.read_text()
ftmax = float(re.search(r"ftmax\s*=\s*([-+0-9.eE]+)", feed_text, re.M).group(1))
ftmin = float(re.search(r"ftmin\s*=\s*([-+0-9.eE]+)", feed_text, re.M).group(1))
(RESULTS / "control_feedthrough_summary.txt").write_text(f"2% D4 Is mismatch, 0-to-2 V / 1 ms control ramp: peak |Vout| = {max(abs(ftmax), abs(ftmin)):.6g} V\n")
print("simulation complete")