| #!/usr/bin/env python3 |
| # Script to read all files in a directory of SPECTRE-compatible device model |
| # files, and convert them to a form that is compatible with ngspice. |
| |
| import csv |
| import glob |
| import json |
| import os |
| import pprint |
| import re |
| import sys |
| import textwrap |
| |
| from collections import defaultdict |
| |
| |
| WIDTH=200 |
| |
| |
| def usage(): |
| print('spectre_to_spice.py <path_to_spectre> <path_to_spice>') |
| print(' [-nocmt] [-debug] [-ignore=file1,file2,...]') |
| |
| # Check if a parameter value is a valid number (real, float, integer) |
| # or is some kind of expression. |
| |
| def is_number(s): |
| try: |
| float(s) |
| return True |
| except ValueError: |
| return False |
| |
| # Parse a parameter line. If "inparam" is true, then this is a continuation |
| # line of an existing parameter statement. If "insub" is not true, then the |
| # paramters are global parameters (not part of a subcircuit). |
| # |
| # If inside a subcircuit, remove the keyword "parameters". If outside, |
| # change it to ".param" |
| |
| def parse_param_line(line, inparam, insub, iscall, ispassed): |
| |
| # Regexp patterns |
| parm1rex = re.compile('[ \t]*parameters[ \t]*(.*)') |
| parm2rex = re.compile('[ \t]*params:[ \t]*(.*)') |
| parm3rex = re.compile('[ \t]*\+[ \t]*(.*)') |
| parm4rex = re.compile('[ \t]*([^= \t]+)[ \t]*=[ \t]*([^ \t]+)[ \t]*(.*)') |
| parm5rex = re.compile('[ \t]*([^= \t]+)[ \t]*(.*)') |
| parm6rex = re.compile('[ \t]*([^= \t]+)[ \t]*=[ \t]*([\'{][^\'}]+[\'}])[ \t]*(.*)') |
| rtok = re.compile('([^ \t\n]+)[ \t]*(.*)') |
| |
| fmtline = [] |
| |
| if iscall: |
| rest = line |
| elif inparam: |
| pmatch = parm3rex.match(line) |
| if pmatch: |
| fmtline.append('+') |
| rest = pmatch.group(1) |
| else: |
| return '', ispassed |
| else: |
| pmatch = parm1rex.match(line) |
| if pmatch: |
| if insub: |
| fmtline.append('+') |
| else: |
| fmtline.append('.param') |
| rest = pmatch.group(1) |
| else: |
| pmatch = parm2rex.match(line) |
| if pmatch: |
| if insub: |
| fmtline.append('+') |
| else: |
| return '', ispassed |
| rest = pmatch.group(1) |
| else: |
| return '', ispassed |
| |
| while rest != '': |
| if iscall: |
| # It is hard to believe this is legal syntax, but there are |
| # instances of "x * y" with spaces in the expression and no |
| # quotes or braces delimiting the expression. |
| rest = re.sub('[ \t]*\*[ \t]*', '*', rest) |
| |
| pmatch = parm4rex.match(rest) |
| if pmatch: |
| if ispassed: |
| # End of passed parameters. Break line and generate ".param" |
| ispassed = False |
| fmtline.append('\n.param ') |
| |
| # If expression is already in single quotes or braces, then catch |
| # everything inside the delimiters, including any spaces. |
| if pmatch.group(2).startswith("'") or pmatch.group(2).startswith('{'): |
| pmatchx = parm6rex.match(rest) |
| if pmatchx: |
| pmatch = pmatchx |
| |
| fmtline.append(pmatch.group(1)) |
| fmtline.append('=') |
| value = pmatch.group(2) |
| rest = pmatch.group(3) |
| |
| # Watch for spaces in expressions (have they no rules??!) |
| # as indicated by something after a space not being an |
| # alphabetical character (parameter name) or '$' (comment) |
| |
| needmore = False |
| while rest != '': |
| rmatch = rtok.match(rest) |
| if rmatch: |
| expch = rmatch.group(1)[0] |
| if expch == '$': |
| break |
| elif expch.isalpha() and not needmore: |
| break |
| else: |
| needmore = False |
| value += rmatch.group(1) |
| rest = rmatch.group(2) |
| if any((c in '+-*/({^~!') for c in rmatch.group(1)[-1]): |
| needmore = True |
| if rest != '' and any((c in '+-*/(){}^~!') for c in rest[0]): |
| needmore = True |
| else: |
| break |
| |
| if is_number(value): |
| fmtline.append(value) |
| elif value.strip().startswith("'"): |
| fmtline.append(value) |
| else: |
| # It is not possible to know if a spectre expression continues |
| # on another line without some kind of look-ahead, but check |
| # if the parameter ends in an operator. |
| lastc = value.strip()[-1] |
| if any((c in '*+-/,(') for c in lastc): |
| fmtline.append('{' + value) |
| else: |
| fmtline.append('{' + value + '}') |
| |
| # These parameter sub-expressions are related to monte carlo |
| # simulation and are incompatible with ngspice. So put them |
| # in an in-line comment. Avoid double-commenting things that |
| # were already in-line comments. |
| |
| if rest != '': |
| nmatch = parm4rex.match(rest) |
| if not nmatch: |
| if rest.lstrip().startswith('$ '): |
| fmtline.append(rest) |
| elif rest.strip() != '': |
| fmtline.append(' $ ' + rest.replace(' ', '').replace('\t', '')) |
| rest = '' |
| else: |
| # Match to a CDL subckt parameter that does not have an '=' and so |
| # assumes that the parameter is always passed, and therefore must |
| # be part of the .subckt line. A parameter without a value is not |
| # legal SPICE, so supply a default value of 1. |
| |
| pmatch = parm5rex.match(rest) |
| if pmatch: |
| # NOTE: Something that is not a parameter name should be |
| # extended from the previous line. Note that this parsing is |
| # not rigorous and is possible to break. . . |
| if any((c in '+-*/(){}^~!') for c in pmatch.group(1).strip()): |
| fmtline.append(rest) |
| if not any((c in '*+-/,(') for c in rest.strip()[-1]): |
| fmtline.append('}') |
| rest = '' |
| else: |
| # Parameter name must have a value. Set default 1 |
| fmtline.append(pmatch.group(1) + '=1') |
| ispassed = True |
| rest = pmatch.group(2) |
| else: |
| break |
| |
| finalline = ' '.join(fmtline) |
| |
| # ngspice does not understand round(), so replace it with the equivalent |
| # floor() expression. |
| |
| finalline = re.sub('round\\(', 'floor(0.5+', finalline) |
| |
| # use of "no" and "yes" as parameter values is not allowed in ngspice. |
| |
| finalline = re.sub('sw_et[ \t]*=[ \t]*{no}', 'sw_et=0', finalline) |
| finalline = re.sub('sw_et[ \t]*=[ \t]*{yes}', 'sw_et=1', finalline) |
| finalline = re.sub('isnoisy[ \t]*=[ \t]*{no}', 'isnoisy=0', finalline) |
| finalline = re.sub('isnoisy[ \t]*=[ \t]*{yes}', 'isnoisy=1', finalline) |
| |
| # Use of "m" in parameters is forbidden. Specifically look for "{N*m}". |
| # e.g., replace "mult = {2*m}" with "mult = 2". Note that this usage |
| # is most likely an error in the source. |
| |
| finalline = re.sub('\{([0-9]+)\*[mM]\}', r'\1', finalline) |
| |
| return finalline, ispassed |
| |
| def get_param_names(line): |
| # Find parameter names in a ".param" line and return a list of them. |
| # This is used to check if a bare word parameter name is passed to |
| # a capacitor or resistor device in the position of a value but |
| # without delimiters, so that it cannot be distinguished from a |
| # model name. There are only a few instances of this, so this |
| # routine is not rigorously checking all parameters, just entries |
| # on lines with ".param". |
| parmrex = re.compile('[ \t]*([^= \t]+)[ \t]*=[ \t]*([^ \t]+)[ \t]*(.*)') |
| rest = line |
| paramnames = [] |
| while rest != '': |
| pmatch = parmrex.match(rest) |
| if pmatch: |
| paramnames.append(pmatch.group(1)) |
| rest = pmatch.group(3) |
| else: |
| break |
| return paramnames |
| |
| def convert_file(in_file, out_file, nocmt, ignore='', debug=False): |
| #if in_file.endswith('monte.cor') or in_file.endswith('models.all'): |
| # print("Skipping", in_file) |
| # return |
| |
| if in_file in ignore: |
| print('Skipping ', in_file) |
| return |
| |
| print("Starting to convert", in_file) |
| |
| # Regexp patterns |
| setsoarex = re.compile('[ \t]*\.setsoa[ \t]+', re.IGNORECASE) |
| statrex = re.compile('[ \t]*statistics[ \t]*\{(.*)') |
| simrex = re.compile('[ \t]*simulator[ \t]+([^= \t]+)[ \t]*=[ \t]*(.+)') |
| insubrex = re.compile('[ \t]*inline[ \t]+subckt[ \t]+([^ \t\(]+)[ \t]*\(([^)]*)') |
| cdlsubrex = re.compile('\.?subckt[ \t]+([^ \t\(]+)[ \t]*\(([^)]*)') |
| endsubrex = re.compile('[ \t]*ends[ \t]*(.*)') |
| endonlysubrex = re.compile('[ \t]*ends[ \t]*') |
| modelrex = re.compile('[ \t]*model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+\{(.*)') |
| cdlmodelrex = re.compile('[ \t]*model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.*)') |
| binrex = re.compile('[ \t]*([0-9]+):[ \t]+type[ \t]*=[ \t]*(.*)') |
| shincrex = re.compile('\.inc[ \t]+') |
| isexprrex = re.compile('[^0-9a-zA-Z_]') |
| paramrex = re.compile('\.param[ \t]+(.*)') |
| |
| stdsubrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+(.*)') |
| stdmodelrex = re.compile('\.model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]*(.*)') |
| stdendsubrex = re.compile('\.ends[ \t]+(.+)') |
| stdendonlysubrex = re.compile('\.ends[ \t]*') |
| |
| # Devices (resistor, capacitor, subcircuit as resistor or capacitor) |
| caprex = re.compile('c([^ \t]+)[ \t]*\(([^)]*)\)[ \t]*capacitor[ \t]*(.*)', re.IGNORECASE) |
| resrex = re.compile('r([^ \t]+)[ \t]*\(([^)]*)\)[ \t]*resistor[ \t]*(.*)', re.IGNORECASE) |
| cdlrex = re.compile('[ \t]*([npcrdlmqx])([^ \t]+)[ \t]*\(([^)]*)\)[ \t]*([^ \t]+)[ \t]*(.*)', re.IGNORECASE) |
| stddevrex = re.compile('[ \t]*([cr])([^ \t]+)[ \t]+([^ \t]+[ \t]+[^ \t]+)[ \t]+([^ \t]+)[ \t]*(.*)', re.IGNORECASE) |
| stddev2rex = re.compile('[ \t]*([cr])([^ \t]+)[ \t]+([^ \t]+[ \t]+[^ \t]+)[ \t]+([^ \t\'{]+[\'{][^\'}]+[\'}])[ \t]*(.*)', re.IGNORECASE) |
| stddev3rex = re.compile('[ \t]*([npcrdlmqx])([^ \t]+)[ \t]+(.*)', re.IGNORECASE) |
| |
| in_dir = os.path.dirname(in_file) |
| |
| with open(in_file, 'r') as ifile: |
| idata = ifile.read() |
| |
| in_s8xfile = '../../s8x/Models/'+os.path.basename(in_file) |
| |
| def include(m): |
| incfile = m.group('file') |
| print(" Includes", incfile) |
| if incfile == in_s8xfile or incfile.endswith('sonos_tteol.cor'): |
| s8xpath = os.path.abspath(os.path.join(in_dir, incfile)) |
| print(" Pulling", s8xpath, "({}) into".format(incfile), in_file) |
| return open(s8xpath).read() |
| else: |
| return m.group(0) |
| |
| idata = RE_TRAILING_WS.sub('', idata) |
| idata = RE_INCLUDE.sub(include, idata) |
| |
| if nocmt: |
| speclines = rewrite_comments(out_file, idata) |
| else: |
| speclines = idata.splitlines() |
| |
| insub = False |
| inparam = False |
| inmodel = False |
| inpinlist = False |
| isspectre = False |
| ispassed = False |
| spicelines = [] |
| calllines = [] |
| modellines = [] |
| paramnames = [] |
| savematch = None |
| blockskip = 0 |
| subname = '' |
| subnames = [] |
| modname = '' |
| modtype = '' |
| |
| # For future reference: A much better approach to this is to first contatenate |
| # all continuation lines by replacing the "+" with "\n", which can be treated |
| # as whitespace in any regexp. This makes parsing continuation lines much |
| # simpler, as there is no need to do any "look-ahead" to check what's on the |
| # next line(s). At the end, all "\n" in strings can be replaced by "\n+", |
| # which preserves the original formatting of the continuation lines. |
| |
| for line in speclines: |
| |
| # Item 1a. C++-style // comments get replaced with * comment character |
| if line.strip().startswith('//'): |
| # Replace the leading "//" with SPICE-style comment "*". |
| if modellines != []: |
| modellines.append(line.strip().replace('//', '*', 1)) |
| elif calllines != []: |
| calllines.append(line.strip().replace('//', '*', 1)) |
| else: |
| spicelines.append(line.strip().replace('//', '*', 1)) |
| continue |
| |
| # Item 1b. In-line C++-style // comments get replaced with $ comment character |
| elif ' //' in line: |
| line = line.replace(' //', ' $ ', 1) |
| elif '//' in line: |
| line = line.replace('//', ' $ ', 1) |
| elif '\t//' in line: |
| line = line.replace('\t//', '\t$ ', 1) |
| |
| # Item 2. Handle SPICE-style comment lines |
| if line.strip().startswith('*'): |
| if modellines != []: |
| modellines.append(line.strip()) |
| elif calllines != []: |
| calllines.append(line.strip()) |
| else: |
| spicelines.append(line.strip()) |
| continue |
| |
| # Item 4. Flag continuation lines |
| if line.strip().startswith('+'): |
| contline = True |
| else: |
| contline = False |
| if line.strip() != '': |
| if inparam: |
| inparam = False |
| if inpinlist: |
| inpinlist = False |
| |
| # Item 3. Handle blank lines like comment lines |
| if line.strip() == '': |
| if modellines != []: |
| modellines.append(line.strip()) |
| elif calllines != []: |
| calllines.append(line.strip()) |
| else: |
| spicelines.append(line.strip()) |
| continue |
| |
| # Item 5. Count through { ... } blocks that are not SPICE syntax |
| if blockskip > 0: |
| # Warning: Assumes one brace per line, may or may not be true |
| if '{' in line: |
| blockskip = blockskip + 1 |
| elif '}' in line: |
| blockskip = blockskip - 1 |
| if blockskip == 0: |
| spicelines.append('* ' + line) |
| continue |
| |
| if blockskip > 0: |
| spicelines.append('* ' + line) |
| continue |
| |
| # Item 6. Handle continuation lines |
| if contline: |
| if inparam: |
| # Continue handling parameters |
| fmtline, ispassed = parse_param_line(line, inparam, insub, False, ispassed) |
| if fmtline != '': |
| if modellines != []: |
| modellines.append(fmtline) |
| elif calllines != []: |
| calllines.append(fmtline) |
| else: |
| spicelines.append(fmtline) |
| continue |
| |
| # Item 7. Regexp matching |
| |
| # Catch "simulator lang=" |
| smatch = simrex.match(line) |
| if smatch: |
| if smatch.group(1) == 'lang': |
| if smatch.group(2) == 'spice': |
| isspectre = False |
| elif smatch.group(2) == 'spectre': |
| isspectre = True |
| continue |
| |
| # If inside a subcircuit, remove "parameters". If outside, |
| # change it to ".param" |
| fmtline, ispassed = parse_param_line(line, inparam, insub, False, ispassed) |
| if fmtline != '': |
| inparam = True |
| spicelines.append(fmtline) |
| continue |
| |
| # ".setsoa" not handled by ngspice (comment it out) |
| smatch = setsoarex.match(line) |
| if smatch: |
| spicelines.append('* ' + line) |
| continue |
| |
| # statistics---not sure if it is always outside an inline subcircuit |
| smatch = statrex.match(line) |
| if smatch: |
| if '}' not in smatch.group(1): |
| blockskip = 1 |
| spicelines.append('* ' + line) |
| continue |
| |
| # model---not sure if it is always inside an inline subcircuit |
| iscdl = False |
| if isspectre: |
| mmatch = modelrex.match(line) |
| if not mmatch: |
| mmatch = cdlmodelrex.match(line) |
| if mmatch: |
| iscdl = True |
| else: |
| mmatch = stdmodelrex.match(line) |
| |
| if mmatch: |
| modname = mmatch.group(1) |
| modtype = mmatch.group(2) |
| |
| if isspectre and '}' in mmatch.group(1): |
| savematch = mmatch |
| inmodel = 1 |
| # Continue to "if inmodel == 1" block below |
| else: |
| fmtline, ispassed = parse_param_line(mmatch.group(3), True, False, True, ispassed) |
| if isspectre and (modtype == 'resistor' or modtype == 'r2'): |
| modtype = 'r' |
| modellines.append('.model ' + modname + ' ' + modtype + ' ' + fmtline) |
| if fmtline != '': |
| inparam = True |
| |
| inmodel = 2 |
| continue |
| |
| if not insub: |
| # Things to parse if not in a subcircuit |
| imatch = insubrex.match(line) if isspectre else None |
| |
| if not imatch: |
| # Check for spectre format subckt or CDL format .subckt lines |
| imatch = cdlsubrex.match(line) |
| |
| if not imatch: |
| if not isspectre: |
| # Check for standard SPICE format .subckt lines |
| imatch = stdsubrex.match(line) |
| |
| if imatch: |
| # If a model block is pending, then dump it |
| if modellines != []: |
| for line in modellines: |
| spicelines.append(line) |
| modellines = [] |
| inmodel = False |
| |
| insub = True |
| ispassed = True |
| subname = imatch.group(1) |
| subnames.append(subname) |
| if isspectre: |
| devrex = re.compile(subname + '[ \t]*\(([^)]*)\)[ \t]*([^ \t]+)[ \t]*(.*)', re.IGNORECASE) |
| else: |
| devrex = re.compile(subname + '[ \t]*([^ \t]+)[ \t]*([^ \t]+)[ \t]*(.*)', re.IGNORECASE) |
| # If there is no close-parenthesis then we should expect it on |
| # a continuation line |
| inpinlist = True if ')' not in line else False |
| # Remove parentheses groups from subcircuit arguments |
| spicelines.append('.subckt ' + ' ' + subname + ' ' + imatch.group(2)) |
| continue |
| |
| else: |
| # Things to parse when inside of an "inline subckt" block |
| |
| if inpinlist: |
| # Watch for pin list continuation line. |
| if isspectre: |
| if ')' in line: |
| inpinlist = False |
| pinlist = line.replace(')', '') |
| spicelines.append(pinlist) |
| else: |
| spicelines.append(line) |
| continue |
| |
| else: |
| if isspectre: |
| ematch = endsubrex.match(line) |
| if not ematch: |
| ematch = endonlysubrex.match(line) |
| else: |
| ematch = stdendsubrex.match(line) |
| if not ematch: |
| ematch = stdendonlysubrex.match(line) |
| |
| if ematch: |
| try: |
| endname = ematch.group(1).strip() |
| except: |
| pass |
| else: |
| if endname != subname and endname != '': |
| print('Error: "ends" name does not match "subckt" name!') |
| print('"ends" name = ' + endname) |
| print('"subckt" name = ' + subname) |
| if len(calllines) > 0: |
| line = calllines[0] |
| if modtype.startswith('bsim'): |
| line = 'M' + line |
| elif modtype.startswith('nmos'): |
| line = 'M' + line |
| elif modtype.startswith('pmos'): |
| line = 'M' + line |
| elif modtype.startswith('res'): |
| line = 'R' + line |
| elif modtype.startswith('cap'): |
| line = 'C' + line |
| elif modtype.startswith('pnp'): |
| line = 'Q' + line |
| elif modtype.startswith('npn'): |
| line = 'Q' + line |
| elif modtype.startswith('d'): |
| line = 'D' + line |
| spicelines.append(line) |
| |
| for line in calllines[1:]: |
| spicelines.append(line) |
| calllines = [] |
| |
| # Last check: Do any model types confict with the way they |
| # are called within the subcircuit? Spectre makes it very |
| # hard to know what type of device is being instantiated. . . |
| |
| for modelline in modellines: |
| mmatch = stdmodelrex.match(modelline) |
| if mmatch: |
| modelname = mmatch.group(1).lower().split('.')[0] |
| modeltype = mmatch.group(2).lower() |
| newspicelines = [] |
| for line in spicelines: |
| cmatch = stddev3rex.match(line) |
| if cmatch: |
| devtype = cmatch.group(1).lower() |
| if modelname in cmatch.group(3): |
| if devtype == 'x': |
| if modeltype == 'pnp' or modeltype == 'npn': |
| line = 'q' + line[1:] |
| elif modeltype == 'c' or modeltype == 'r': |
| line = modeltype + line[1:] |
| elif modeltype == 'd': |
| line = modeltype + line[1:] |
| elif modeltype == 'nmos' or modeltype == 'pmos': |
| line = 'm' + line[1:] |
| newspicelines.append(line) |
| spicelines = newspicelines |
| |
| # Now add any in-circuit models |
| spicelines.append('') |
| for line in modellines: |
| spicelines.append(line) |
| modellines = [] |
| |
| # Complete the subcircuit definition |
| spicelines.append('.ends ' + subname) |
| |
| insub = False |
| inmodel = False |
| subname = '' |
| paramnames = [] |
| continue |
| |
| # Check for close of model |
| if isspectre and inmodel: |
| if '}' in line: |
| inmodel = False |
| continue |
| |
| # Check for devices R and C. |
| dmatch = caprex.match(line) |
| if dmatch: |
| fmtline, ispassed = parse_param_line(dmatch.group(3), True, insub, True, ispassed) |
| if fmtline != '': |
| inparam = True |
| spicelines.append('c' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + fmtline) |
| continue |
| else: |
| spicelines.append('c' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + dmatch.group(3)) |
| continue |
| |
| dmatch = resrex.match(line) |
| if dmatch: |
| fmtline, ispassed = parse_param_line(dmatch.group(3), True, insub, True, ispassed) |
| if fmtline != '': |
| inparam = True |
| spicelines.append('r' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + fmtline) |
| continue |
| else: |
| spicelines.append('r' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + dmatch.group(3)) |
| continue |
| |
| cmatch = cdlrex.match(line) |
| if not cmatch: |
| if not isspectre or 'capacitor' in line or 'resistor' in line: |
| cmatch = stddevrex.match(line) |
| |
| if cmatch: |
| ispassed = False |
| devtype = cmatch.group(1) |
| devmodel = cmatch.group(4) |
| |
| # Handle spectreisms. . . |
| if devmodel == 'capacitor': |
| devtype = 'c' |
| devmodel = '' |
| elif devmodel == 'resistor': |
| devtype = 'r' |
| devmodel = '' |
| elif devtype.lower() == 'n' or devtype.lower() == 'p': |
| # May be specific to SkyWater models, or is it a spectreism? |
| devtype = 'x' + devtype |
| |
| # If a capacitor or resistor value is a parameter or expression, |
| # it must be enclosed in single quotes. Otherwise, if the named |
| # device model is a subcircuit, then the devtype must be "x". |
| |
| elif devtype.lower() == 'c' or devtype.lower() == 'r': |
| if devmodel in subnames: |
| devtype = 'x' + devtype |
| else: |
| devvalue = devmodel.split('=') |
| if len(devvalue) > 1: |
| if "'" in devvalue[1] or "{" in devvalue[1]: |
| # Re-parse this catching everything in delimiters, |
| # including spaces. |
| cmatch2 = stddev2rex.match(line) |
| if cmatch2: |
| cmatch = cmatch2 |
| devtype = cmatch.group(1) |
| devmodel = cmatch.group(4) |
| devvalue = devmodel.split('=') |
| |
| if isexprrex.search(devvalue[1]): |
| if devvalue[1].strip("'") == devvalue[1]: |
| devmodel = devvalue[0] + "='" + devvalue[1] + "'" |
| else: |
| if devmodel in paramnames or isexprrex.search(devmodel): |
| if devmodel.strip("'") == devmodel: |
| devmodel = "'" + devmodel + "'" |
| |
| fmtline, ispassed = parse_param_line(cmatch.group(5), True, insub, True, ispassed) |
| if fmtline != '': |
| inparam = True |
| # "perim" in diodes is not permitted by ngspice |
| if devtype == 'd': |
| fmtline = re.sub('[ \t]+perim[ \t]*=[ \t]*[^ \t]+', '', fmtline) |
| spicelines.append(devtype + cmatch.group(2) + ' ' + cmatch.group(3) + ' ' + devmodel + ' ' + fmtline) |
| continue |
| else: |
| spicelines.append(devtype + cmatch.group(2) + ' ' + cmatch.group(3) + ' ' + devmodel + ' ' + cmatch.group(5)) |
| continue |
| else: |
| # SkyWater-specific handling for model nvhv and pvhv, which |
| # call the _base subcircuit as type 'm'. |
| cmatch = stddev3rex.match(line) |
| if cmatch: |
| devtype = cmatch.group(1) |
| if devtype.lower() == 'm': |
| devname = cmatch.group(2) |
| if devname == 'ain1': |
| line = 'x' + line |
| |
| # Check for a .param line and collect parameter names |
| pmatch = paramrex.match(line) |
| if pmatch: |
| paramnames.extend(get_param_names(pmatch.group(1))) |
| |
| # Check for a line that begins with the subcircuit name |
| |
| dmatch = devrex.match(line) |
| if dmatch: |
| fmtline, ispassed = parse_param_line(dmatch.group(3), True, insub, True, ispassed) |
| if fmtline != '': |
| inparam = True |
| calllines.append(subname + ' ' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + fmtline) |
| continue |
| else: |
| calllines.append(subname + ' ' + dmatch.group(1) + ' ' + dmatch.group(2) + ' ' + dmatch.group(3)) |
| continue |
| |
| if inmodel == 1 or inmodel == 2: |
| # This line should have the model bin, if there is one, and a type. |
| if inmodel == 1: |
| bmatch = binrex.match(savematch.group(3)) |
| savematch = None |
| else: |
| bmatch = binrex.match(line) |
| |
| if bmatch: |
| bin = bmatch.group(1) |
| type = bmatch.group(2) |
| |
| if type == 'n': |
| convtype = 'nmos' |
| elif type == 'p': |
| convtype = 'pmos' |
| else: |
| convtype = type |
| |
| # If there is a binned model then it replaces any original |
| # model line that was saved. |
| if modellines[-1].startswith('.model'): |
| modellines = modellines[0:-1] |
| modellines.append('') |
| modellines.append('.model ' + modname + '.' + bin + ' ' + convtype) |
| continue |
| |
| else: |
| fmtline, ispassed = parse_param_line(line, True, True, False, ispassed) |
| if fmtline != '': |
| modellines.append(fmtline) |
| continue |
| |
| # Copy line as-is |
| spicelines.append(line) |
| |
| # Make sure any pending model lines are taken care of |
| if modellines != []: |
| for line in modellines: |
| spicelines.append(line) |
| modellines = [] |
| inmodel = False |
| |
| output = '\n'.join(spicelines) |
| output = cleanup_spice_data(output) |
| if not output.strip(): |
| print("Skipping empty file:", outfile) |
| return |
| |
| # Output the result to out_file. |
| print("Writing", out_file) |
| assert not os.path.exists(out_file), out_file |
| with open(out_file, 'w') as ofile: |
| ofile.write(output) |
| |
| |
| RE_MULTI_NEWLINE = re.compile('\\n+') |
| RE_BSIM4 = re.compile('^NEW BSIM4 Parameters\\(([^)]*)\\)$', flags=re.I|re.M) |
| |
| RE_DUP_LINES = re.compile('^(?P<l>.+)$\\n*(?P=l)', flags=re.M) |
| |
| |
| def strip_comment(cmt): |
| """ |
| |
| >>> strip_comment([' // File created for mismatch simulations.']) |
| 'File created for mismatch simulations.' |
| |
| >>> strip_comment(['//', '// DC IV MOS PARAMETERS', '//']) |
| 'DC IV MOS PARAMETERS' |
| |
| >>> strip_comment(['// ****', '//', '// ROUT PARAMETERS', '//']) |
| 'ROUT PARAMETERS' |
| |
| >>> strip_comment([ |
| ... '** models for resistance and capacitances **', |
| ... '** this is lrhc corner **', |
| ... '*********************************************', |
| ... '**********************************************', |
| ... '** tolerances of different layers **', |
| ... '**********************************************', |
| ... ]) |
| 'models for resistance and capacitances\\n this is lrhc corner\\n tolerances of different layers' |
| |
| >>> strip_comment(['// **PARAMETERS FOR ASYMMETRIC AND BIAS-DEPENDENT RDS MODEL******']) |
| 'PARAMETERS FOR ASYMMETRIC AND BIAS-DEPENDENT RDS MODEL' |
| |
| >>> strip_comment(['// NEW BSIM4 Parameters(Model Selectors)']) |
| 'BSIM4 - Model Selectors' |
| |
| """ |
| if not cmt: |
| return '' |
| cmt = list(cmt) |
| |
| for i, c in enumerate(cmt): |
| cmt[i] = c.lstrip() |
| |
| # Remove the comment character |
| if cmt[0].startswith('*'): |
| p = '*' |
| elif cmt[0].startswith('//'): |
| p = '//' |
| elif cmt[0].startswith('$'): |
| p = '$' |
| else: |
| raise ValueError('Unknown starting comment: '+repr(cmt[0])) |
| |
| for i, c in enumerate(cmt): |
| assert c.startswith(p), repr((p, i, c))+'\n'+pprint.pformat(cmt, width=100) |
| if c.endswith(p): |
| cmt[i] = c[len(p):-len(p)] |
| else: |
| cmt[i] = c[len(p):] |
| |
| # Remove boxes around stuff |
| # This should also cover full lines of a single character like '*****' or '#####' |
| for i, c in enumerate(cmt): |
| if len(c) <= 2: |
| continue |
| c = c.strip() |
| while c and c[0] == c[-1] and not c[0].isalnum() and c[0] != ' ': |
| c = c[1:-1] |
| cmt[i] = c |
| |
| for i, c in enumerate(cmt): |
| cmt[i] = c.strip('*') |
| |
| # Find the longest prefix |
| if len(cmt) > 1: |
| p = os.path.commonprefix(cmt) |
| for i, c in enumerate(cmt): |
| cmt[i] = c[len(p):].rstrip() |
| |
| cmt = '\n'.join(cmt) |
| cmt = textwrap.dedent(cmt).strip() |
| |
| cmt = RE_MULTI_NEWLINE.sub('\n', cmt) |
| |
| # Fix a spelling mistake... |
| cmt = cmt.replace('PARAMTERS', 'PARAMETERS') |
| |
| # Strip out the BSIM4 parameters comment |
| cmt = RE_BSIM4.sub('BSIM4 - \\1', cmt) |
| |
| return cmt |
| |
| |
| |
| RE_BIN = re.compile('([^,]+),\\s+Bin\\s+([^,]+),(.*)') |
| |
| RE_PARAM = re.compile('[\w\s+.()/-]*((PARAMETERS)|(SELECTORS))[\w\s+.()/-]*', flags=re.I) |
| RE_PARAM_END = re.compile('PARAMETERS$', flags=re.I) |
| RE_FIX_CASE = re.compile('\\b([A-Z][A-Z][A-Z][A-Z][A-Z]*)\\b') |
| |
| RE_UNITS = re.compile('Units:\\w*(.*)', flags=re.I) |
| |
| # for L>=4 and W<=0.75 |
| RE_L_AND_W = re.compile('((L[\s<>]=)|(W[\s<>]=))') |
| |
| |
| # 'High Density 3-terminal Vertical Parallel Plate Capacitor (for S8P ONLY)\nFixed layout is here:\n$depot/icm/proj/s8rf2/dev/opus/s8rf2/s8rf2_xcmvpp_hd5_atlas_fingercap2_l5' |
| # '3-terminal Vertical Parallel Plate Capacitor /w LI-M5 Shield (for S8P ONLY)\nThis is the ~100fF fixed capacitor model\nFixed layout is here:\ndepot/icm/proj/s8rf2/dev/opus/s8rf2/s8rf2_xcmvpp11p5x11p7_m3_lim5shield/layout' |
| |
| RE_CAP = re.compile('(?P<desc1>.*)\\(for S8P ONLY\\)(?P<desc2>.*)Fixed layout is here:(?P<oldp>.*?)/(?P<p>[^/]*)((?:/layout)|$)', flags=re.DOTALL|re.I) |
| |
| UNIT_MAPPING = { |
| '1/c': '1/coulomb', |
| '1/c^2': '1/coulomb^2', |
| '1/oc': '1/celsius', |
| '1/volt': 'volt', |
| 'a': 'amper', |
| 'a/m': 'amper/meter', |
| 'a/m^2': 'amper/meter^2', |
| 'angstrom': 'angstrom', |
| 'c': 'coulomb', |
| 'ev': 'electron-volt', |
| 'ev/c': 'electron-volt/coulomb', |
| 'ev/k': 'electron-volt/kelvin', |
| 'farads/m': 'farad/meter', |
| 'farads/m^2': 'farad/meter^2', |
| 'f/m': 'farad/meter', |
| 'f/m^2': 'farad/meter^2', |
| 'm': 'meter', |
| 'meter': 'meter', |
| 'f/um': 'farad/micrometer', |
| 'f/um^2': 'farad/micrometer^2', |
| 'v': 'volt', |
| 'v/c': 'volt/coulomb', |
| # Long replacements |
| 'ohms (ohms/m^2 if area defined in netlist)': 'ohm (ohm/meter^2 if area defined)', |
| 'temerature in oc passed from eldo *.temp statements': 'celsius', |
| } |
| |
| |
| |
| |
| def process_special_comments(cmt): |
| """ |
| |
| >>> process_special_comments(strip_comment(['// NEW BSIM4 Parameters(Mobility Parameters)'])) |
| ('BSIM4 - Mobility Parameters', 'param', ['BSIM4 - Mobility']) |
| |
| >>> process_special_comments('nlowvt_rf_base_m4_b, Bin 008, W = 5.05, L = 0.25') |
| ('\\nnlowvt_rf_base_m4_b, Bin 008, W = 5.05, L = 0.25\\n------------------------------------------------', 'bin', [['nlowvt_rf_base_m4_b', 'Bin 008', 'W = 5.05', 'L = 0.25']]) |
| |
| >>> process_special_comments('Parameters TO Make Model Into Cadflow') |
| (False, None, []) |
| |
| >>> process_special_comments('DC IV MOS PARAMETERS') |
| ('DC IV MOS Parameters', 'param', ['DC IV MOS']) |
| |
| >>> process_special_comments('DC IV MOS parameters') |
| ('DC IV MOS parameters', 'param', ['DC IV MOS']) |
| |
| >>> process_special_comments('GATE INDUCED DRAIN LEAKAGE MODEL PARAMETERS') |
| ('Gate Induced Drain Leakage Model Parameters', 'param', ['Gate Induced Drain Leakage Model']) |
| |
| >>> process_special_comments('P+ Poly Preres Parameters') |
| ('P+ Poly Preres Parameters', 'param', ['P+ Poly Preres']) |
| |
| >>> process_special_comments('Units: F/um') |
| ('Units: farad/micrometer', 'units', ['farad/micrometer']) |
| >>> process_special_comments(' F/um ') |
| ('Units: farad/micrometer', 'units', ['farad/micrometer']) |
| |
| >>> process_special_comments('All devices') |
| ('All devices', None, []) |
| |
| >>> process_special_comments('Not used') |
| ('Not used', None, []) |
| |
| >>> process_special_comments('All W with L=0.5um') |
| ('All W with L=0.5um', None, []) |
| |
| >>> process_special_comments('for L>=4 and W<=0.75') |
| ('for L>=4 and W<=0.75', None, []) |
| |
| >>> process_special_comments('Default Corner File created by discrete_corners\\nNumber of BINS: 1') |
| ('Number of bins: 1', None, []) |
| |
| >>> process_special_comments('4-terminal Vertical Parallel Plate Capacitor /w LI-M4 fingers and M5 Shield (for S8P ONLY)\\nFixed layout is here:\\ndepot/icm/proj/s8rf2/dev/opus/s8rf2/s8rf2_xcmvpp11p5x11p7_m5shield') |
| ('4-terminal Vertical Parallel Plate Capacitor /w LI-M4 fingers and M5 Shield\\nLayout: s8rf2_xcmvpp11p5x11p7_m5shield', None, []) |
| |
| >>> process_special_comments("3-terminal Vertical Parallel Plate Capacitor /w LI-M5 Shield (for S8P ONLY)\\nThis is the ~100fF fixed capacitor model\\nFixed layout is here:\\ndepot/icm/proj/s8rf2/dev/opus/s8rf2/s8rf2_xcmvpp11p5x11p7_m3_lim5shield/layout") |
| ('3-terminal Vertical Parallel Plate Capacitor /w LI-M5 Shield\\nThis is the ~100fF fixed capacitor model\\nLayout: s8rf2_xcmvpp11p5x11p7_m3_lim5shield', None, []) |
| |
| >>> process_special_comments("Include files in ../../../s8x/ directory\\nP+ Poly Preres Corner Parameters") |
| ('P+ Poly Preres Corner Parameters', 'param', ['P+ Poly Preres Corner']) |
| |
| |
| >>> pprint.pprint(process_special_comments("nhvnativeesd w/l's to match MRGA and pm3 file\\nnhvnativeesd, Bin 011, W = 10.0, L = 2.0\\nnhvnativeesd, Bin 012, W = 10.0, L = 4.0\\nnhvnativeesd, Bin 013, W = 10.0, L = 0.9\\n")) |
| ('\\n' |
| "nhvnativeesd w/l's to match MRGA and pm3 file\\n" |
| 'nhvnativeesd, Bin 011, W = 10.0, L = 2.0\\n' |
| 'nhvnativeesd, Bin 012, W = 10.0, L = 4.0\\n' |
| 'nhvnativeesd, Bin 013, W = 10.0, L = 0.9\\n' |
| '---------------------------------------------', |
| 'bin', |
| [["nhvnativeesd w/l's to match MRGA and pm3 file"], |
| ['nhvnativeesd', 'Bin 011', 'W = 10.0', 'L = 2.0'], |
| ['nhvnativeesd', 'Bin 012', 'W = 10.0', 'L = 4.0'], |
| ['nhvnativeesd', 'Bin 013', 'W = 10.0', 'L = 0.9']]) |
| |
| |
| #>>> process_special_comments('DIODE DC IV PARAMTERS\\n\\nNEW BSIM4 Parameters(DIODE DC IV parameters)') |
| #('* Diode DC IV Parameters', 'param', 'Diode DC IV Parameters') |
| |
| Returns: |
| new - Comment value to put in output file, empty string if the comment is discarded. |
| str - comment type |
| [d] - comment data |
| """ |
| |
| if 'rights' in cmt.lower(): |
| return '', None, [] |
| |
| # Strip duplicate stuff |
| cmt = RE_DUP_LINES.sub(lambda m: m.group('l'), cmt) |
| |
| m = RE_BIN.match(cmt) |
| if m: |
| a = [] |
| b = [] |
| for l in cmt.splitlines(): |
| l = l.strip() |
| if not l: |
| continue |
| |
| if ',' not in l: |
| a.append(l) |
| b.append([l]) |
| else: |
| d = [v.strip() for v in l.split(',')] |
| a.append(', '.join(d)) |
| b.append(d) |
| return '\n'+'\n'.join(a)+'\n'+'-'*max(len(nc) for nc in a), 'bin', b |
| |
| m = RE_UNITS.match(cmt) |
| if m: |
| unit = m.group(1) |
| unit = unit.strip().lower() |
| |
| unit_new = UNIT_MAPPING.get(unit, None) |
| if not unit_new: |
| assert unit in UNIT_MAPPING.values(), (unit, unit_new) |
| unit_new = new |
| return 'Units: ' + unit_new, 'units', [unit_new] |
| |
| if 'L' in cmt and 'W' in cmt: |
| m = RE_L_AND_W.search(cmt) |
| if m: |
| return cmt, None, [] |
| |
| uc = cmt.strip().lower() |
| if uc in UNIT_MAPPING: |
| unit_new = UNIT_MAPPING[uc] |
| return 'Units: ' + unit_new, 'units', [unit_new] |
| |
| if '\n' not in uc: |
| if 'all' in uc: |
| return cmt, None, [] |
| |
| if 'not used' in uc: |
| return cmt, None, [] |
| |
| if 'number of bins:' in uc: |
| before, after = uc.split('number of bins:') |
| return 'Number of bins: '+after.strip(), None, [] |
| |
| if '(for s8p only)\n' in uc: |
| m = RE_CAP.match(' '.join(cmt.splitlines())) |
| assert m, cmt |
| new_cmt = m.group('desc1').strip() |
| assert new_cmt, (m, m.groups()) |
| if m.group('desc2').strip(): |
| new_cmt += '\n'+m.group('desc2').strip() |
| if m.group('p'): |
| new_cmt += '\nLayout: '+m.group('p') |
| return new_cmt, None, [] |
| |
| m = RE_PARAM.match(cmt) |
| if m and 'cadflow' not in cmt.lower(): |
| if '\n' in cmt: |
| cmt = '\n'.join(s for s in cmt.splitlines() if 'parameters' in s.lower()) |
| |
| cmt = RE_FIX_CASE.sub(lambda m: m.group(0).title(), cmt) |
| ocmt = cmt |
| if RE_PARAM_END.search(ocmt): |
| ocmt = ocmt[:-len(' Parameters')] |
| return cmt, 'param', [ocmt] |
| |
| |
| return False, None, [] |
| |
| |
| def convert_comments(idata): |
| # Replace all C style // comments with '*' or '$' |
| cmt_start = None |
| for i, l in enumerate(idata): |
| if '//' in l: |
| if l.strip().startswith('//'): |
| assert '//.' not in l, repr(l) |
| idata[i] = '*' + l.lstrip('/') |
| else: |
| before, after = l.split('//', 1) |
| idata[i] = before + '$' + after.lstrip('/') |
| |
| #assert '/*' not in l, idata |
| assert '*/' not in idata[i], idata[i]+'\n'+'\n'.join(idata) |
| |
| |
| def get_last_comment(comments): |
| i = len(comments) |
| while i > 0: |
| if comments[i-1].startswith('...'): |
| break |
| i -= 1 |
| return comments[i:] |
| |
| |
| EOF_MARKER = 'EOF\0' |
| |
| |
| def rewrite_comments(out_file, idata): |
| idata = list(idata.splitlines()) |
| idata.append(EOF_MARKER) |
| convert_comments(idata) |
| |
| speclines = ['* SKY130 Spice File.'] |
| |
| comments_raw = [] |
| comments_nonspecial = [] |
| |
| comments_special = defaultdict(list) |
| comments_special_debug = defaultdict(list) |
| |
| for l in idata: |
| if l.strip().startswith('*'): |
| if not l.startswith('*.'): |
| comments_raw.append(l) |
| continue |
| |
| if not l.strip(): |
| continue |
| |
| if comments_raw and not comments_raw[-1].startswith('...'): |
| cmt_last = get_last_comment(comments_raw) |
| comments_raw.append('...') |
| |
| cmt_clean = strip_comment(cmt_last) |
| |
| cmt_new, cmt_type, cmt_value = process_special_comments(cmt_clean) |
| if cmt_new: |
| cmt_new = textwrap.indent(cmt_new, '* ', lambda line: True) |
| print('Replacing comment:', repr('\n'.join(cmt_last)), 'with', repr(cmt_new)) |
| speclines.append(cmt_new) |
| |
| if cmt_type: |
| comments_special[cmt_type].extend(cmt_value) |
| comments_special_debug[cmt_type].append((cmt_last, cmt_clean, (cmt_new, cmt_type, cmt_value))) |
| elif not cmt_new and cmt_clean: |
| comments_nonspecial.append(cmt_clean) |
| |
| if '$' in l: |
| l, cmt = l.split('$', 1) |
| comments_raw.append('... $ ' + cmt) |
| |
| cmt_clean = cmt.strip() |
| |
| cmt_new, cmt_type, cmt_value = process_special_comments(cmt_clean) |
| if cmt_new: |
| l = "{}$ {}".format(l, cmt_new) |
| if cmt_type: |
| comments_special[cmt_type].append(cmt_value) |
| comments_special_debug[cmt_type].append((cmt, cmt_clean, (cmt_new, cmt_type, cmt_value))) |
| elif not cmt_new and cmt_clean: |
| comments_nonspecial.append('... $ ' + cmt_clean) |
| |
| speclines.append(l) |
| |
| assert speclines[-1] == EOF_MARKER, pprint.pformat(speclines, width=WIDTH) |
| speclines.pop(-1) |
| assert not comments_raw or comments_raw[-1].startswith('...'), pprint.pformat(comments_raw) |
| |
| if comments_nonspecial: |
| print('Non-special comments:') |
| print('-'*10) |
| pprint.pprint(comments_nonspecial, width=WIDTH) |
| print('-'*10) |
| cmt_file = out_file+'.stripped.comments' |
| print("Writing stripped comments to:", cmt_file) |
| assert not os.path.exists(cmt_file), cmt_file |
| with open(cmt_file, 'w') as f: |
| for c in comments_nonspecial: |
| f.write(c) |
| f.write('\n') |
| f.write('---\n') |
| |
| if comments_special: |
| comments_special = dict(comments_special) |
| print("Special comments:") |
| print('-'*10) |
| pprint.pprint(comments_special, width=WIDTH) |
| print('-'*10) |
| |
| if 'bin' in comments_special: |
| def clean(s, a='='): |
| s = s.split(a)[-1] |
| s = s.strip() |
| s = s.lstrip('0') |
| if '.' in s: |
| s = s.rstrip('0') |
| if len(s) == 0: |
| s = '0' |
| elif s[0] == '.': |
| s = '0'+s |
| elif s[-1] == '.': |
| s = s+'0' |
| return s |
| |
| bin_csv = out_file+'.bins.csv' |
| with open(bin_csv, 'w', newline='') as f: |
| c = csv.writer(f) |
| c.writerow(['device','bin','W','L']) |
| for r in comments_special['bin']: |
| assert len(r) in (1, 4), r |
| if len(r) == 1: |
| c.writerow(r) |
| else: |
| d, b, w, l = r |
| b = clean(b, ' ') |
| w = clean(w) |
| l = clean(l) |
| c.writerow([d, b, w, l]) |
| del comments_special['bin'] |
| |
| for k in comments_special: |
| sfile = out_file+'.'+k+'.json' |
| with open(sfile, 'w') as f: |
| json.dump(comments_special[k], f, sort_keys=True, indent=' ') |
| |
| if len(comments_raw) > 1: |
| cmt_file = out_file+'.raw.comments' |
| print("Writing comments to:", cmt_file) |
| assert not os.path.exists(cmt_file), cmt_file |
| with open(cmt_file, 'w') as f: |
| for c in comments_raw: |
| f.write(c) |
| f.write('\n') |
| |
| return speclines |
| |
| |
| |
| # * <comment> |
| RE_CMT_FULL_LINE = re.compile('\\n(\\*[^\\.]*[^\\n]*\\n)+') |
| |
| # XXXX $ <comment> |
| RE_CMT_END_LINE = re.compile('\\$.*?$', flags=re.MULTILINE) |
| |
| RE_LINE_PLUS_START = re.compile('^\\+([ \\t]*)', flags=re.MULTILINE) |
| RE_LINE_EQUALS = re.compile('[ \\t]+=[ \\t]+') |
| RE_CMT_INCLUDE_IG = re.compile('^\\*\\*Include files in.*\\n', flags=re.MULTILINE) |
| RE_EXTRA_CMT = re.compile('^(\\*\\*\\*\\n)+', flags=re.MULTILINE) |
| RE_BIG_CMT = re.compile('^\\*\\*\\*(.*)$', flags=re.MULTILINE) |
| RE_SMALL_CMT = re.compile('\\n(\\*[ \\t]*\\n)+', flags=re.MULTILINE) |
| RE_MULTI_NEWLINE = re.compile('\\n+') |
| RE_INCLUDE = re.compile('^\\.inc(lude)?[ \\t]+["\']?(?P<file>[^"\'\\n\\r]+)["\']?$', flags=re.MULTILINE|re.IGNORECASE) |
| RE_INCLUDE_CMT = re.compile('^\\*[ \\t]*\\.inc(lude)?[ \\t]+["\']?(?P<file>[^"\'\\n\\r]+)["\']?$', flags=re.MULTILINE|re.IGNORECASE) |
| RE_TRAILING_WS = re.compile('[ \\t]+$', flags=re.MULTILINE) |
| |
| RE_INT = re.compile('= [ \\t]*(-?[0-9]+)[ \\t]*$', flags=re.MULTILINE) |
| RE_FIX_SCIENTIFIC = re.compile('= [ \\t]*(-?)([0-9.]+)[eE]([+\\-]?)([0-9.]+)[ \\t]*$', flags=re.MULTILINE) |
| |
| RE_CPAR_REMOVE = re.compile("^.param ((cpar_area)|(cpar_perim)) = '[^']*'\n", flags=re.MULTILINE) |
| |
| |
| |
| def cleanup_comment(m): |
| return '*** ' + ' '.join(m.group(1).strip().split()) |
| |
| |
| def fix_scientific(m): |
| """ |
| |
| >>> RE_FIX_SCIENTIFIC.sub(fix_scientific, '= 0.0') |
| '= 0.0' |
| |
| >>> RE_FIX_SCIENTIFIC.sub(fix_scientific, '= 0e0') |
| '= 0.0e+0' |
| |
| >>> RE_FIX_SCIENTIFIC.sub(fix_scientific, '= -2.4222865e-005') |
| '= -2.4222865e-5' |
| |
| >>> RE_FIX_SCIENTIFIC.sub(fix_scientific, '= 1e+10') |
| '= 1.0e+10' |
| |
| >>> RE_FIX_SCIENTIFIC.sub(fix_scientific, '= 1e00010') |
| '= 1.0e+10' |
| """ |
| neg = m.group(1) |
| before = m.group(2) |
| sign = m.group(3) |
| after = m.group(4) |
| |
| while len(before) > 1 and before[0] == '0': |
| before = before[1:] |
| while len(after) > 1 and after[0] == '0': |
| after = after[1:] |
| |
| if not sign: |
| sign = '+' |
| |
| if '.' not in before: |
| before += '.0' |
| |
| return '= {}{}e{}{}'.format(neg, before, sign, after) |
| |
| |
| def cleanup_spice_data(data): |
| """ |
| |
| >>> cleanup_spice_data(''' |
| ... **Include files in ../../../s8x/ directory |
| ... .inc "../../s8x/Models/ss.cor" |
| ... ''') |
| '.include "../../s8x/Models/ss.cor"\\n' |
| |
| >>> cleanup_spice_data(''' |
| ... .inc "../../s8x/Models/sonos_ttteol.cor" |
| ... ''') |
| '.include "../../s8x/Models/sonos_ttteol.cor"\\n' |
| |
| >>> cleanup_spice_data(''' |
| ... .inc "models.a" |
| ... ''') |
| '.include "models.a"\\n' |
| |
| >>> cleanup_spice_data(''' |
| ... .include models.a |
| ... ''') |
| '.include "models.a"\\n' |
| |
| >>> cleanup_spice_data(''' |
| ... .include 'models.a' |
| ... ''') |
| '.include "models.a"\\n' |
| |
| >>> print(cleanup_spice_data(''' |
| ... * RF MOS PARAMETERS |
| ... .inc "nshort_rf_base_b_fs.cor" |
| ... .inc "nlowvt_rf_base_b_fs.cor" |
| ... ''').strip()) |
| * RF MOS PARAMETERS |
| .include "nshort_rf_base_b_fs.cor" |
| .include "nlowvt_rf_base_b_fs.cor" |
| |
| >>> print(cleanup_spice_data(''' |
| ... + dvt1w = 1147200.0 |
| ... + vbm = -3 |
| ... + rdswmin = 0 |
| ... + rdw = 1 |
| ... + rdwmin = 0 |
| ... + rsw = 0 |
| ... + rswmin = 0 |
| ... ''').strip()) |
| + dvt1w = 1147200.0 |
| + vbm = -3.0 |
| + rdswmin = 0.0 |
| + rdw = 1.0 |
| + rdwmin = 0.0 |
| + rsw = 0.0 |
| + rswmin = 0.0 |
| |
| >>> print(cleanup_spice_data(''' |
| ... + nsd = 1.0e+020 |
| ... + alpha1 = 1.0e-10 |
| ... + pdiblcb = -2.4222865e-005 |
| ... + alpha1 = 1e-010 |
| ... ''').strip()) |
| + nsd = 1.0e+20 |
| + alpha1 = 1.0e-10 |
| + pdiblcb = -2.4222865e-5 |
| + alpha1 = 1.0e-10 |
| |
| >>> print(cleanup_spice_data(''' |
| ... + nsd = 1.0e+020 |
| ... + lint = {1.1932e-008+nshort_lint_diff} |
| ... + alpha1 = 1e-010 |
| ... ''').strip()) |
| + nsd = 1.0e+20 |
| + lint = {1.1932e-008+nshort_lint_diff} |
| + alpha1 = 1.0e-10 |
| |
| >>> print(cleanup_spice_data(''' |
| ... .param vc1 = -25.0e-6 |
| ... .param vc2 = 90.0e-6 |
| ... .param carea = 'camimc*(wc)*(lc)' |
| ... .param cperim = 'cpmimc*((wc)+(lc))*2' |
| ... .param czero = 'carea + cperim' dev/gauss='0.01*2.8*(carea + cperim)/sqrt(wc*lc*mf)' |
| ... .param cpar_area = 'mcm4f_ca_w_7_680_s_1_240*(wc)*(lc)*m*1e-12' |
| ... .param cpar_perim = 'mcm4f_cf_w_7_680_s_1_240*((wc)+(lc))*m*2*1e-6' |
| ... c1 c0 a 'czero*(1+vc1*(v(c0)-v(c1))+vc2*(v(c0)-v(c1))*(v(c0)-v(c1)))' tc1=0 tc2=0 |
| ... rs1 a b1 'r1' tc1=tc1rm4 tc2=tc2rm4 |
| ... rs2 b1 c1 'r2' tc1=tc1rvia4 tc2=tc2rvia4 |
| ... .ends sky130_fd_pr__cap_mim_m3_2 |
| ... ''').strip()) |
| .param vc1 = -25.0e-6 |
| .param vc2 = 90.0e-6 |
| .param carea = 'camimc*(wc)*(lc)' |
| .param cperim = 'cpmimc*((wc)+(lc))*2' |
| .param czero = 'carea + cperim' dev/gauss='0.01*2.8*(carea + cperim)/sqrt(wc*lc*mf)' |
| c1 c0 a 'czero*(1+vc1*(v(c0)-v(c1))+vc2*(v(c0)-v(c1))*(v(c0)-v(c1)))' tc1=0 tc2=0 |
| rs1 a b1 'r1' tc1=tc1rm4 tc2=tc2rm4 |
| rs2 b1 c1 'r2' tc1=tc1rvia4 tc2=tc2rvia4 |
| .ends sky130_fd_pr__cap_mim_m3_2 |
| |
| |
| """ |
| |
| # "../../s8x/Models/ss.cor" |
| |
| data = RE_TRAILING_WS.sub('', data) |
| |
| data = RE_LINE_PLUS_START.sub('+ ', data) |
| data = RE_LINE_EQUALS.sub(' = ', data) |
| data = RE_CMT_INCLUDE_IG.sub('', data) |
| data = RE_EXTRA_CMT.sub('', data) |
| data = RE_BIG_CMT.sub(cleanup_comment, data) |
| data = RE_MULTI_NEWLINE.sub('\n', data) |
| data = RE_INCLUDE.sub('.include "\\g<file>"', data) |
| data = RE_INCLUDE_CMT.sub('*.include "\\g<file>"', data) |
| |
| data = RE_INT.sub('= \\1.0', data) |
| data = RE_FIX_SCIENTIFIC.sub(fix_scientific, data) |
| data = RE_CPAR_REMOVE.sub('', data) |
| |
| iinc = data.find('.inc ') |
| assert iinc == -1, (iinc, data[iinc-100:iinc+100]) |
| |
| data = data.strip() |
| if data and data[-1] != '\n': |
| data += '\n' |
| |
| return data |
| |
| |
| if __name__ == '__main__': |
| import doctest |
| fails, _ = doctest.testmod() |
| if fails != 0: |
| sys.exit("Some test failed") |
| |
| if len(sys.argv) == 1: |
| print("No options given to spectre_to_spice.py.") |
| usage() |
| sys.exit(0) |
| |
| optionlist = [] |
| arguments = [] |
| |
| for option in sys.argv[1:]: |
| if option.find('-', 0) == 0: |
| optionlist.append(option) |
| else: |
| arguments.append(option) |
| |
| if len(arguments) != 2: |
| print("Wrong number of arguments given to convert_spectre.py.") |
| usage() |
| sys.exit(0) |
| |
| debug = '-debug' in optionlist |
| nocmt = '-nocmt' in optionlist |
| try: |
| ignore = next(item for item in optionlist if item.split('=')[0] == '-ignore').split('=')[1].split(',') |
| except: |
| ignore = '' |
| |
| specpath = os.path.abspath(arguments[0]) |
| spicepath = os.path.abspath(arguments[1]) |
| do_one_file = False |
| |
| if not os.path.exists(specpath): |
| print('No such source directory ' + specpath) |
| sys.exit(1) |
| |
| if os.path.isfile(specpath): |
| do_one_file = True |
| |
| if do_one_file: |
| if arguments[1].endswith('/'): |
| if not os.path.exists(spicepath): |
| print("Creating:", spicepath) |
| os.makedirs(spicepath) |
| else: |
| assert os.path.isdir(spicepath), "Not directory? " + spicepath |
| |
| c = os.path.commonprefix([spicepath, specpath]) |
| spicepath = os.path.join(spicepath, specpath[len(c):].replace('/', '_')) |
| |
| if os.path.exists(spicepath): |
| print('Error: File ' + spicepath + ' exists.') |
| sys.exit(1) |
| |
| convert_file(specpath, spicepath, nocmt, ignore, debug) |
| |
| else: |
| if not os.path.exists(spicepath): |
| os.makedirs(spicepath) |
| |
| specfilelist = glob.glob(specpath + '/*') |
| |
| for filename in specfilelist: |
| if filename.endswith('readme'): |
| continue |
| if filename.endswith('.tmp'): |
| continue |
| if filename.endswith('.comments'): |
| continue |
| fileext = os.path.splitext(filename)[1] |
| |
| # Ignore verilog or verilog-A files that might be in a model directory |
| if fileext == '.v' or fileext == '.va': |
| continue |
| |
| # .scs files are purely spectre and meaningless to SPICE, so ignore them. |
| if fileext == '.scs': |
| continue |
| |
| froot = os.path.split(filename)[1] |
| print() |
| convert_file(filename, spicepath + '/' + froot, nocmt, ignore, debug) |
| print() |
| |
| exit(0) |