Skip to content

Re-measuring

A pass a simulator reports is a claim. spicerack.remeasure recomputes the deciding number from the waveforms in a .raw file, without the simulator's .meas results and without running anything.

import os, subprocess, tempfile
from spicerack import remeasure as rm

# a .raw file from any simulator run; here, ngspice on an RC noise deck
work = tempfile.mkdtemp()
deck = os.path.join(work, "rc.sp")
with open(deck, "w") as f:
    f.write("rc\nV1 in 0 DC 0 AC 1\nR1 in out 1k\nC1 out 0 1n\n.noise v(out) V1 dec 50 1 1e9\n.end\n")
subprocess.run(["ngspice", "-b", "-r", os.path.join(work, "rc.raw"), deck], check=True, capture_output=True)

raw = rm.read(os.path.join(work, "rc.raw"))     # every plot in the file
print([p.plot_name for p in raw])                # ['Noise Spectral Density Curves', 'Integrated Noise']
print(rm.noise_total(raw))                       # V rms, about sqrt(kT/C) = 2.0e-6

Each function finds its vector in the first plot that has it; pass plot= to pick one.

Spectrum: SNDR, SFDR, THD, ENOB

# rm.sndr(raw, "v(out)", fs=5.12e6, fin=5.12e6 * 251 / 4096, n=4096)
# -> {"sndr_db", "sfdr_db", "enob", "thd_db", "signal_bin", "coherent", "signal_bins", "harmonic_bins"}

sndr resamples n points at rate fs (ending at the last time point, or starting at t0=) and needs a coherent record: fin * n / fs an integer that shares no factor with n. It refuses anything else unless you name a window (window="blackmanharris"). Run the transient with a max step well below 1/fs, because interpolating a coarse time grid adds distortion.

Time domain

Call Returns
rm.settling(raw, "v(out)", final=0.9, tol=1e-3, t0=1e-6) seconds from t0 until the signal stays within 0.1% of the step around final
rm.settling(raw, "v(out)", final=0.9, abs_tol=1e-3, t0=1e-6) the same with a band in volts
rm.slew(raw, "v(out)", lo=0.1, hi=0.9) {"rising", "falling"} in V/s between 10% and 90% of the swing

settling returns inf when the record ends outside the band.

Loop margins

rm.loop_margins(raw, plot="Loop Stability Analysis") reads a complex loop gain (ESPice .lstb writes it as loop_gain) and returns pm_deg, ugf_hz, gm_db, pcf_hz and unity_crossings. More than one unity crossing means a conditionally stable or gain-boosted loop; read the first margin with care.

Noise

rm.noise_total(raw) integrates onoise_spectrum over the swept band and returns V rms. Each segment is integrated as a power law, so a sparse log sweep stays accurate.

Yield

print(rm.yield_ci(passes=2300, n=2300, conf=0.95))   # (0.99840..., 1.0)

This is the exact (Clopper-Pearson) interval: 2300 clean Monte Carlo runs prove a yield of at least 99.84%, not 100%.

Checking a simulator's measures

ESPice writes its .meas results as JSON with --measures=meas.json. compare lists every place they disagree with your recomputed numbers:

measures = {"format": "espice-measures/1", "results": [
    {"name": "ugf", "analysis": "ac", "variant": {"kind": "nominal"}, "value": 30.3e6}]}
print(rm.compare(measures, {"ugf": 30.31e6}, rtol=1e-3))   # [] means they agree

A missing measure, a failed one and a value outside rtol/atol each give a row with a reason. Pass the file path instead of a dict for a real run.