From d2386c61cbf38f9efcb33a7e01d623488eb608ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Wed, 8 Apr 2026 08:49:56 +0300 Subject: [PATCH 01/12] add ngspice simulation commands and parsing of DC OP --- spice/ngspice/ngspice.py | 125 ++++++++++++++++++++++++--- spice/ngspice/ngspice_testbench.py | 131 ++++++++++++++++++++++++++--- spice/testbench.py | 31 +++++-- 3 files changed, 254 insertions(+), 33 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 3756c1d..8fa06c4 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -15,6 +15,8 @@ import multiprocessing from spice.spice_common import spice_common import numpy as np +import traceback +import glob class ngspice(spice_common): @@ -69,7 +71,8 @@ def syntaxdict(self, value): @property def cmdfile_ext(self): """str : Extension of the command file""" - return ".ngcir" +# return ".ngcir" + return ".spice" @property def resultfile_ext(self): @@ -313,18 +316,112 @@ def read_oppts(self): """Internally called function to read the DC operating points of the circuit""" try: - if ( - "dc" in self.parent.simcmd_bundle.Members.keys() - ): # Unsupported model - self.print_log( - type="F", - msg="DC analysis unsupported for %s" % (self.parent.model), - ) - raise Exception( - "DC optpoint extraction not supported for Eldo." - ) + if "dc" in self.parent.simcmd_bundle.Members.keys(): + self.extracts.Members.update({"oppts": {}}) + # TODO: SWEEP AND MC NOT VERIFIED + sweep = False + # Get dc simulation file name + for name, val in self.parent.simcmd_bundle.Members.items(): + mc = val.mc + if name == "dc": + fname = "" + if len(val.sweep) != 0: + for i in range(0, len(val.sweep)): + sweep = True + fname += "Sweep%d-[0-9]*_" % i + if mc: + fname += "mc_oppoint.dc" + else: + fname += "oppoint.dc" + else: + if mc: + fname = "mc_oppoint*.dc" + else: + fname = "oppoint*.dc" + break + # For distributed runs + if self.parent.distributed_run: + path = os.path.join( + self.parent.spicesimpath, + "tb_%s.raw" % self.parent.name, + "[0-9]*", + fname, + ) + else: + path = os.path.join( + self.parent.spicesimpath, + "tb_%s.raw" % self.parent.name, + ) + # Sort files so that sweeps are in correct order + if sweep: + num_sweeps = len(val.sweep) + files = glob.glob(path) + for i in range(num_sweeps): + files = sorted(files, key=lambda x: self.sorter(x, i)) + else: + files = glob.glob(path) + if len(files) > 1: # This shoudln't happen + self.print_log( + type="W", + msg="DC analysis was not a sweep, but multiple output files were found! Results may be in incorrect order!", + ) + varbegin = "Variables:\n" + variables = [] + valbegin = "Values:\n" + values = [] + parsevars = False + parsevals = False + for file in files: + with open(file, "r") as f: + for line in f: + # Scan file until unit descriptions end and values start + if (line == varbegin): + parsevars = True + # Scan values from output until EOF + elif (line != valbegin and parsevars): + parts = line.split() + if len(parts) >= 3: + variables.append(parts[1]) + elif (parsevals): + parts = line.split() + if len(parts) >= 2: + values.append(parts[1]) + elif len(parts) == 1: + values.append(parts[0]) + elif line == valbegin: + parsevars = False + parsevals = True + for i in range(len(values)): + # Found new device + var = variables[i].replace('(','.').replace(')','.').replace('[','.').replace(']','.').split('.') + if self.parent.name not in variables[i]: + # Currently node voltages are ignored + param = var[0] + dev = ''.join(var[1:]) + + elif '[' in variables[i]: + if self.parent.name in var[1]: + var = var[1:-1] + elif self.parent.name in var[2]: + var = var[2:-2] + dev = '.'.join(var[0:-2]) + param = var[-1] + elif '(' in variables[i]: + param = var[0] + dev = '.'.join(var[1:-1]) + + val = float(values[i]) + + if (dev not in self.extracts.Members["oppts"]): + self.extracts.Members["oppts"].update({dev: {}}) + # Found new parameter for device + if (param not in self.extracts.Members["oppts"][dev]): + self.extracts.Members["oppts"][dev].update({param: [val]}) + else: # Parameter already existed, just append value. This can occur in e.g. sweeps + self.extracts.Members["oppts"][dev][param].append(val) else: # DC analysis not in simcmds, oppts is empty self.extracts.Members.update({"oppts": {}}) + except: self.print_log(type="W", msg=traceback.format_exc()) self.print_log( @@ -523,11 +620,11 @@ def read_output_file(self, file, dtype): k ] # Indexing of line numbers starts from one if k == len(linenumbers) - 1: - stop = numlines - 1 + stop = numlines else: stop = ( - linenumbers[k + 1] - 6 - ) # Previous data column ends 5 rows before start of next one + linenumbers[k + 1] - 1 + ) nrows = stop - start if nrows < 20e6: self.print_log( diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 764686f..1b0c476 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -68,7 +68,7 @@ def options(self, value): @property def libcmd(self): """str : Library inclusion string. Parsed from self.spicecorner -dictionary in - the parent entity, as well as 'ELDOLIBFILE' or 'SPECTRELIBFILE' global + the parent entity, as well as 'NGSPICELIBFILE', 'ELDOLIBFILE' or 'SPECTRELIBFILE' global variables in TheSDK.config. """ if not hasattr(self, "_libcmd"): @@ -373,7 +373,6 @@ def simcmdstr(self): msg="Inferred transient duration is %g s from '%s'." % (simtime, self._trantime_name), ) - # TODO could this if-else be avoided? self._simcmdstr += ".%s %s %s %s\n" % ( sim, str(val.tprint), @@ -389,10 +388,9 @@ def simcmdstr(self): ) elif str(sim).lower() == "dc": - self.print_log( - type="E", - msg="Unsupported model %s." % self.parent.model, - ) + self._simcmdstr += ".op " + self._simcmdstr += "\n\n" + elif str(sim).lower() == "ac": if val.fscale.lower() == "dec": if val.fpoints != 0: @@ -422,7 +420,116 @@ def simcmdstr(self): val.fmax, ) self._simcmdstr += "\n\n" - + elif str(sim).lower() == "pz": + inpnode = val.inpnode + innnode = val.innnode + outpnode = val.outpnode + outnnode = val.outnnode + # TODO: could also be vol/cur or pz/pol/zer + self._simcmdstr += ".pz %s %s %s %s vol pz" % ( + inpnode, + innnode, + outpnode, + outnnode, + ) + self._simcmdstr += "\n\n" + elif str(sim).lower() == "sp": + if val.fscale.lower() == "log": + if val.fpoints != 0: + pts_str = "oct %d" % val.fpoints + elif val.fstepsize != 0: + pts_str = "dec %d" % val.fstepsize + else: + self.print_log( + type="F", + msg="Set either fpoints or fstepsize for SP simulation!", + ) + elif val.fscale.lower() == "lin": + if val.fpoints != 0: + pts_str = "lin %d" % val.fpoints + else: + self.print_log( + type="F", + msg="Set fpoints for SP simulation!", + ) + self._simcmdstr += f'.sp %s {val.fmin} {val.fmax} ' % ( + pts_str, + ) + if val.noise: + self._simcmdstr += '1' + else: + self._simcmdstr += '0' + self._simcmdstr += "\n" + elif str(sim).lower() == "noise": + if val.fscale.lower() == "log": + if val.fpoints != 0: + pts_str = "oct %d" % val.fpoints + elif val.fstepsize != 0: + pts_str = "dec %d" % val.fstepsize + else: + self.print_log( + type="F", + msg="Set either fpoints or fstepsize for noise simulation!", + ) + elif val.fscale.lower() == "lin": + if val.fpoints != 0: + pts_str = "lin %d" % val.fpoints + else: + self.print_log( + type="F", + msg="Set fpoints for noise simulation!", + ) + if len(val.nodes) == 0: + self.print_log( + type="F", + msg="Nodes list is empty. Set the nodes for noise simulation!", + ) + if val.fmin == None: + self.print_log( + type="F", + msg="Fmin must be given for noise simulation", + ) + if val.fmax == None: + self.print_log( + type="F", + msg="Fmax must be given for noise simulation", + ) + if val.noisesrc== None: + self.print_log( + type="F", + msg="noisesrc must be given for noise simulation", + ) + for node in val.nodes: + self._simcmdstr += f".noise v({node}) {val.noisesrc} {pts_str} {val.fmin} {val.fmax} \n" + elif str(sim).lower() == "pss": + if val.fsig == None: + self.print_log( + type="F", + msg="fsig must be given for PSS simulation", + ) + if val.tstab == None: + self.print_log( + type="F", + msg="tstab must be given for PSS simulation", + ) + if len(val.nodes) == 0: + self.print_log( + type="F", + msg="node must be given for PSS simulation", + ) + if val.fpoints == None: + self.print_log( + type="F", + msg="Fpoints must be defined for PSS simulation", + ) + if val.harmonics == None: + self.print_log( + type="F", + msg="Harmonics must be defined for PSS simulation", + ) + # TODO: add sciter and steady_coeff + self._simcmdstr += f".pss {val.fsig} {val.tstab} {val.nodes[0]} {val.fpoints} {val.harmonics} " + # TODO: .PSS .SENS .TF else: self.print_log( type="E", @@ -469,17 +576,19 @@ def plotcmd(self): "%s DC operating points to be captured:\n" % self.parent.spice_simulator.commentchar ) - self._plotcmd += "save " - + # control sequence: save all to get the node voltages + self._plotcmd += ".control\n" + self._plotcmd += "save all\n save " for i in val.plotlist: - self._plotcmd += self.esc_bus(i, esc_colon=False) + " " + self._plotcmd += "@" + self.esc_bus(i, esc_colon=False) + " " if val.excludelist != []: self._plotcmd += "exclude=[ " for i in val.excludelist: self._plotcmd += i + " " self._plotcmd += "]" self._plotcmd += "\n\n" - + printfile=val.parent.spicetbsrc.split('.spice')[0]+'.raw' + self._plotcmd += f"op\nset filetype=ascii\nwrite {printfile}\n" if name.lower() == "tran" or name.lower() == "ac": self._plotcmd += ( "%s Output signals\n" diff --git a/spice/testbench.py b/spice/testbench.py index f43b39e..cf7ee42 100644 --- a/spice/testbench.py +++ b/spice/testbench.py @@ -166,11 +166,16 @@ def includecmd(self, value): def copy_dspf(self): try: for cell in self.parent.dspf: + if self.model=='ngspice': + fileformat='pex.spice' + elif self.model=='spectre' or self.model=='eldo': + fileformat='pex.dspf' + src = os.path.join( - self.parent.spicesrcpath, "%s.pex.dspf" % cell + self.parent.spicesrcpath, "%s.%s" % (cell, fileformat) ) dest = os.path.join( - self.parent.spicesimpath, "%s.pex.dspf" % cell + self.parent.spicesimpath, "%s.%s" % (cell, fileformat) ) shutil.copy(src, dest) except: @@ -198,12 +203,19 @@ def dspfincludecmd(self): "%s Extracted parasitics\n" % self.parent.spice_simulator.commentchar ) - origcellmatch = re.compile(r"DESIGN") for cellname in self.parent.dspf: - dspfpath = "%s/%s.pex.dspf" % ( - self.parent.spicesimpath, - cellname, - ) + if self.model=='spectre' or self.model=='eldo': + origcellmatch = re.compile(r"DESIGN") + dspfpath = "%s/%s.pex.dspf" % ( + self.parent.spicesimpath, + cellname, + ) + elif self.model=='ngspice': + origcellmatch = re.compile(r".SUBCKT") + dspfpath = "%s/%s.pex.spice" % ( + self.parent.spicesimpath, + cellname, + ) try: found = False with open(dspfpath) as dspffile: @@ -212,7 +224,10 @@ def dspfincludecmd(self): # This mathch only check if there is a DESIGN in dpsf file. if origcellmatch.search(line) != None: words = line.split() - cellname = words[-1].replace('"', "") + if self.model=='spectre' or self.model=='eldo': + cellname = words[-1].replace('"', "") + elif self.model=='ngspice': + cellname = words[1] if ( cellname.lower() == self.parent.name.lower() From 1d89a6e7daab8dd0ae4477e9d28e890d3fb4b057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Wed, 8 Apr 2026 14:05:05 +0300 Subject: [PATCH 02/12] add support for ngspice ports and sparameter simulations --- spice/ngspice/ngspice.py | 82 ++++++++++++++++++++++++++++-- spice/ngspice/ngspice_testbench.py | 22 ++++++-- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 8fa06c4..2312607 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -18,7 +18,6 @@ import traceback import glob - class ngspice(spice_common): """This class is used as instance in simulatormodule property of spice class. Contains language dependent definitions. @@ -279,10 +278,87 @@ def run_plotprogram(self): def read_sp_result(self, **kwargs): """Internally called function to read the S-parameter simulation results""" read_type = kwargs.get("read_type") - if "sp" in self.parent.simcmd_bundle.Members.keys(): + try: + if "sp" in self.parent.simcmd_bundle.Members.keys(): + self.extracts.Members.update({read_type: {}}) + sweep = False + # For distributed runs + if self.parent.distributed_run: + # TODO: check functionality and implement + self.print_log( + type="F", + msg=f"Distributed runs not currently supported for \ + S-parameter analyses.", + ) + path = os.path.join( + self.parent.spicesimpath, + "tb_%s.raw" % self.parent.name, + "[0-9]*", + ) + else: + path = os.path.join( + self.parent.spicesimpath, + "tb_%s.raw" % self.parent.name, + ) + # Sort files such that the sweeps are in correct order. + if sweep: + num_sweeps = len(val.sweep) + files = glob.glob(path) + for i in range(num_sweeps): + files = sorted(files, key=lambda x: self.sorter(x, i)) + if len(files) > 0: + rd, fileptr = self.create_nested_sweepresult_dict( + 0, + 0, + self.extracts.Members["sweeps_ran"], + files, + read_type, + ) + else: + files = glob.glob(path) + if len(files) > 1: # This should not happen + self.print_log( + type="W", + msg="S-parameter analysis was not a sweep, but for \ + some reason multiple output files were found. \ + results may be in wrong order!", + ) + srange = range(1, len(self.parent.spice_ports)+1) + sp = [f's{i}{j}' for i in srange for j in srange] + result = {} + + if len(files) > 0: + with open(files[0], "r") as f: + for line in f: + values = line.split() + frequency = float(values[0]) + real = [float(v) for v in values[1::3]] + imag = [float(v) for v in values[2::3]] + if self.parent.noise: + nfmin = complex(real.pop(), imag.pop()) + nf = complex(real.pop(), imag.pop()) + if len(real)==len(sp): + if sp[0] not in result: + for i in range(len(sp)): + result[sp[i]]=[frequency, complex(real[i]+imag[i])] + if self.parent.noise: + result['NF']=[frequency, nf] + result['NFmin']=[frequency, nfmin] + else: + for i in range(len(sp)): + result[sp[i]]=np.vstack([result[sp[i]],[frequency, complex(real[i]+imag[i])]]) + if self.parent.noise: + result['NF']=np.vstack([result['NF'],[frequency, nf]]) + result['NFmin']=np.vstack([result['NFmin'],[frequency, nfmin]]) + rd = { + 0: {"param": "nosweep", "value": 0, read_type: result} + } + self.extracts.Members[read_type].update({"results": rd}) + except: + self.print_log(type="W", msg=traceback.format_exc()) self.print_log( type="W", - msg="S-Parameters unsupported for %s" % (self.parent.model), + msg="Something went wrong while extracting S-parameters", ) def read_noise_result(self, **kwargs): diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 1b0c476..4cc13a2 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -110,10 +110,14 @@ def portsrcstr(self): Port source defintions parsed from from self.parent.spice_ports """ if not hasattr(self, "_portsrcstr"): - self.portsrcstr = "" - self.print_log( - type="W", msg="Port support not yet implemented for ngspice!" + self._portsrcstr = ( + f"{self.parent.spice_simulator.commentchar} Port sources \n" ) + for name, port in self.parent.spice_ports.items(): + if port.res is not None: + self.portsrcstr += f"{name} {port.pos} {port.neg} dc 0 ac 1 portnum {port.num} z0 {port.res}\n" + else: + self.portsrcstr += f"{name} {port.pos} {port.neg} dc 0 ac 1 portnum {port.num} \n" return self._portsrcstr @portsrcstr.setter @@ -765,6 +769,18 @@ def plotcmd(self): val.ext_file, supply, ) + if name.lower() == "sp": + self._plotcmd += ".control\n" + self._plotcmd += "run\n" + printfile=val.parent.spicetbsrc.split('.spice')[0]+'.raw' + self._plotcmd += ("wrdata %s " % (printfile)) + srange = range(1, len(self.parent.spice_ports)+1) + sp = [f'S_{i}_{j}' for i in srange for j in srange] + self._plotcmd += (' '.join(sp)) + if self.parent.noise: + self._plotcmd += (" NF NFmin\n") + else: + self._plotcmd += ("\n") self._plotcmd += ".endc\n" return self._plotcmd From aae87398349efa8a9b025f0e5f30127d38e84edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Wed, 8 Apr 2026 15:15:11 +0300 Subject: [PATCH 03/12] add ngspice noise simulation --- spice/ngspice/ngspice.py | 29 ++++++++++++++++++++++++++--- spice/ngspice/ngspice_testbench.py | 7 ++++++- spice/spice_simcmd.py | 4 ++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 2312607..4d4dba8 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -363,10 +363,33 @@ def read_sp_result(self, **kwargs): def read_noise_result(self, **kwargs): """Internally called function to read the noise simulation results""" - if "noise" in self.parent.simcmd_bundle.Members.keys(): + try: + if "noise" in self.parent.simcmd_bundle.Members.keys(): + self.extracts.Members.update({'noise': {}}) + path = os.path.join( + self.parent.spicesimpath, + "tb_%s.raw" % self.parent.name, + ) + files = glob.glob(path) + onoise = [] + inoise = [] + freq = [] + + if len(files) > 0: + with open(files[0], "r") as f: + for line in f: + values = line.split() + freq.append(float(values[0])) + onoise.append(values[1]) + inoise.append(values[3]) + self.extracts.Members['noise'].update({"onoise_spectrum": onoise}) + self.extracts.Members['noise'].update({"inoise_spectrum": inoise}) + self.extracts.Members['noise'].update({"frequency": freq}) + except: + self.print_log(type="W", msg=traceback.format_exc()) self.print_log( - type="F", - msg="Noise analysis unsupported for %s" % (self.parent.model), + type="W", + msg="Something went wrong while extracting results of noise simulation.", ) return None, None diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 4cc13a2..75efc3f 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -498,7 +498,7 @@ def simcmdstr(self): type="F", msg="Fmax must be given for noise simulation", ) - if val.noisesrc== None: + if val.noisesrc == None: self.print_log( type="F", msg="noisesrc must be given for noise simulation", @@ -781,6 +781,11 @@ def plotcmd(self): self._plotcmd += (" NF NFmin\n") else: self._plotcmd += ("\n") + if name.lower() == "noise": + self._plotcmd += ".control\n" + self._plotcmd += "save onoise_spectrum inoise_spectrum\nrun\n" + printfile=val.parent.spicetbsrc.split('.spice')[0]+'.raw' + self._plotcmd += ("wrdata %s " % (printfile) + "onoise_spectrum inoise_spectrum\n") self._plotcmd += ".endc\n" return self._plotcmd diff --git a/spice/spice_simcmd.py b/spice/spice_simcmd.py index 73fc8cd..6ad57f6 100644 --- a/spice/spice_simcmd.py +++ b/spice/spice_simcmd.py @@ -154,6 +154,9 @@ class spice_simcmd(thesdk): For Spectre only! If true, print model parameters to raw-file. maxstep : float Maximum time step Spectre simulator will use during transient analysis + noisesrc: string + Name of an independent source to which ngspice refers noise to (in .noise simulations). + Defaul: None. step: float According to Spectre: minimum time step used by the simulator solely to maintain the aesthetics of the computed waveforms. strobeperiod: float @@ -224,6 +227,7 @@ def __init__(self, parent, **kwargs): self.iprobe = kwargs.get("iprobe", None) self.probe = kwargs.get("probe", None) self.harmonics = kwargs.get("harmonics", None) + self.noisesrc = kwargs.get("noisesrc", None) # Make list, if they are not already self.sweep = ( kwargs.get("sweep", []) From 97a59d8b0dea9d5d2359c67f2c53a9960d98b056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Thu, 9 Apr 2026 13:02:35 +0300 Subject: [PATCH 04/12] fix noise values to floats --- spice/ngspice/ngspice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 4d4dba8..4a82f87 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -380,11 +380,11 @@ def read_noise_result(self, **kwargs): for line in f: values = line.split() freq.append(float(values[0])) - onoise.append(values[1]) - inoise.append(values[3]) + onoise.append(float(values[1])) + inoise.append(float(values[3])) self.extracts.Members['noise'].update({"onoise_spectrum": onoise}) self.extracts.Members['noise'].update({"inoise_spectrum": inoise}) - self.extracts.Members['noise'].update({"frequency": freq}) + self.extracts.Members['noise'].update({"freq": freq}) except: self.print_log(type="W", msg=traceback.format_exc()) self.print_log( From dfe381fa6046634dc58a8e03a504f99de7a76470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Thu, 9 Apr 2026 13:03:17 +0300 Subject: [PATCH 05/12] prepare pss --- spice/spice_iofile.py | 2 +- spice/spice_simcmd.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spice/spice_iofile.py b/spice/spice_iofile.py index 8a43dc3..f1a426c 100644 --- a/spice/spice_iofile.py +++ b/spice/spice_iofile.py @@ -181,7 +181,7 @@ def file(self): for ioname in self.ionames: if self.dir == "out": analysis = self.parent.analysis_type - if analysis.lower() == "pss": + if analysis.lower() == "pss" and self.parent.simulator!="ngspice": filename = "tb_%s.raw/*%s.fd.pss" % ( self.parent.name, self.parent.spice_simulator.pss_analysis_name, diff --git a/spice/spice_simcmd.py b/spice/spice_simcmd.py index 6ad57f6..e54e7cc 100644 --- a/spice/spice_simcmd.py +++ b/spice/spice_simcmd.py @@ -109,6 +109,8 @@ class spice_simcmd(thesdk): Step size of the sweep simulation. Default 10. tprint : float or str Print interval. Default 1e-12 (same as '1p'). + tstab: float + Stabilization time before finding PSS for Ngspice PSS simulations. Default is 1e-6. tstop : float or str Transient simulation duration. When not defined, the simulation time is the duration of the longest input signal. @@ -197,6 +199,7 @@ def __init__(self, parent, **kwargs): self.plotlist = kwargs.get("plotlist", []) self.excludelist = kwargs.get("excludelist", []) self.tprint = kwargs.get("tprint", 1e-12) + self.tstab = kwargs.get("tstab", 1e-6) self.tstop = kwargs.get("tstop", None) self.uic = kwargs.get("uic", False) self.noise = kwargs.get("noise", False) From 5b8aae66308bb9e5593edb4647b5fdf732e9009c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Thu, 9 Apr 2026 14:30:09 +0300 Subject: [PATCH 06/12] only read noise output if simulating noise --- spice/ngspice/ngspice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 4a82f87..14180c2 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -382,9 +382,9 @@ def read_noise_result(self, **kwargs): freq.append(float(values[0])) onoise.append(float(values[1])) inoise.append(float(values[3])) - self.extracts.Members['noise'].update({"onoise_spectrum": onoise}) - self.extracts.Members['noise'].update({"inoise_spectrum": inoise}) - self.extracts.Members['noise'].update({"freq": freq}) + self.extracts.Members['noise'].update({"onoise_spectrum": onoise}) + self.extracts.Members['noise'].update({"inoise_spectrum": inoise}) + self.extracts.Members['noise'].update({"freq": freq}) except: self.print_log(type="W", msg=traceback.format_exc()) self.print_log( From 965c8197fbda9f081660b31efce27293e2af6e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Mon, 4 May 2026 15:46:37 +0300 Subject: [PATCH 07/12] fix broken sp results --- spice/ngspice/ngspice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 14180c2..7882bbb 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -340,13 +340,13 @@ def read_sp_result(self, **kwargs): if len(real)==len(sp): if sp[0] not in result: for i in range(len(sp)): - result[sp[i]]=[frequency, complex(real[i]+imag[i])] + result[sp[i]]=[frequency, complex(real[i],imag[i])] if self.parent.noise: result['NF']=[frequency, nf] result['NFmin']=[frequency, nfmin] else: for i in range(len(sp)): - result[sp[i]]=np.vstack([result[sp[i]],[frequency, complex(real[i]+imag[i])]]) + result[sp[i]]=np.vstack([result[sp[i]],[frequency, complex(real[i],imag[i])]]) if self.parent.noise: result['NF']=np.vstack([result['NF'],[frequency, nf]]) result['NFmin']=np.vstack([result['NFmin'],[frequency, nfmin]]) From b09b8c07876199be9e8c41d280b4aae5d44c93db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Tue, 5 May 2026 15:23:32 +0300 Subject: [PATCH 08/12] fix ngspice rfports --- spice/ngspice/ngspice_testbench.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 75efc3f..9ade503 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -114,10 +114,14 @@ def portsrcstr(self): f"{self.parent.spice_simulator.commentchar} Port sources \n" ) for name, port in self.parent.spice_ports.items(): + if port.dc is None: + port.dc = 0 + if port.mag is None: + port.mag = 0 if port.res is not None: - self.portsrcstr += f"{name} {port.pos} {port.neg} dc 0 ac 1 portnum {port.num} z0 {port.res}\n" + self.portsrcstr += f"{name} {port.pos} {port.neg} dc {port.dc} ac {port.mag} portnum {port.num} z0 {port.res}\n" else: - self.portsrcstr += f"{name} {port.pos} {port.neg} dc 0 ac 1 portnum {port.num} \n" + self.portsrcstr += f"{name} {port.pos} {port.neg} dc {port.dc} ac {port.mag} portnum {port.num} \n" return self._portsrcstr @portsrcstr.setter From fd0086442d407abdab610902e04761660260cdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Fri, 22 May 2026 15:00:10 +0300 Subject: [PATCH 09/12] set strobeperiod as tstep in tran --- spice/ngspice/ngspice_testbench.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 9ade503..3bffdd5 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -383,7 +383,7 @@ def simcmdstr(self): ) self._simcmdstr += ".%s %s %s %s\n" % ( sim, - str(val.tprint), + str(val.strobeperiod), str(simtime), "uic" if val.uic else "", ) From 743ba7bf11225d03291f5c713f24a10c1319171b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Mon, 25 May 2026 13:47:27 +0300 Subject: [PATCH 10/12] parse Rn and SOpt from sparams --- spice/ngspice/ngspice.py | 6 ++++++ spice/ngspice/ngspice_testbench.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 7882bbb..36a7f4f 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -335,6 +335,8 @@ def read_sp_result(self, **kwargs): real = [float(v) for v in values[1::3]] imag = [float(v) for v in values[2::3]] if self.parent.noise: + sopt = complex(real.pop(), imag.pop()) + rn = complex(real.pop(), imag.pop()) nfmin = complex(real.pop(), imag.pop()) nf = complex(real.pop(), imag.pop()) if len(real)==len(sp): @@ -344,12 +346,16 @@ def read_sp_result(self, **kwargs): if self.parent.noise: result['NF']=[frequency, nf] result['NFmin']=[frequency, nfmin] + result['Rn']=[frequency, rn] + result['SOpt']=[frequency, sopt] else: for i in range(len(sp)): result[sp[i]]=np.vstack([result[sp[i]],[frequency, complex(real[i],imag[i])]]) if self.parent.noise: result['NF']=np.vstack([result['NF'],[frequency, nf]]) result['NFmin']=np.vstack([result['NFmin'],[frequency, nfmin]]) + result['Rn']=np.vstack([result['Rn'],[frequency, rn]]) + result['SOpt']=np.vstack([result['SOpt'],[frequency, sopt]]) rd = { 0: {"param": "nosweep", "value": 0, read_type: result} } diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 3bffdd5..db03663 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -782,7 +782,7 @@ def plotcmd(self): sp = [f'S_{i}_{j}' for i in srange for j in srange] self._plotcmd += (' '.join(sp)) if self.parent.noise: - self._plotcmd += (" NF NFmin\n") + self._plotcmd += (" NF NFmin Rn SOpt\n") else: self._plotcmd += ("\n") if name.lower() == "noise": From 56c26586f715b71829afa892c93eddb9a50073f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Mon, 8 Jun 2026 09:23:43 +0300 Subject: [PATCH 11/12] select tprint as timestep if available --- spice/ngspice/ngspice_testbench.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index db03663..799f1b1 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -381,9 +381,10 @@ def simcmdstr(self): msg="Inferred transient duration is %g s from '%s'." % (simtime, self._trantime_name), ) + tstop = val.tprint if val.tprint else val.strobeperiod self._simcmdstr += ".%s %s %s %s\n" % ( sim, - str(val.strobeperiod), + str(tstop), str(simtime), "uic" if val.uic else "", ) From 905867d0ae8f0b56c85d1c05e09a14400f5385cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miikka=20M=C3=A4ki?= Date: Tue, 9 Jun 2026 09:30:22 +0300 Subject: [PATCH 12/12] oops wrong way around --- spice/ngspice/ngspice_testbench.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spice/ngspice/ngspice_testbench.py b/spice/ngspice/ngspice_testbench.py index 799f1b1..4e0e6ca 100644 --- a/spice/ngspice/ngspice_testbench.py +++ b/spice/ngspice/ngspice_testbench.py @@ -381,10 +381,10 @@ def simcmdstr(self): msg="Inferred transient duration is %g s from '%s'." % (simtime, self._trantime_name), ) - tstop = val.tprint if val.tprint else val.strobeperiod + tstep = val.strobeperiod if val.strobeperiod else val.tprint self._simcmdstr += ".%s %s %s %s\n" % ( sim, - str(tstop), + str(tstep), str(simtime), "uic" if val.uic else "", )