diff --git a/Changelog.md b/Changelog.md index 1c9dcf5..3780e09 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,15 @@ +# 2.4.1 + +## CLI + +- After successful execution of CACE, the documentation is generated under the specified path "documentation" + - Generate Markdown summary of the design + - Generate Markdown summary of the results + - Export the symbol as SVG + - Export the schematic as SVG + - Export the layout as PNG +- Added `--nofail` argument + # 2.4.0 ## Common diff --git a/cace/__version__.py b/cace/__version__.py index 4252a4f..deaefd2 100644 --- a/cace/__version__.py +++ b/cace/__version__.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '2.4.0' +__version__ = '2.4.1' if __name__ == '__main__': print(__version__, end='') diff --git a/cace/cace_cli.py b/cace/cace_cli.py index bc3961f..126d186 100755 --- a/cace/cace_cli.py +++ b/cace/cace_cli.py @@ -180,9 +180,9 @@ def cli(): help='do not display the progress bar', ) parser.add_argument( - '--save', - type=str, - help='directory to save the summary file to after successful completion', + '--nofail', + action='store_true', + help='do not fail on any errors or failing parameters', ) # Parse arguments @@ -351,17 +351,17 @@ def cli(): elif result['type'] == ResultType.CANCELED: returncode = 4 - # Upon successful completion save the summary file - if returncode == 0 and args.save: - if not os.path.isdir(args.save): - err('Save argument is not a directory!') - returncode = 1 - else: - with open(os.path.join(args.save, 'summary.md'), 'w') as ofile: - ofile.write(summary) + # Create the documentation + if returncode == 0 or args.nofail: + parameter_manager.generate_documentation() + else: + info(f'CACE failed, skipping documentation generation.') # Exit with final status - sys.exit(returncode) + if args.nofail: + sys.exit(0) + else: + sys.exit(returncode) if __name__ == '__main__': diff --git a/cace/common/cace_regenerate.py b/cace/common/cace_regenerate.py index a2c5227..4c3cb15 100755 --- a/cace/common/cace_regenerate.py +++ b/cace/common/cace_regenerate.py @@ -345,7 +345,7 @@ def regenerate_rcx_netlist(datasheet, runtime_options): if not os.path.exists(rcx_netlist_path): os.makedirs(rcx_netlist_path) - rcfile = get_magic_rcfile(datasheet, magicfilename) + rcfile = get_magic_rcfile() newenv = os.environ.copy() if 'PDK_ROOT' in datasheet: @@ -551,8 +551,30 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): info('Extracting LVS netlist from layout…') + # Assemble stdin for magic + magic_input = '' + if magicfilename and os.path.isfile(magicfilename): + magic_input += f'load {magicfilename}\n' + else: + magic_input += f'gds read {gdsfilename}\n' + magic_input += f'load {dname}\n' + # Use readspice to get the port order + magic_input += f'readspice {schem_netlist}\n' + + # magic_input += 'select top cell\n' + magic_input += f'select {dname}\n' # TODO? + magic_input += 'expand\n' + magic_input += 'extract path cace_extfiles\n' + magic_input += 'extract all\n' + magic_input += 'ext2spice lvs\n' + if pex == True: + magic_input += 'ext2spice cthresh 0.01\n' + magic_input += f'ext2spice -p cace_extfiles -o {lvs_netlist}\n' + magic_input += 'quit -noprompt\n' + magicargs = ['magic', '-dnull', '-noconsole', '-rcfile', rcfile] dbg('Executing: ' + ' '.join(magicargs)) + dbg(f'magic stdin:\n{magic_input}') mproc = subprocess.Popen( magicargs, @@ -561,39 +583,19 @@ def regenerate_lvs_netlist(datasheet, runtime_options, pex=False): stderr=subprocess.STDOUT, cwd=root_path, env=newenv, - universal_newlines=True, - ) - if magicfilename and os.path.isfile(magicfilename): - mproc.stdin.write('load ' + magicfilename + '\n') - else: - mproc.stdin.write('gds read ' + gdsfilename + '\n') - mproc.stdin.write('load ' + dname + '\n') - # Use readspice to get the port order - mproc.stdin.write('readspice ' + schem_netlist + '\n') - mproc.stdin.write('select top cell\n') - mproc.stdin.write('expand\n') - mproc.stdin.write('extract path cace_extfiles\n') - mproc.stdin.write('extract all\n') - mproc.stdin.write('ext2spice lvs\n') - if pex == True: - mproc.stdin.write('ext2spice cthresh 0.01\n') - mproc.stdin.write( - 'ext2spice -p cace_extfiles -o ' + lvs_netlist + '\n' + text=True, ) - mproc.stdin.write('quit -noprompt\n') + + mproc.stdin.write(magic_input) magout, magerr = mproc.communicate() - dbg(magout) - dbg(magerr) + dbg(f'magic stdout:\n{magout}') + dbg(f'magic stderr:\n{magerr}') printwarn(magout) if mproc.returncode != 0: - err( - 'Magic process returned error code ' - + str(mproc.returncode) - + '\n' - ) + err(f'Magic process returned error code {mproc.returncode}\n') if need_lvs_extract and not os.path.isfile(lvs_netlist): err('No LVS netlist extracted from magic.') @@ -882,7 +884,6 @@ def regenerate_schematic_netlist(datasheet, runtime_options): err( 'Subcircuit ' + mmatch.group(1) + ' was not found!' ) - os.remove(schem_netlist) if need_schem_capture: if not os.path.isfile(schem_netlist): diff --git a/cace/common/cace_write.py b/cace/common/cace_write.py index 542a654..33cd6bb 100755 --- a/cace/common/cace_write.py +++ b/cace/common/cace_write.py @@ -21,6 +21,11 @@ import subprocess from .cace_regenerate import printwarn, get_pdk_root +from .common import ( + xschem_generate_svg, + magic_generate_svg, + klayout_generate_png, +) from .spiceunits import spice_unit_convert, spice_unit_unconvert from ..parameter.parameter import ResultType from ..logging import ( @@ -35,846 +40,216 @@ ) -def generate_svg(datasheet, runtime_options): +def generate_documentation(datasheet): """ - Generate an SVG drawing of the schematic symbol using xschem + Generate documentation - Return the name of the SVG file if the drawing was generated, - None if not. + Generates a Markdown file for the design under the documentation path + Another Markdown file is generated for the results based on the netlist source + Exports the schematic and symbol as svg, and saves the layout as png """ - paths = datasheet['paths'] - if 'documentation' in paths: - docdir = paths['documentation'] - else: - docdir = '.' - - if 'schematic' in paths: - schempath = paths['schematic'] - symname = datasheet['name'] + '.sym' - sympath = os.path.join(schempath, symname) - svgname = datasheet['name'] + '_sym.svg' - svgpath = os.path.join(docdir, svgname) - if os.path.isfile(sympath): - - if 'PDK_ROOT' in datasheet: - pdk_root = datasheet['PDK_ROOT'] - else: - pdk_root = get_pdk_root() - - if 'PDK' in datasheet: - pdk = datasheet['PDK'] - else: - pdk = get_pdk(None) - - newenv = os.environ.copy() - if pdk_root and 'PDK_ROOT' not in newenv: - newenv['PDK_ROOT'] = pdk_root - if pdk and 'PDK' not in newenv: - newenv['PDK'] = pdk - - xschemargs = [ - 'xschem', - '-b', - '-x', - '-q', - '--svg', - '--plotfile', - svgpath, - ] - - # Use the PDK xschemrc file for xschem startup - xschemrcfile = os.path.join( - pdk_root, pdk, 'libs.tech', 'xschem', 'xschemrc' - ) - if os.path.isfile(xschemrcfile): - xschemargs.extend(['--rcfile', xschemrcfile]) - - xschemargs.append(sympath) - - info('Generating SVG of schematic symbol.') - dbg('Running: ' + ' '.join(xschemargs)) - - xproc = subprocess.Popen( - xschemargs, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - xout = xproc.communicate()[0] - if xproc.returncode != 0: - for line in xout.splitlines(): - print(line.decode('utf-8')) - - print( - 'Xschem process returned error code ' - + str(xproc.returncode) - + '\n' - ) - else: - printwarn(xout) - return svgname - - return None - - -# --------------------------------------------------------------- -# cace_generate_html -# -# Convert the characterization data into a formatted datasheet -# in HTML format -# -# Filename is set to .html and placed in the -# documents directory -# -# If filename is None, then filename is automatically generated -# from the project name with extension ".html" and placed in -# the documentation directory specified in 'paths'. -# --------------------------------------------------------------- - -def cace_generate_html(datasheet, filename=None): - - paths = datasheet['paths'] - if 'root' in paths: - rootdir = paths['root'] - else: - rootdir = '.' - - if 'documentation' in paths: - docdir = paths['documentation'] - else: - docdir = rootdir - - docname = datasheet['name'] + '.html' - - if not os.path.isdir(docdir): - os.makedirs(docdir) - - if filename: - ofilename = filename - if os.path.splitext(ofilename)[1] == '': - ofilename += '.html' - else: - ofilename = os.path.join(docdir, docname) - - svgname = generate_svg(datasheet) - - with open(ofilename, 'w') as ofile: - ofile.write('\n') - ofile.write('\n') - - if 'cace_format' in datasheet: - vformat = datasheet['cace_format'] - else: - vformat = '' - ofile.write(' '.join(['

