diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 3756c1d..387ac34 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): @@ -276,18 +278,155 @@ 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: + 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): + 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] + 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} + } + 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): """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(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}) + 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 @@ -313,18 +452,121 @@ 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 +765,9 @@ 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 + stop = 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..0b9391b 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"): @@ -110,10 +110,18 @@ 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.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 {port.dc} ac {port.mag} portnum {port.num} z0 {port.res}\n" + else: + self.portsrcstr += f"{name} {port.pos} {port.neg} dc {port.dc} ac {port.mag} portnum {port.num} \n" return self._portsrcstr @portsrcstr.setter @@ -373,10 +381,10 @@ def simcmdstr(self): msg="Inferred transient duration is %g s from '%s'." % (simtime, self._trantime_name), ) - # TODO could this if-else be avoided? + tstep = val.strobeperiod if val.strobeperiod else val.tprint self._simcmdstr += ".%s %s %s %s\n" % ( sim, - str(val.tprint), + str(tstep), str(simtime), "uic" if val.uic else "", ) @@ -389,10 +397,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 +429,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 +585,25 @@ 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(".ngcir")[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" @@ -656,6 +780,32 @@ def plotcmd(self): val.ext_file, supply, ) + if name.lower() == "sp": + self._plotcmd += ".control\n" + self._plotcmd += "run\n" + printfile = ( + val.parent.spicetbsrc.split(".ngcir")[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 Rn SOpt\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(".ngcir")[0] + ".raw" + ) + self._plotcmd += ( + "wrdata %s " % (printfile) + + "onoise_spectrum inoise_spectrum\n" + ) self._plotcmd += ".endc\n" return self._plotcmd diff --git a/spice/spice_iofile.py b/spice/spice_iofile.py index 8a43dc3..41e50cb 100644 --- a/spice/spice_iofile.py +++ b/spice/spice_iofile.py @@ -181,7 +181,10 @@ 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 73fc8cd..0bee56e 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. @@ -154,6 +156,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 @@ -194,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) @@ -224,6 +230,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", []) diff --git a/spice/testbench.py b/spice/testbench.py index f43b39e..cbbd3c6 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,13 @@ 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()