aboutsummaryrefslogtreecommitdiffstats
path: root/kisim.py
diff options
context:
space:
mode:
Diffstat (limited to 'kisim.py')
-rw-r--r--kisim.py33
1 files changed, 22 insertions, 11 deletions
diff --git a/kisim.py b/kisim.py
index 167052a..ec689e7 100644
--- a/kisim.py
+++ b/kisim.py
@@ -1,13 +1,17 @@
-"""kisim -- run a KiCad schematic in ngspice from Python and read signals by name.
+"""kisim -- run a KiCad schematic (or a plain ngspice netlist) in ngspice from
+Python and read signals by name.
-Single file, no project knowledge. Needs: kicad-cli, ngspice, numpy, spicelib.
+Single file, no project knowledge. Needs: ngspice, numpy, spicelib
+(kicad-cli only when the input is a .kicad_sch). A .cir/.spice/.net input is
+run as-is, no KiCad needed.
-From a script in a subfolder of sim/:
+From a script in a project subfolder:
import sys; from pathlib import Path; sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from kisim import KiSim
sim = KiSim("softstart.kicad_sch", tran="10u 400m") # path relative to the calling script
+ sim = KiSim("rc_lp.cir", tran="1u 2m") # plain netlist: run as-is, no kicad-cli
r = sim.run() # or sim.run(params={"X1": {"TACT": "250m"}})
r = sim.run(subs={r"^V1 .*$": "V1 +28V GND DC 35"}, tag="v35") # regex edits of netlist lines
r = sim.run(sources={"V1": pwl((0, 0), (1e-3, 28))}, tag="ramp") # replace a source's value
@@ -23,6 +27,7 @@ From a script in a subfolder of sim/:
Signal names: ngspice lower-cases everything and keeps KiCad's net names
(e.g. "Net-(Q2-D)" -> "v(net-_q2-d_)"; KiCad's SPICE export swaps "(" ")" for "_").
+Plain netlists keep their own node names as-is.
Subcircuit internals are "v(x1.p12r)" style. r.names lists all of them.
Saving: by default ngspice keeps every node voltage and every voltage-source
current. save=[...] keeps only the listed signals: KiCad net names ("+12V",
@@ -60,7 +65,8 @@ class KiSim:
extra: list[str] | None = None, currents: bool = False, power: list[str] | None = None,
workdir=None, ngspice: str = "ngspice", compat: str | None = None,
save: list[str] | None = None):
- """schematic: .kicad_sch path (relative paths resolve against the calling script).
+ """schematic: .kicad_sch path, or a plain ngspice netlist (.cir/.spice/.net) used
+ as-is (no kicad-cli). Relative paths resolve against the calling script.
tran: '.tran' arguments. options: '.options' line contents. extra: raw netlist lines
to add (e.g. '.ic', '.param'). currents: also save all device terminal currents
('.probe alli', off by default). save: only keep these signals (KiCad net names
@@ -70,6 +76,7 @@ class KiSim:
compat: ngspice compatibility mode, e.g. "psa" for PSpice vendor models (TI)."""
caller = Path(inspect.stack()[1].filename).resolve().parent
self.sch = (caller / schematic).resolve()
+ self.plain = self.sch.suffix.lower() in (".cir", ".spice", ".net") # netlist: no kicad-cli
self.tran, self.options, self.extra, self.ngspice = tran, options, extra or [], ngspice
self.compat = ["-D", f"ngbehavior={compat}"] if compat else []
self.currents, self.power, self.save = currents, power or [], save or []
@@ -78,13 +85,16 @@ class KiSim:
def netlist(self, params: dict | None = None, tag: str = "run", subs: dict | None = None,
sources: dict | None = None, tran: str | None = None) -> Path:
- """Export the schematic to a SPICE netlist, apply parameter overrides, source values
- (sources={"V1": "PWL(...)"}: everything after the two nodes is replaced) and regex
- substitutions (subs={pattern: replacement}, multiline), add the analysis."""
+ """Get the netlist (KiCad export, or the plain file as-is), apply parameter overrides,
+ source values (sources={"V1": "PWL(...)"}: everything after the two nodes is replaced)
+ and regex substitutions (subs={pattern: replacement}, multiline), add the analysis."""
cir = self.workdir / f"{tag}.cir"
- subprocess.run(["kicad-cli", "sch", "export", "netlist", "--format", "spice", "-o", cir, self.sch],
- check=True, capture_output=True)
- lines = cir.read_text().splitlines()
+ if self.plain: # plain netlist: take the file as-is
+ lines = self.sch.read_text().splitlines()
+ else: # KiCad schematic: export it first
+ subprocess.run(["kicad-cli", "sch", "export", "netlist", "--format", "spice", "-o", cir, self.sch],
+ check=True, capture_output=True)
+ lines = cir.read_text().splitlines()
def find(ref): # schematic ref -> netlist line (Q2 may be XQ2)
for i, l in enumerate(lines):
if l.split()[:1] and l.split()[0].upper() in (ref.upper(), "X" + ref.upper()):
@@ -105,7 +115,8 @@ class KiSim:
if not n:
raise ValueError(f"subs pattern {pat!r} matched nothing")
lines = text.splitlines()
- end = max(i for i, l in enumerate(lines) if l.strip().lower() == ".end")
+ ends = [i for i, l in enumerate(lines) if l.strip().lower() == ".end"]
+ end = ends[-1] if ends else len(lines)
probes = [".save all", ".probe alli"] if self.currents else []
if self.save: # KiCad names -> v(node); raw names pass through
probes += [".save " + " ".join(n.lower() if "(" in n else f"v({Result.node(n)})" for n in self.save)]