CACE', vformat, 'datasheet

\n'])) + doc_file = os.path.join( + datasheet['paths']['root'], + datasheet['paths']['documentation'], + f'{datasheet["name"]}.md', + ) - ofile.write('\n\n
\n\n') - ofile.write('' + datasheet['name'] + '\n') - ofile.write('\n\n
\n\n') + with open(doc_file, 'w') as ofile: + ofile.write(f'# {datasheet["name"]}\n\n') if 'description' in datasheet: - ofile.write( - ' '.join(['', datasheet['description'], '

\n']) - ) + ofile.write(f'- Description: {datasheet["description"]}\n') - # Output PDK and designer information as a table + if 'commit' in datasheet: + ofile.write(f'- Commit: {datasheet["commit"]}\n') if 'PDK' in datasheet: - ofile.write( - '\n') - ofile.write('\n') - ofile.write('\n') - ofile.write('\n') - ofile.write('\n') - ofile.write('
PDK:\n') - ofile.write(' ' + datasheet['PDK'] + '\n') - ofile.write('
\n') - ofile.write('
\n\n') - - # Output authorship information + ofile.write(f'- PDK: {datasheet["PDK"]}\n') if 'authorship' in datasheet: - authdict = datasheet['authorship'] - ofile.write( - '\n') - ofile.write('\n') - - known_fields = [ - 'designer', - 'company', - 'institution', - 'email', - 'creation_date', - 'modification_date', - 'license', - ] - - if 'designer' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'company' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'institution' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'email' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'creation_date' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'modification_date' in authdict: - ofile.write('\n') - ofile.write('\n') - - if 'license' in authdict: - ofile.write('\n') - ofile.write('\n') - - for key in authdict.keys(): - if key not in known_fields: - ofile.write('\n') - ofile.write('\n') - - ofile.write('\n') - ofile.write('
Designer:\n') - ofile.write(' ' + authdict['designer'] + '\n') - ofile.write('
Company:\n') - ofile.write(' ' + authdict['company'] + '\n') - ofile.write('
Institution:\n') - ofile.write(' ' + authdict['institution'] + '\n') - ofile.write('
Contact:\n') - ofile.write(' ' + authdict['email'] + '\n') - ofile.write('
Created:\n') - ofile.write(' ' + authdict['creation_date'] + '\n') - ofile.write('
Last modified:\n') - ofile.write(' ' + authdict['modification_date'] + '\n') - ofile.write('
License:\n') - ofile.write(' ' + authdict['license'] + '\n') - ofile.write('
' + key + ':\n') - ofile.write(' ' + authdict[key] + '\n') - ofile.write('
\n') - - if 'dependencies' in datasheet: - ofile.write('

Project dependencies

\n') - dictlist = datasheet['dependencies'] - if isinstance(dictlist, dict): - dictlist = [datasheet['dependencies']] - - numdepend = 0 - if len(dictlist) == 0 or len(dictlist) == 1 and dictlist[0] == {}: - ofile.write('
\n') - ofile.write( - ' (' - + datasheet['name'] - + ' has no external dependencies.)\n' - ) - ofile.write('
\n') - else: - ofile.write('\n\n\n') + ofile.write(f'\n## Authorship\n\n') + + known_fields = { + 'designer': 'Designer', + 'company': 'Company', + 'institution': 'Institution', + 'email': 'Contact', + 'creation_date': 'Created', + 'modification_date': 'Last modified', + 'license': 'License', + } + + for entry in datasheet['authorship']: + if entry in known_fields: + ofile.write( + f'- {known_fields[entry]}: {datasheet["authorship"][entry]}\n' + ) + else: + warn(f'Unknown entry in authorship: {entry}') if 'pins' in datasheet: - ofile.write('

Pin names and descriptions

