diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py index 9e60a99..c9483b8 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py @@ -17,209 +17,143 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +Module for generating cell layouts for given technology files and GDS files. + +Creates cell layout SVG files from GDS cell files using `magic` tool. +""" + import sys import os import re -import subprocess import argparse -FATAL_ERROR = re.compile('((Error parsing)|(No such file or directory)|(couldn\'t be read))') # noqa: E501 -READING_REGEX = re.compile('Reading "([^"]*)".') +sys.path.insert(0, os.path.abspath(__file__ + '../../../')) -debug = True -superdebug = True +from skywater_pdk.tools import magic, draw # noqa: E402 -def _magic_tcl_header(ofile, gdsfile): +def convert_gds_to_svg( + input_gds, + input_techfile, + output_svg=None, + tmp_tcl=None, + keep_temporary_files=False) -> int: """ - Adds a header to TCL file. + Converts GDS file to SVG cell layout diagram. + + Generates TCL script for drawing a cell layout in `magic` tool and creates + a SVG file with the diagram. Parameters ---------- - ofile: output file stream - gdsfile: path to GDS file - """ - print('#!/bin/env wish', file=ofile) - print('drc off', file=ofile) - print('scalegrid 1 2', file=ofile) - print('cif istyle vendorimport', file=ofile) - print('gds readonly true', file=ofile) - print('gds rescale false', file=ofile) - print('tech unlock *', file=ofile) - print('cif warning default', file=ofile) - print('set VDD VPWR', file=ofile) - print('set GND VGND', file=ofile) - print('set SUB SUBS', file=ofile) - print('gds read ' + gdsfile, file=ofile) + input_gds : str + Path to input GDS file + input_techfile : str + Path to input technology definition file (.tech) + output_svg : str + Path to output SVG file + keep_temporary_files : bool + Determines if intermediate TCL script should be kept - -def run_magic(destdir, tcl_path, input_techfile, d="null"): - """ - Runs magic to generate layout files. - - Parameters - ---------- - destdir: destination directory - tcl_path: path to input TCL file - input_techfile: path to the technology file - d: Workstation type, can be NULL, X11, OGL or XWIND - """ - cmd = [ - 'magic', - '-nowrapper', - '-d'+d, - '-noconsole', - '-T', input_techfile, - os.path.abspath(tcl_path) - ] - with open(tcl_path.replace(".tcl", ".sh"), "w") as f: - f.write("#!/bin/sh\n") - f.write(" ".join(cmd)) - mproc = subprocess.run( - cmd, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=destdir, - universal_newlines=True) - assert mproc.stdout - max_cellname_width = 0 - output_by_cells = [('', [])] - fatal_errors = [] - for line in mproc.stdout.splitlines(): - cifwarn = ('CIF file read warning: Input off lambda grid by 1/2; ' + - 'snapped to grid') - if line.startswith(cifwarn): - continue - m = FATAL_ERROR.match(line) - if m: - fatal_errors.append(line) - m = READING_REGEX.match(line) - if m: - cell_name = m.group(1) - max_cellname_width = max(max_cellname_width, len(cell_name)) - output_by_cells.append((cell_name, [])) - output_by_cells[-1][-1].append(line) - for cell, lines in output_by_cells: - prefix = "magic " + cell.ljust(max_cellname_width) + ':' - for line in lines: - is_error = 'rror' in line - if superdebug or (debug and is_error): - print(prefix, line) - assert not mproc.stderr, mproc.stderr - if mproc.returncode != 0 or fatal_errors: - if fatal_errors: - msg = ['ERROR: Magic had fatal errors in output:'] + fatal_errors - else: - msg = ['ERROR: Magic exited with status ' + str(mproc.returncode)] - msg.append("") - msg.append(" ".join(cmd)) - msg.append('='*75) - msg.append(mproc.stdout) - msg.append('='*75) - msg.append(destdir) - msg.append(tcl_path) - msg.append('-'*75) - msg.append(msg[0]) - raise SystemError('\n'.join(msg)) - return output_by_cells - - -def convert_to_svg(input_gds, input_techfile, output=None): - """ - Converts GDS file to a SVG layout image. - - Parameters - ---------- - input_gds: path to input GDS file - input_techfile: path to the technology file - output: optional path to the final SVG file + Returns + ------- + int : 0 if finished successfully, error code from `magic` otherwise """ input_gds = os.path.abspath(input_gds) - input_techfile = os.path.abspath(input_techfile) - destdir, gdsfile = os.path.split(input_gds) - basename, ext = os.path.splitext(gdsfile) - if output: - output_svg = output + if output_svg: + output_svg = os.path.abspath(output_svg) + destdir, _ = os.path.split(output_svg) else: - output_svg = os.path.join(destdir, "{}.svg".format(basename)) - assert not os.path.exists(output_svg), output_svg + " already exists!?" - tcl_path = os.path.join(destdir, "{}.gds2svg.tcl".format(basename)) - with open(tcl_path, 'w') as ofile: - _magic_tcl_header(ofile, input_gds) - ofile.write("load " + basename + "\n") - ofile.write("box 0 0 0 0\n") - ofile.write("select top cell\n") - ofile.write("expand\n") - ofile.write("view\n") - ofile.write("select clear\n") - ofile.write("box position -1000 -1000\n") - ofile.write("plot svg " + basename + ".tmp1.svg\n") - ofile.write("quit -noprompt\n") - run_magic(destdir, tcl_path, input_techfile, " XR") - tmp1_svg = os.path.join(destdir, "{}.tmp1.svg".format(basename)) - tmp2_svg = os.path.join(destdir, "{}.tmp2.svg".format(basename)) - assert os.path.exists(tmp1_svg), tmp1_svg + " doesn't exist!?" - os.unlink(tcl_path) - for i in range(0, 3): - # Remove the background - with open(tmp1_svg) as f: - data = f.read() - data = re.sub( - '<rect[^>]* style="[^"]*fill-opacity:1;[^"]*"/>', - '', - data - ) - with open(tmp2_svg, 'w') as f: - f.write(data) - # Use inkscape to crop - retcode = run_inkscape([ + destdir, name = os.path.split(input_gds) + output_svg = os.path.join(destdir, f'{name}.svg') + input_techfile = os.path.abspath(input_techfile) + + workdir, _ = os.path.split(input_techfile) + + if output_svg: + filename, _ = os.path.splitext(output_svg) + if not tmp_tcl: + tmp_tcl = f'{filename}.tcl' + try: + tmp_tcl, output_svg = magic.create_tcl_plot_script_for_gds( + input_gds, + tmp_tcl, + output_svg) + magic.run_magic( + tmp_tcl, + input_techfile, + workdir, + display_workstation='XR') + + if not keep_temporary_files: + if os.path.exists(tmp_tcl): + os.unlink(tmp_tcl) + assert os.path.exists(output_svg), f'Magic did not create {output_svg}' + except magic.MagicError as err: + if not keep_temporary_files: + if os.path.exists(tmp_tcl): + os.unlink(tmp_tcl) + print(err) + return err.errorcode + except Exception: + if not keep_temporary_files: + if os.path.exists(tmp_tcl): + os.unlink(tmp_tcl) + raise + return 0 + + +def cleanup_gds_diagram(input_svg, output_svg) -> int: + """ + Crops and cleans up GDS diagram. + + Parameters + ---------- + input_svg : str + Input SVG file with cell layout + output_svg : str + Output SVG file with cleaned cell layout + + Returns + ------- + int : 0 if successful, error code from Inkscape otherwise + """ + with open(input_svg, 'r') as f: + data = f.read() + data = re.sub( + '<rect[^>]* style="[^"]*fill-opacity:1;[^"]*"/>', + '', + data + ) + with open(output_svg, 'w') as f: + f.write(data) + result = draw.run_inkscape([ "--verb=FitCanvasToDrawing", "--verb=FileSave", "--verb=FileClose", "--verb=FileQuit", - tmp2_svg]) - if retcode == 0: - break - for i in range(0, 3): - # Convert back to plain SVG - retcode = run_inkscape([ - "--export-plain-svg=%s" % (tmp2_svg), - "--export-background-opacity=1.0", - tmp2_svg]) - if retcode == 0: - break - os.unlink(tmp1_svg) - # Move into the correct location - os.rename(tmp2_svg, output_svg) - print("Created", output_svg) + output_svg + ], + 3) + if result[-1] != 0: + return result[-1] - -def run_inkscape(args): - """ - Run Inkscape with given arguments. - - Parameters - ---------- - args: List of arguments to be passed to Inkscape - """ - p = subprocess.Popen(["inkscape"] + args) - try: - p.wait(timeout=60) - except subprocess.TimeoutExpired: - print("ERROR: Inkscape timed out! Sending SIGTERM") - p.terminate() - try: - p.wait(timeout=60) - except subprocess.TimeoutExpired: - print("ERROR: Inkscape timed out! Sending SIGKILL") - p.kill() - p.wait() - return p.returncode + result = draw.run_inkscape([ + f'--export-plain-svg={output_svg}', + '--existsport-background-opacity=1.0', + output_svg + ], + 3) + return result[-1] def main(argv): - parser = argparse.ArgumentParser(argv[0]) + parser = argparse.ArgumentParser( + prog=argv[0], + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) parser.add_argument( 'input_gds', help="Path to the input .gds file" @@ -232,9 +166,42 @@ def main(argv): '--output-svg', help='Path to the output .svg file' ) + parser.add_argument( + '--output-tcl', + help='Path to temporary TCL file' + ) + parser.add_argument( + '--keep-temporary-files', + help='Keep the temporary files in the end', + action='store_true' + ) args = parser.parse_args(argv[1:]) - convert_to_svg(args.input_gds, args.input_tech, args.output_svg) - return 0 + + if args.output_svg: + filename, _ = os.path.splitext(args.output_svg) + tmp_svg = f'{filename}.tmp.svg' + else: + filename, _ = os.path.splitext(args.input_gds) + tmp_svg = f'{filename}.tmp.svg' + args.output_svg = f'{filename}.svg' + + result = convert_gds_to_svg( + args.input_gds, + args.input_tech, + tmp_svg, + args.output_tcl, + args.keep_temporary_files + ) + + if result != 0: + return result + + result = cleanup_gds_diagram(tmp_svg, args.output_svg) + + if not args.keep_temporary_files: + os.unlink(tmp_svg) + + return result if __name__ == '__main__': diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py index 2ab151e..dada432 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py @@ -17,6 +17,10 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +Creates cell layouts for cells with GDS files in the skywater-pdk libraries. +""" + import argparse from pathlib import Path import sys @@ -24,11 +28,15 @@ import contextlib import traceback import errno -from gds_to_svg import convert_to_svg +from gds_to_svg import convert_gds_to_svg def main(argv): - parser = argparse.ArgumentParser(prog=argv[0]) + parser = argparse.ArgumentParser( + prog=argv[0], + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) parser.add_argument( 'libraries_dir', help='Path to the libraries directory of skywater-pdk', @@ -92,7 +100,7 @@ def main(argv): print('Run the script with --create-dirs') return errno.ENOENT outfile = outdir / gdsfile.with_suffix('.svg').name - convert_to_svg(gdsfile, args.tech_file, outfile) + convert_gds_to_svg(gdsfile, args.tech_file, outfile) except Exception: print( f'Failed to generate cell layout for {str(gdsfile)}', diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py b/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py new file mode 100644 index 0000000..646d462 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 SkyWater PDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +A module for altering diagrams and image files using Inkscape and other tools. +""" + +import subprocess + + +def run_inkscape(args, retries=1, inkscape_executable='inkscape') -> int: + """ + Runs Inkscape for given arguments. + + Parameters + ---------- + args : List[str] + List of arguments to provide to Inkscape + retries : int + Number of tries to run Inkscape with given arguments + + Returns + Union[List[int], int] : error codes for Inkscape runs + """ + returncodes = [] + for i in range(retries): + p = subprocess.Popen([inkscape_executable] + args) + try: + p.wait(timeout=60) + except subprocess.TimeoutExpired: + print("ERROR: Inkscape timed out! Sending SIGTERM") + p.terminate() + try: + p.wait(timeout=60) + except subprocess.TimeoutExpired: + print("ERROR: Inkscape timed out! Sending SIGKILL") + p.kill() + p.wait() + if retries == 1: + return p.returncode + returncodes.append(p.returncode) + if p.returncode == 0: + break + return returncodes diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py b/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py new file mode 100644 index 0000000..4a42cd7 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 SkyWater PDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +A module containing various functions related to `magic` tool +""" + +import os +import re +import subprocess +from typing import Tuple + +FATAL_ERROR = re.compile('((Error parsing)|(No such file or directory)|(couldn\'t be read))') # noqa: E501 + + +class MagicError(Exception): + """ + Raised when there are errors in magic tool execution. + """ + def __init__(self, message, errorcode): + self.errorcode = errorcode + super().__init__(message) + + +def add_magic_tcl_header(ofile, gdsfile): + """ + Adds a header to a TCL file. + + Parameters + ---------- + ofile : TextIOWrapper + output file stream + gdsfile : str + path to GDS file + """ + ofile.write('#!/bin/env wish\n') + ofile.write('drc off\n') + ofile.write('scalegrid 1 2\n') + ofile.write('cif istyle vendorimport\n') + ofile.write('gds readonly true\n') + ofile.write('gds rescale false\n') + ofile.write('tech unlock *\n') + ofile.write('cif warning default\n') + ofile.write('set VDD VPWR\n') + ofile.write('set GND VGND\n') + ofile.write('set SUB SUBS\n') + ofile.write(f'gds read {gdsfile}\n') + + +def create_tcl_plot_script_for_gds( + input_gds, + output_tcl=None, + output_svg=None) -> Tuple[str, str]: + """ + Creates TCL script for creating cell layout image from GDS file. + + Parameters + ---------- + input_gds : str + Path to GDS file + output_tcl : str + Path to created TCL file + output_svg : str + Path where the SVG file created by output_tcl script should be located + + Returns + ------- + Tuple(str, str) : paths to TCL and SVG files (can be used if autogenerated) + """ + input_gds = os.path.abspath(input_gds) + + destdir, gdsfile = os.path.split(input_gds) + basename, ext = os.path.splitext(gdsfile) + + if not output_tcl: + output_tcl = os.path.join(destdir, f'{basename}.gds2svg.tcl') + + if not output_svg: + output_svg = os.path.join(destdir, f'{basename}.tmp.svg') + + with open(output_tcl, 'w') as ofile: + add_magic_tcl_header(ofile, input_gds) + ofile.write(f"load {basename}\n") + ofile.write("box 0 0 0 0\n") + ofile.write("select top cell\n") + ofile.write("expand\n") + ofile.write("view\n") + ofile.write("select clear\n") + ofile.write("box position -1000 -1000\n") + ofile.write(f"plot svg {output_svg}\n") + ofile.write("quit -noprompt\n") + + return output_tcl, output_svg + + +def run_magic( + tcl_file, + technology_file, + workdir=None, + display_workstation='NULL', + debug=False, + magic_executable='magic') -> int: + """ + Generates layout files for a given TCL file and technology file. + + Uses `magic` tool for generating the layout. + + Parameters + ---------- + tcl_file : str + path to input TCL file + technology_file : str + path to the technology file + workdir : str + path to the working directory for `magic` + display_workstation : str + graphics interface, can be NULL, X11 or OpenGL + debug : bool + True if all output from `magic` tool should be displayed + magic_executable : str + Path to `magic` executable + + Returns + ------- + int: return code for `magic` tool + + Raises + ------ + AssertionError + Raised when display_workstation is not NULL, X11 or OpenGL + MagicError + Raised when `magic` tool failed to run for given files + """ + assert display_workstation in ['NULL', 'X11', 'OpenGL', 'XR'] + cmd = [ + magic_executable, + '-nowrapper', + '-noconsole', + f'-d{display_workstation}', + f'-T{os.path.abspath(technology_file)}', + '-D' if debug else '', + os.path.abspath(tcl_file) + ] + result = subprocess.run( + cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=workdir, + universal_newlines=True + ) + + errors_present = False + for line in result.stdout.splitlines(): + m = FATAL_ERROR.match(line) + if m: + errors_present = True + break + + if result.returncode != 0 or errors_present: + msg = ['ERROR: There were fatal errors in magic.'] + msg += result.stdout.splitlines() + msg += [f'ERROR: Magic exited with status {result.returncode}'] + msg.append("") + msg.append(" ".join(cmd)) + msg.append('='*75) + msg.append(result.stdout) + msg.append('='*75) + msg.append(tcl_file) + msg.append('-'*75) + msg.append(msg[0]) + raise MagicError('\n'.join(msg), result.returncode) + + if debug: + print(result.stdout)