diff --git a/spice/ngspice/ngspice.py b/spice/ngspice/ngspice.py index 3756c1d..36a7f4f 100644 --- a/spice/ngspice/ngspice.py +++ b/spice/ngspice/ngspice.py @@ -15,7 +15,8 @@ import multiprocessing from spice.spice_common import spice_common import numpy as np - +import traceback +import glob class ngspice(spice_common): """This class is used as instance in simulatormodule property of @@ -69,7 +70,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): @@ -276,18 +278,124 @@ 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 +421,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 +725,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..799f1b1 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? + tstop = val.tprint if val.tprint else val.strobeperiod self._simcmdstr += ".%s %s %s %s\n" % ( sim, - str(val.tprint), + str(tstop), 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,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" @@ -656,6 +774,23 @@ 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 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('.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_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 73fc8cd..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. @@ -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..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()