\n') - if svgname: - ofile.write('\n
\n
\n') - ofile.write(' \n') - ofile.write( - '
\n Project schematic symbol\n' - ) - ofile.write('
\n') - ofile.write('
\n
\n\n') - - ofile.write( - '\n') - ofile.write('\n\n') - ofile.write('\n\n\n') - for pin in datasheet['pins']: - ofile.write('\n') - if 'display' in pin: - pinname = pin['display'] - else: - pinname = pin['name'] - ofile.write('\n') + known_fields = { + 'display': 'Display', + 'description': 'Description', + 'type': 'Type', + 'direction': 'Direction', + 'Vmin': 'Vmin', + 'Vmax': 'Vmax', + 'note': 'Note', + } - ofile.write('\n') - ofile.write('
pin name') - ofile.write(' description') - ofile.write(' type') - ofile.write(' direction') - ofile.write(' Vmin') - ofile.write(' Vmax') - ofile.write(' notes') - ofile.write('
\n
' + pinname + '\n') - if 'description' in pin: - pindesc = pin['description'] - else: - pindesc = '' - ofile.write(' ' + pindesc + '\n') - if 'type' in pin: - pintype = pin['type'] - else: - pintype = '' - ofile.write(' ' + pintype + '\n') + ofile.write(f'\n## Pins\n\n') - if 'direction' in pin: - pindir = pin['direction'] - else: - pindir = '' - ofile.write(' ' + pindir + '\n') - if 'Vmin' in pin: - pinvmin = pin['Vmin'] - else: - pinvmin = '' - if isinstance(pinvmin, list): - ofile.write(' ' + ' '.join(pinvmin) + '\n') - else: - ofile.write(' ' + pinvmin + '\n') - if 'Vmax' in pin: - pinvmax = pin['Vmax'] - else: - pinvmax = '' - if isinstance(pinvmax, list): - ofile.write(' ' + ' '.join(pinvmax) + '\n') - else: - ofile.write(' ' + pinvmax + '\n') - if 'note' in pin: - pinnote = pin['note'] - else: - pinnote = '' - ofile.write(' ' + pinnote + '\n') - ofile.write('
\n') + for pin in datasheet['pins']: + ofile.write(f'- {pin}\n') - if 'default_conditions' in datasheet: - ofile.write('

Default conditions

\n') - ofile.write( - '\n') - ofile.write('\n\n') - ofile.write('\n\n\n') - - for cond in datasheet['default_conditions']: - ofile.write('\n') - if 'display' in cond: - condname = cond['display'] - else: - condname = cond['name'] - ofile.write('\n') - - ofile.write('\n') - ofile.write('
name') - ofile.write(' description') - ofile.write(' unit') - ofile.write(' minimum') - ofile.write(' typical') - ofile.write(' maximum') - ofile.write(' notes') - ofile.write('
\n
' + condname + '\n') - if 'description' in cond: - conddesc = cond['description'] - else: - conddesc = '' - ofile.write(' ' + conddesc + '\n') - if 'unit' in cond: - condunit = cond['unit'] - else: - condunit = '' - ofile.write(' ' + condunit + '\n') - if 'minimum' in cond: - condmin = cond['minimum'] - else: - condmin = '' - ofile.write(' ' + condmin + '\n') - if 'typical' in cond: - condtyp = cond['typical'] - else: - condtyp = '' - ofile.write(' ' + condtyp + '\n') - if 'maximum' in cond: - condmax = cond['maximum'] - else: - condmax = '' - ofile.write(' ' + condmax + '\n') - if 'note' in cond: - condnote = cond['note'] - else: - condnote = '' - ofile.write(' ' + condnote + '\n') - ofile.write('
\n') - - hasplots = False - netlist_source = 'spec' - - if 'electrical_parameters' in datasheet: - ofile.write('

Electrical parameters

\n') - ofile.write( - '\n') - ofile.write('\n\n') - ofile.write('\n\n\n') - - for param in datasheet['electrical_parameters']: - if 'spec' not in param and 'plot' in param: - hasplots = True - continue - ofile.write('\n') - if 'display' in param: - paramname = param['display'] - else: - paramname = param['name'] - ofile.write('\n') - - ofile.write('\n') - ofile.write('
name') - ofile.write(' description') - ofile.write(' unit') - ofile.write(' minimum') - ofile.write(' typical') - ofile.write(' maximum') - ofile.write(' notes') - ofile.write('
\n
' + paramname + '\n') - if 'description' in param: - paramdesc = param['description'] - else: - paramdesc = '' - ofile.write(' ' + paramdesc + '\n') - if 'unit' in param: - paramunit = param['unit'] - else: - paramunit = '' - ofile.write(' ' + paramunit + '\n') - - # NOTE: To do: Split between specification and result - # Handle netlist source for result. - - netlist_source = 'spec' - netlist_source_text = 'Specification' - if 'results' in param: - results = param['results'] - if isinstance(results, list): - for result in results: - if result['name'] == 'rcx': - netlist_source = 'rcx' - netlist_source_text = 'Parasitic R,C-extracted' - break - elif result['name'] == 'pex': - netlist_source = 'pex' - netlist_source_text = 'Parasitic C-extracted' - break - elif result['name'] == 'layout': - netlist_source = 'layout' - netlist_source_text = 'Layout extracted' - break - elif result['name'] == 'schematic': - netlist_source = 'schematic' - netlist_source_text = 'Schematic captured' - break + for entry in datasheet['pins'][pin]: + if entry in known_fields: + ofile.write( + f' + {known_fields[entry]}: {datasheet["pins"][pin][entry]}\n' + ) else: - result = results + warn(f'Unknown entry in pins: {entry}') - if 'minimum' in result: - resultmin = result['minimum'] - else: - resultmin = '' - - if isinstance(resultmin, list): - if len(resultmin) > 1 and resultmin[1] == 'fail': - ofile.write( - ' ' - + resultmin[0] - + '\n' - ) - else: - ofile.write(' ' + resultmin[0] + '\n') - else: - ofile.write(' ' + resultmin + '\n') - if 'typical' in result: - resulttyp = result['typical'] - else: - resulttyp = '' - if isinstance(resulttyp, list): - if len(resulttyp) > 1 and resulttyp[1] == 'fail': - ofile.write( - ' ' - + resulttyp[0] - + '\n' - ) - else: - ofile.write(' ' + resulttyp[0] + '\n') - else: - ofile.write(' ' + resulttyp + '\n') - if 'maximum' in result: - resultmax = result['maximum'] - else: - resultmax = '' - if isinstance(resultmax, list): - if len(resultmax) > 1 and resultmax[1] == 'fail': - ofile.write( - ' ' - + resultmax[0] - + '\n' - ) - else: - ofile.write(' ' + resultmax[0] + '\n') - else: - ofile.write(' ' + resultmax + '\n') + if 'default_conditions' in datasheet: - elif 'spec' in param: - spec = param['spec'] - if 'minimum' in spec: - specmin = spec['minimum'] - else: - specmin = '' - if isinstance(specmin, list): - spectext = specmin[0] - else: - spectext = specmin - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') - if 'typical' in spec: - spectyp = spec['typical'] - else: - spectyp = '' - if isinstance(spectyp, list): - spectext = spectyp[0] - else: - spectext = spectyp - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') - if 'maximum' in spec: - specmax = spec['maximum'] - else: - specmax = '' - if isinstance(specmax, list): - spectext = specmax[0] + ofile.write(f'\n## Default Conditions\n\n') + + known_fields = { + 'display': 'Display', + 'description': 'Description', + 'unit': 'Unit', + 'direction': 'Direction', + 'minimum': 'Minimum', + 'typical': 'Typical', + 'maximum': 'Maximum', + 'enumerate': 'Enumerate', + 'step': 'Step', + 'stepsize': 'Stepsize', + 'note': 'Note', + } + + for default_condition in datasheet['default_conditions']: + ofile.write(f'- {default_condition}\n') + + for entry in datasheet['default_conditions'][ + default_condition + ]: + if entry in known_fields: + ofile.write( + f' + {known_fields[entry]}: {datasheet["default_conditions"][default_condition][entry]}\n' + ) else: - spectext = specmax - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') + warn(f'Unknown entry in default_conditions: {entry}') - if 'note' in param: - paramnote = param['note'] - else: - paramnote = '' - ofile.write(' ' + paramnote + '\n') - ofile.write('
\n') - - ofile.write( - '
Note: Values taken from ' - + netlist_source_text - + '
\n' - ) - - if 'physical_parameters' in datasheet: - ofile.write('

