From 6dc0271b39acf1ec5049b7f378c2cf7ba7259362 Mon Sep 17 00:00:00 2001 From: Vaino Kauppila Date: Sun, 11 Oct 2026 15:21:53 +0300 Subject: kisim: accept plain ngspice netlists (.cir/.spice/.net) A netlist input is used as-is instead of shelling out to kicad-cli; the netlist edits, probes and results API are unchanged. Also append the analysis lines when a netlist has no .end. --- kisim.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) (limited to 'kisim.py') 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)] -- cgit v1.3