Physical parameters

\n') - ofile.write( - '\n') - ofile.write('\n\n') - ofile.write('\n\n\n') - - for param in datasheet['physical_parameters']: - ofile.write('\n') - if 'display' in param: - paramname = param['display'] - else: - paramname = param['name'] - ofile.write('\n') - - ofile.write('\n') - ofile.write('
name') - ofile.write(' description') - ofile.write(' unit') - ofile.write(' minimum') - ofile.write(' typical') - ofile.write(' maximum') - ofile.write(' notes') - ofile.write('
\n
' + paramname + '\n') - if 'description' in param: - paramdesc = param['description'] - else: - paramdesc = '' - ofile.write(' ' + paramdesc + '\n') - if 'unit' in param: - paramunit = param['unit'] - else: - paramunit = '' - ofile.write(' ' + paramunit + '\n') - - # NOTE: To do: Split between specification and result - # Handle netlist source for result. - - netlist_source = 'spec' - netlist_source_text = 'Specification' - if 'results' in param: - results = param['results'] - if isinstance(results, list): - for result in results: - if result['name'] == 'rcx': - netlist_source = 'rcx' - netlist_source_text = 'Parasitic R,C-extracted' - break - elif result['name'] == 'pex': - netlist_source = 'pex' - netlist_source_text = 'Parasitic R,C-extracted' - netlist_source_text = 'Parasitic C-extracted' - break - elif result['name'] == 'layout': - netlist_source = 'layout' - netlist_source_text = 'Layout extracted' - break - elif result['name'] == 'schematic': - netlist_source = 'schematic' - netlist_source_text = 'Schematic captured' - break - else: - result = results + # Add symbol image + ofile.write(f'\n## Symbol\n\n') + ofile.write( + f'![Symbol of {datasheet["name"]}]({datasheet["name"]}_symbol.svg)\n' + ) - if 'minimum' in result: - resultmin = result['minimum'] - else: - resultmin = '' - - if isinstance(resultmin, list): - if len(resultmin) > 1 and resultmin[1] == 'fail': - ofile.write( - ' ' - + resultmin[0] - + '\n' - ) - else: - ofile.write(' ' + resultmin[0] + '\n') - else: - ofile.write(' ' + resultmin + '\n') - if 'typical' in result: - resulttyp = result['typical'] - else: - resulttyp = '' - if isinstance(resulttyp, list): - if len(resulttyp) > 1 and resulttyp[1] == 'fail': - ofile.write( - ' ' - + resulttyp[0] - + '\n' - ) - else: - ofile.write(' ' + resulttyp[0] + '\n') - else: - ofile.write(' ' + resulttyp + '\n') - if 'maximum' in result: - resultmax = result['maximum'] - else: - resultmax = '' - if isinstance(resultmax, list): - if len(resultmax) > 1 and resultmax[1] == 'fail': - ofile.write( - ' ' - + resultmax[0] - + '\n' - ) - else: - ofile.write(' ' + resultmax[0] + '\n') - else: - ofile.write(' ' + resultmax + '\n') + # Add schematic image + ofile.write(f'\n## Schematic\n\n') + ofile.write( + f'![Schematic of {datasheet["name"]}]({datasheet["name"]}_schematic.svg)\n' + ) - elif 'spec' in param: - spec = param['spec'] - if 'minimum' in spec: - specmin = spec['minimum'] - else: - specmin = '' - if isinstance(specmin, list): - spectext = specmin[0] - else: - spectext = specmin - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') - if 'typical' in spec: - spectyp = spec['typical'] - else: - spectyp = '' - if isinstance(spectyp, list): - spectext = spectyp[0] - else: - spectext = spectyp - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') - if 'maximum' in spec: - specmax = spec['maximum'] - else: - specmax = '' - if isinstance(specmax, list): - spectext = specmax[0] - else: - spectext = specmax - if spectext == 'any': - spectext = ' ' - ofile.write(' ' + spectext + '\n') + # Add layout images + ofile.write(f'\n## Layout\n\n') + ofile.write( + f'![Layout of {datasheet["name"]} with white background]({datasheet["name"]}_w.png)\n' + ) + ofile.write( + f'![Layout of {datasheet["name"]} with black background]({datasheet["name"]}_b.png)\n' + ) - if 'note' in param: - paramnote = param['note'] - else: - paramnote = '' - ofile.write(' ' + paramnote + '\n') - ofile.write('
\n') - - ofile.write( - '
Note: Values taken from ' - + netlist_source_text - + '
\n' - ) - - ofile.write('\n') - ofile.write('\n') - - # Plots to do: Group plots in pairs and add two plots per row. - # Plots really need to be embedded in the document, not referenced to - # the local file system. - - if hasplots: - for param in datasheet['electrical_parameters']: - if 'spec' not in param and 'plot' in param: - plotrec = param['plot'] - if 'filename' in plotrec: - plotfile = plotrec['filename'] - if os.path.splitext(plotfile)[1] == '': - plotfile += '.png' - if 'plots' in paths: - plotpath = paths['plots'] - elif 'simulation' in paths: - plotpath = paths['simulation'] - else: - plotpath = '' - - plottypes = [ - 'rcx', - 'pex', - 'layout', - 'schematic', - 'none', - ] - for plotsubdir in plottypes: - plotfilepath = os.path.join( - plotpath, plotsubdir, plotfile - ) - absfilepath = os.path.abspath(plotfilepath) - if not filename: - fullfilepath = os.path.join('..', plotfilepath) - else: - fullfilepath = absfilepath - if os.path.isfile(absfilepath): - break - - if plotsubdir == 'none': - plotfilepath = os.path.join(plotpath, plotfile) - absfilepath = os.path.abspath(plotfilepath) - if not filename: - fullfilepath = os.path.join('..', plotfilepath) - else: - fullfilepath = absfilepath - - if os.path.isfile(absfilepath): - ofile.write('\n
\n') - ofile.write('
\n') - ofile.write( - ' \n' - ) - if 'description' in param: - ofile.write('
\n') - ofile.write( - ' ' - + param['description'] - + '\n' - ) - if plotsubdir != 'none': - ofile.write('
\n') - ofile.write( - ' (Values taken from ' - + plotsubdir - + ' extraction)\n' - ) - ofile.write('
\n') - ofile.write('
\n\n') - else: - print('Warning: Cannot find plot ' + absfilepath) - - print('Done writing HTML output file ' + ofilename) - - -# --------------------------------------------------------------- -# cace_summarize_result -# -# Print a summary report of a single "results" block from a -# datasheet. -# --------------------------------------------------------------- + # Generate xschem symbol svg + svgpath = os.path.join( + datasheet['paths']['root'], + datasheet['paths']['documentation'], + f'{datasheet["name"]}_symbol.svg', + ) + symname = datasheet['name'] + '.sym' + sympath = os.path.join( + datasheet['paths']['root'], datasheet['paths']['schematic'], symname + ) + + if xschem_generate_svg(sympath, svgpath): + err(f'Error generating SVG for symbol.') + + # Generate xschem schematic svg + svgpath = os.path.join( + datasheet['paths']['root'], + datasheet['paths']['documentation'], + f'{datasheet["name"]}_schematic.svg', + ) -def cace_summarize_result(param, result): - spec = param['spec'] - unit = param['unit'] if 'unit' in param else '' + schemname = datasheet['name'] + '.sch' + schempath = os.path.join( + datasheet['paths']['root'], datasheet['paths']['schematic'], schemname + ) + + if xschem_generate_svg(schempath, svgpath): + err(f'Error generating SVG for schematic.') - keys = ['minimum', 'typical', 'maximum'] + # Generate KLayout image + + svgpath = os.path.join( + datasheet['paths']['root'], + datasheet['paths']['documentation'], + f'{datasheet["name"]}_klayout.svg', + ) - print(' Source type: ' + result['name']) - for key in keys: - if key in spec and key in result: - speclist = spec[key] - if isinstance(speclist, str): - specvalue = speclist - else: - specvalue = speclist[0] - resultlist = result[key] + # Use GDSII + if 'layout' in datasheet['paths']: + layout_directory = datasheet['paths']['layout'] + layoutname = datasheet['name'] + '.gds' + layout_path = os.path.join(layout_directory, layoutname) + # Search for compressed layout + if not os.path.exists(layout_path): + layoutname = projname + '.gds.gz' + layout_path = os.path.join(layout_directory, layoutname) + else: + err('No "layout" specified in datasheet paths.') - # Use this complicated replacement for print() - # to keep any unicode characters in the units - # from mis-printing. + klayout_generate_png( + layout_path, + os.path.join( + datasheet['paths']['root'], datasheet['paths']['documentation'] + ), + ) - # Output format is, e.g., : - # minimum(mA): spec = any measured = 12.7534 (pass) + # Generate magic image - outline = ' ' + key + ' (' + unit + '):' - outline += ' spec = ' + specvalue + ' measured = ' - outline += resultlist[0] + ' (' + resultlist[1] + ')\n' + svgpath = os.path.join( + datasheet['paths']['root'], + datasheet['paths']['documentation'], + f'{datasheet["name"]}_magic.svg', + ) - sys.stdout.buffer.write(outline.encode('latin1')) + # Prefer magic layout + if 'magic' in datasheet['paths']: + magic_directory = datasheet['paths']['magic'] + magicname = datasheet['name'] + '.mag' + layout_path = os.path.join(magic_directory, magicname) + is_mag = True + # Else use GDSII + elif 'layout' in datasheet['paths']: + layout_directory = datasheet['paths']['layout'] + layoutname = datasheet['name'] + '.gds' + layout_path = os.path.join(layout_directory, layoutname) + # Search for compressed layout + if not os.path.exists(layout_path): + layoutname = projname + '.gds.gz' + layout_path = os.path.join(layout_directory, layoutname) + else: + err('Neither "magic" nor "layout" specified in datasheet paths.') - print('') + # magic_generate_svg(layout_path, svgpath) def markdown_summary(datasheet, runtime_options, results): @@ -889,28 +264,7 @@ def markdown_summary(datasheet, runtime_options, results): # Table spacings sp = [20, 20, 10, 12, 10, 12, 10, 12, 8] - result += '\n# CACE Summary\n\n' - - result += ''.join( - [ - f'**general**\n\n', - f'- name: {datasheet["name"] if "name" in datasheet else ""}\n', - f'- description: {datasheet["description"] if "description" in datasheet else ""}\n', - f'- commit: {datasheet["commit"] if "commit" in datasheet else ""}\n', - f'- PDK: {datasheet["PDK"] if "PDK" in datasheet else ""}\n', - f'- cace_format: {datasheet["cace_format"] if "cace_format" in datasheet else ""}\n\n', - ] - ) - - result += ''.join( - [ - f'**authorship**\n\n', - f'- designer: {datasheet["authorship"]["designer"] if "designer" in datasheet["authorship"] else ""}\n', - f'- company: {datasheet["authorship"]["company"] if "company" in datasheet["authorship"] else ""}\n', - f'- creation_date: {datasheet["authorship"]["creation_date"] if "creation_date" in datasheet["authorship"] else ""}\n', - f'- license: {datasheet["authorship"]["license"] if "license" in datasheet["authorship"] else ""}\n\n', - ] - ) + result += f'\n# CACE Summary for {datasheet["name"]}\n\n' result += f'**netlist source**: {runtime_options["netlist_source"]}\n\n' @@ -1041,103 +395,6 @@ def markdown_summary(datasheet, runtime_options, results): return result -# --------------------------------------------------------------- -# cace_summary -# -# Print a summary report of results from a datasheet -# -# "datasheet" is a CACE characterization dataset -# "paramname" is a list of parameters to summarize, -# or if it is None, then all parameters should be output. -# --------------------------------------------------------------- - - -def cace_summary(datasheet, paramnames): - - # Summarize all parameters - if not paramnames: - if 'electrical_parameters' in datasheet: - for eparam in datasheet['electrical_parameters']: - print('Electrical parameter ' + eparam['name']) - if 'description' in eparam: - print(' ' + eparam['description']) - if 'display' in eparam: - print(' ' + eparam['display']) - if 'spec' not in eparam: - print(' (Parameter does not have a spec)') - elif 'results' not in eparam: - print(' (No results to report)') - else: - results = eparam['results'] - if isinstance(results, list): - for result in eparam['results']: - cace_summarize_result(eparam, result) - else: - cace_summarize_result(eparam, results) - - if 'physical_parameters' in datasheet: - for pparam in datasheet['physical_parameters']: - print('Physical parameter ' + pparam['name']) - if 'description' in pparam: - print(' ' + pparam['description']) - if 'display' in eparam: - print(' ' + eparam['display']) - if 'spec' not in pparam: - print(' (Parameter does not have a spec)') - elif 'results' not in pparam: - print(' (No results to report)') - else: - results = pparam['results'] - if isinstance(results, list): - for result in pparam['results']: - cace_summarize_result(pparam, result) - else: - cace_summarize_result(pparam, results) - - # Only summarize the parameters in the list - else: - for paramname in paramnames: - if 'electrical_parameters' in datasheet: - for eparam in datasheet['electrical_parameters']: - if paramname == eparam['name']: - print('Electrical parameter ' + eparam['name']) - if 'description' in eparam: - print(' ' + eparam['description']) - if 'display' in eparam: - print(' ' + eparam['display']) - if 'spec' not in eparam: - print(' (Parameter does not have a spec)') - elif 'results' not in eparam: - print(' (No results to report)') - else: - results = eparam['results'] - if isinstance(results, list): - for result in eparam['results']: - cace_summarize_result(eparam, result) - else: - cace_summarize_result(eparam, results) - - if 'physical_parameters' in datasheet: - for pparam in datasheet['physical_parameters']: - if paramname == pparam['name']: - print('Physical parameter ' + pparam['name']) - if 'description' in pparam: - print(' ' + pparam['description']) - if 'display' in eparam: - print(' ' + eparam['display']) - if 'spec' not in pparam: - print(' (Parameter does not have a spec)') - elif 'results' not in pparam: - print(' (No results to report)') - else: - results = pparam['results'] - if isinstance(results, list): - for result in pparam['results']: - cace_summarize_result(pparam, result) - else: - cace_summarize_result(pparam, results) - - def uchar_sub(string): """ Convert from unicode to text format diff --git a/cace/common/common.py b/cace/common/common.py index 8975ad5..5d4091c 100644 --- a/cace/common/common.py +++ b/cace/common/common.py @@ -29,26 +29,14 @@ def get_pdk(magicfilename=None): """ - Get a value for the PDK, either from the second line of a .mag file, - or from the environment as environment variable "PDK". - - NOTE: Normally the PDK is provided as part of the datasheet, as - a project does not necessarily have a .mag file; so there is no - source for automatically determining the project PDK. + Get a value for the PDK, as environment variable "PDK". """ - if magicfilename and os.path.isfile(magicfilename): - with open(magicfilename, 'r') as ifile: - for line in ifile.readlines(): - tokens = line.split() - if tokens[0] == 'tech': - pdk = tokens[1] - break - else: - try: - pdk = os.environ['PDK'] - except KeyError: - error('No .mag file and PDK is not defined in the environment.') - pdk = None + + try: + pdk = os.environ['PDK'] + except KeyError: + error('PDK is not defined in the environment.') + pdk = None return pdk @@ -78,12 +66,14 @@ def get_pdk_root(): if pdk_root: os.environ['PDK_ROOT'] = pdk_root else: - error('Could not locate PDK_ROOT!') + error( + 'PDK_ROOT is not defined in the environment and could not automatically locate PDK_ROOT.' + ) return pdk_root -def get_magic_rcfile(datasheet, magicfilename=None): +def get_magic_rcfile(): """ Get the path and filename of the magic startup script corresponding to the PDK. @@ -92,33 +82,55 @@ def get_magic_rcfile(datasheet, magicfilename=None): script (.magicrc file). """ - if 'PDK_ROOT' in datasheet: - pdk_root = datasheet['PDK_ROOT'] - else: - pdk_root = get_pdk_root() - - if 'PDK' in datasheet: - pdk = datasheet['PDK'] - elif magicfilename: - pdk = get_pdk(magicfilename) - else: - paths = datasheet['paths'] - if magicfilename: - pdk = get_pdk(magicfilename) - elif 'magic' in paths: - magicpath = paths['magic'] - magicfilename = os.path.join(magicpath, magicname) - pdk = get_pdk(magicfilename) - else: - return None + pdk_root = get_pdk_root() + pdk = get_pdk() rcfile = os.path.join( pdk_root, pdk, 'libs.tech', 'magic', pdk + '.magicrc' ) + return rcfile -def get_netgen_setupfile(datasheet): +def get_klayout_techfile(): + """ + Get the path and filename of the klayout tech file corresponding + to the PDK. + + Returns a string containing the full path and filename of the tech + file (.lyt file). + """ + + pdk_root = get_pdk_root() + pdk = get_pdk() + + techfile = os.path.join( + pdk_root, pdk, 'libs.tech', 'klayout', 'tech', pdk + '.lyt' + ) + + return techfile + + +def get_klayout_layer_props(): + """ + Get the path and filename of the klayout layer properties corresponding + to the PDK. + + Returns a string containing the full path and filename of the layer + properties (.lyp file). + """ + + pdk_root = get_pdk_root() + pdk = get_pdk() + + techfile = os.path.join( + pdk_root, pdk, 'libs.tech', 'klayout', 'tech', pdk + '.lyp' + ) + + return techfile + + +def get_netgen_setupfile(): """ Get the path and filename of the netgen setup script corresponding to the PDK. @@ -127,27 +139,13 @@ def get_netgen_setupfile(datasheet): script (.tcl file). """ - if 'PDK_ROOT' in datasheet: - pdk_root = datasheet['PDK_ROOT'] - else: - pdk_root = get_pdk_root() - - if 'PDK' in datasheet: - pdk = datasheet['PDK'] - elif magicfilename: - pdk = get_pdk(magicfilename) - else: - paths = datasheet['paths'] - if 'magic' in paths: - magicpath = paths['magic'] - magicfilename = os.path.join(magicpath, magicname) - pdk = get_pdk(magicfilename) - else: - return None + pdk_root = get_pdk_root() + pdk = get_pdk() setupfile = os.path.join( pdk_root, pdk, 'libs.tech', 'netgen', pdk + '_setup.tcl' ) + return setupfile @@ -237,6 +235,221 @@ def set_xschem_paths(datasheet, symbolpath, tclstr=None): return ' ; '.join(tcllist) +def xschem_generate_svg(schempath, svgpath): + """ + Generate an SVG drawing of a schematic or symbol using xschem + + Return 0 if the drawing was generated, 1 if not. + """ + + if not os.path.isfile(schempath): + err(f'Could not find {schempath}.') + return 1 + + # Xschem arguments: + # -r: Bypass readline (because stdin/stdout are piped) + # -x: No X11 / No GUI window + # -q: Quit after processing command line + + xschemargs = [ + '-r', + '-x', + '-q', + '--svg', + '--plotfile', + svgpath, + ] + + pdk_root = get_pdk_root() + pdk = get_pdk() + + # See if there is an xschemrc file we can source + xschemrcfile = os.path.join(os.path.split(schempath)[0], 'xschemrc') + if os.path.isfile(xschemrcfile): + xschemargs.extend(['--rcfile', xschemrcfile]) + else: + warn(f'No project xschemrc file found at: {xschemrcfile}') + warn( + f'It is highly recommended to set up an xschemrc file for your project.' + ) + + # Use the PDK xschemrc file for xschem startup + xschemrcfile = os.path.join( + pdk_root, pdk, 'libs.tech', 'xschem', 'xschemrc' + ) + warn(f'Using the PDK xschemrc instead…') + if os.path.isfile(xschemrcfile): + xschemargs.extend(['--rcfile', xschemrcfile]) + else: + err(f'No xschemrc file found in the {pdk} PDK!') + + xschemargs.append(schempath) + + dbg('Generating SVG using xschem.') + + returncode = run_subprocess('xschem', xschemargs, write_file=False) + + if returncode != 0: + return 1 + + return 0 + + +def magic_generate_svg(layout_path, svgpath): + """ + Generate an SVG drawing of a layout using magic + + Return 0 if the drawing was generated, 1 if not. + """ + + if not os.path.isfile(layout_path): + err(f'Could not find {layout_path}.') + return 1 + + layout_directory = os.path.split(layout_path)[0] + layout_filename = os.path.split(layout_path)[1] + layout_cellname = os.path.splitext(layout_filename)[0] + layout_extension = os.path.splitext(layout_filename)[1] + + rcfile = get_magic_rcfile() + + magic_input = '' + + magic_input += f'addpath {os.path.abspath(layout_directory)}\n' + if layout_extension == '.mag': + magic_input += f'load {layout_filename}\n' + elif layout_extension == '.gds': + magic_input += f'gds read {layout_filename}\n' + magic_input += f'load {layout_cellname}\n' + else: + err(f'Unknown file extension for: {layout_path}') + return 1 + + magic_input += f'plot svg {svgpath}\n' + + returncode = run_subprocess( + 'magic', + ['-noconsole', '-d XR', '-rcfile', rcfile], + input=magic_input, + write_file=False, + ) + + if returncode != 0: + return 1 + + return 0 + + +def klayout_generate_png(layout_path, out_path): + """ + Generate a PNG drawing of a layout using klayout + + Return 0 if the drawing was generated, 1 if not. + """ + + if not os.path.isfile(layout_path): + err(f'Could not find {layout_path}.') + return 1 + + layout_directory = os.path.split(layout_path)[0] + layout_filename = os.path.split(layout_path)[1] + layout_cellname = os.path.splitext(layout_filename)[0] + layout_extension = os.path.splitext(layout_filename)[1] + + techfile = get_klayout_techfile() + layer_props = get_klayout_layer_props() + pdk = get_pdk() + + if pdk == 'sky130A': + tech_name = 'sky130' + elif pdk == 'sky130B': + tech_name = 'sky130' + else: + tech_name = pdk + + klayout_script = """import pya +import os + +# Input: +# gds_path: path to the gds file +# out_path: output directory +# out_name: output name +# w: width + +if not 'w' in globals(): + w = 1024 + +background_white = "#FFFFFF" +background_black = "#000000" + +lv = pya.LayoutView() + +lv.set_config("grid-visible", "false") +lv.set_config("grid-show-ruler", "false") +lv.set_config("text-visible", "false") +tech = pya.Technology.technology_by_name(tech_name) +lv.load_layout(gds_path, tech.load_layout_options, tech_name) +lv.max_hier() + +ly = lv.active_cellview().layout() + +# top cell bounding box in micrometer units +bbox = ly.top_cell().dbbox() + +# compute an image size having the same aspect ratio than +# the bounding box +h = int(0.5 + w * bbox.height() / bbox.width()) + +lv.load_layer_props(layer_props) + +lv.set_config("background-color", background_white) +lv.save_image_with_options(os.path.join(out_path, out_name + "_w.png"), w, h, 0, 0, 0, bbox, False) + +lv.set_config("background-color", background_black) +lv.save_image_with_options(os.path.join(out_path, out_name + "_b.png"), w, h, 0, 0, 0, bbox, False)""" + + scriptpath = 'klayout_script.py' + + with open(scriptpath, 'w') as f: + f.write(klayout_script) + + # -b: batch mode + # -nn: tech file + # -r: script + # -rd =: script variable + + returncode = run_subprocess( + 'klayout', + [ + '-b', + '-nn', + techfile, + '-r', + scriptpath, + '-rd', + f'gds_path={layout_path}', + '-rd', + f'out_path={out_path}', + '-rd', + f'out_name={layout_cellname}', + '-rd', + f'tech_name={tech_name}', + '-rd', + f'layer_props={layer_props}', + ], + write_file=False, + ) + + # Delete script after use + if os.path.isfile(scriptpath): + os.remove(scriptpath) + + if returncode != 0: + return 1 + + return 0 + + # ----------------------------------------------------------------------- # floating-point linear numeric sequence generator, to be used with # condition generator @@ -397,7 +610,12 @@ def get_condition_names_used(template): return (condlist, default_cond) -def run_subprocess(proc, args=[], env=None, input=None, cwd=None): +def run_subprocess( + proc, args=[], env=None, input=None, cwd=None, write_file=True +): + + if not cwd: + cwd = os.getcwd() dbg( f'Subprocess {proc} {" ".join(args)} at \'[repr.filename][link=file://{os.path.abspath(cwd)}]{os.path.relpath(cwd)}[/link][/repr.filename]\'…' @@ -431,7 +649,7 @@ def run_subprocess(proc, args=[], env=None, input=None, cwd=None): dbg(line.rstrip('\n')) # Write stderr to file - if stderr: + if stderr and write_file: with open( f'{os.path.join(cwd, proc)}_stderr.out', 'w' ) as stderr_file: @@ -444,7 +662,7 @@ def run_subprocess(proc, args=[], env=None, input=None, cwd=None): dbg(line.rstrip()) # Write stdout to file - if stdout: + if stdout and write_file: with open( f'{os.path.join(cwd, proc)}_stdout.out', 'w' ) as stdout_file: diff --git a/cace/parameter/parameter_magic_area.py b/cace/parameter/parameter_magic_area.py index 0d56483..02f86c9 100755 --- a/cace/parameter/parameter_magic_area.py +++ b/cace/parameter/parameter_magic_area.py @@ -20,11 +20,7 @@ import threading import subprocess -from ..common.common import ( - run_subprocess, - get_magic_rcfile, - get_netgen_setupfile, -) +from ..common.common import run_subprocess, get_magic_rcfile from ..common.ring_buffer import RingBuffer from .parameter import Parameter, ResultType, Argument from .parameter_manager import register_parameter @@ -93,7 +89,7 @@ def implementation(self): projname = self.datasheet['name'] paths = self.datasheet['paths'] - rcfile = get_magic_rcfile(self.datasheet) + rcfile = get_magic_rcfile() # Prefer magic layout if 'magic' in paths: diff --git a/cace/parameter/parameter_magic_drc.py b/cace/parameter/parameter_magic_drc.py index 7b575df..e8baf01 100755 --- a/cace/parameter/parameter_magic_drc.py +++ b/cace/parameter/parameter_magic_drc.py @@ -16,11 +16,7 @@ import re import sys -from ..common.common import ( - run_subprocess, - get_magic_rcfile, - get_netgen_setupfile, -) +from ..common.common import run_subprocess, get_magic_rcfile from .parameter import Parameter, ResultType, Argument from .parameter_manager import register_parameter from ..logging import ( @@ -52,6 +48,7 @@ def __init__( ) self.add_argument(Argument('args', [], False)) + self.add_argument(Argument('gds_flatten', False, False)) def is_runnable(self): netlist_source = self.runtime_options['netlist_source'] @@ -118,7 +115,7 @@ def implementation(self): layout_locname = os.path.split(layout_filename)[1] layout_cellname = os.path.splitext(layout_locname)[0] - rcfile = get_magic_rcfile(self.datasheet) + rcfile = get_magic_rcfile() magic_input = '' @@ -126,6 +123,8 @@ def implementation(self): if is_mag: magic_input += f'load {layout_cellname}\n' else: + if self.get_argument('gds_flatten'): + magic_input += 'gds flatglob *\n' magic_input += f'gds read {layout_locname}\n' magic_input += 'set toplist [cellname list top]\n' magic_input += 'set numtop [llength $toplist]\n' @@ -166,9 +165,8 @@ def implementation(self): magrex = re.compile('drc[ \t]+=[ \t]+([0-9.]+)[ \t]*$') - drcfilepath = os.path.join( - f'{os.path.join(self.param_dir, "magic")}_stdout.out' - ) + stdoutfilepath = os.path.join(self.param_dir, 'magic_stdout.out') + drcfilepath = os.path.join(self.param_dir, 'magic_drc.out') if not os.path.isfile(drcfilepath): err('No output file generated by magic!') @@ -181,7 +179,7 @@ def implementation(self): ) drccount = None - with open(drcfilepath, 'r') as stdout_file: + with open(stdoutfilepath, 'r') as stdout_file: for line in stdout_file.readlines(): lmatch = magrex.match(line) diff --git a/cace/parameter/parameter_manager.py b/cace/parameter/parameter_manager.py index bd19395..626c142 100755 --- a/cace/parameter/parameter_manager.py +++ b/cace/parameter/parameter_manager.py @@ -23,13 +23,11 @@ import datetime import threading -from ..common.common import run_subprocess from ..common.misc import mkdirp from ..common.cace_read import cace_read, cace_read_yaml from ..common.cace_write import ( - cace_summary, markdown_summary, - cace_generate_html, + generate_documentation, ) from ..common.cace_regenerate import regenerate_netlists, regenerate_gds @@ -250,9 +248,37 @@ def summarize_datasheet(self): self.datasheet, self.runtime_options, self.results ) - def generate_html(self): - debug = self.get_runtime_options('debug') - cace_generate_html(self.datasheet, None, debug) + def generate_documentation(self): + if 'documentation' in self.datasheet['paths']: + doc_path = os.path.join( + self.datasheet['paths']['root'], + self.datasheet['paths']['documentation'], + ) + + info(f"Generating documentation in '{os.path.relpath(doc_path)}'") + + # Create path to documentation + mkdirp(doc_path) + + # Generate the documentation + generate_documentation(self.datasheet) + + # Save summary for netlist type + summary = markdown_summary( + self.datasheet, self.runtime_options, self.results + ) + summarypath = os.path.join( + self.datasheet['paths']['root'], + self.datasheet['paths']['documentation'], + f'{self.datasheet["name"]}_{self.runtime_options["netlist_source"]}.md', + ) + with open(summarypath, 'w') as ofile: + ofile.write(summary) + + else: + info( + f'Path "documentation" not set in datasheet. Skipping documentation generation.' + ) def duplicate_parameter(self, pname): param = self.find_parameter(pname) @@ -346,6 +372,12 @@ def validate_runtime_options(self): f'Invalid netlist source: {self.runtime_options["netlist_source"]}' ) + # If a magic layout is given, make sure layout is also defined + if 'magic' in self.datasheet['paths']: + if not 'layout' in self.datasheet['paths']: + # Default layout path + self.datasheet['paths']['layout'] = 'gds' + # Replace "best" with the best possible source if self.runtime_options['netlist_source'] == 'best': # If a layout is given, the best source is rcx diff --git a/cace/parameter/parameter_netgen_lvs.py b/cace/parameter/parameter_netgen_lvs.py index 5f109c9..f00f515 100755 --- a/cace/parameter/parameter_netgen_lvs.py +++ b/cace/parameter/parameter_netgen_lvs.py @@ -150,7 +150,7 @@ def implementation(self): else: layout_arg = layout_netlist - lvs_setup = get_netgen_setupfile(self.datasheet) + lvs_setup = get_netgen_setupfile() # Run LVS as a subprocess and wait for it to finish. Use the -json # switch to get a file that is easy to parse.