From 7294a3e46eb8737fba96f0f498066fadf1bac34b Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Thu, 19 Nov 2020 12:47:00 +0100 Subject: [PATCH 1/8] Added script for converting GDS to SVG Signed-off-by: Grzegorz Latosinski --- .../skywater_pdk/gds_to_svg/gds_to_svg.py | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py 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 new file mode 100644 index 000000000..4eee510c1 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py @@ -0,0 +1,201 @@ +#!/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 + +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 "([^"]*)".') + +debug = True +superdebug = True + + +def _magic_tcl_header(ofile, gdsfile): + 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) + + +def run_magic(destdir, tcl_path, input_techfile, d="null"): + 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): + 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 + 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( + ']* style="[^"]*fill-opacity:1;[^"]*"/>', + '', + data + ) + with open(tmp2_svg, 'w') as f: + f.write(data) + # Use inkscape to crop + retcode = 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) + + +def run_inkscape(args): + 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 + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + 'input_gds', + help="Path to the input .gds file" + ) + parser.add_argument( + 'input_tech', + help="Path to the input .tech file" + ) + parser.add_argument( + '--output-svg', + help='Path to the output .svg file' + ) + args = parser.parse_args() + convert_to_svg(args.input_gds, args.input_tech, args.output_svg) From 9a40aa6fc885d8f866de3241a94d86777d583173 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Thu, 19 Nov 2020 13:54:41 +0100 Subject: [PATCH 2/8] Added script for generating cell layouts from GDS and tech files Signed-off-by: Grzegorz Latosinski --- .../gds_to_svg/generate-gds-svgs.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py 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 new file mode 100644 index 000000000..2ab151e22 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py @@ -0,0 +1,109 @@ +#!/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 + +import argparse +from pathlib import Path +import sys +import contextlib +import traceback +import errno + +from gds_to_svg import convert_to_svg + + +def main(argv): + parser = argparse.ArgumentParser(prog=argv[0]) + parser.add_argument( + 'libraries_dir', + help='Path to the libraries directory of skywater-pdk', + type=Path + ) + parser.add_argument( + 'tech_file', + help='Path to the .tech file', + type=Path + ) + parser.add_argument( + 'output_dir', + help='Path to the output directory', + type=Path + ) + parser.add_argument( + '--libname', + help='Library name to generate cell layouts for from GDS files', + type=str + ) + parser.add_argument( + '--version', + help='Version to generate cell layouts for from GDS files', + type=str + ) + parser.add_argument( + '--create-dirs', + help='Create directories for output when not present', + action='store_true' + ) + parser.add_argument( + '--failed-inputs', + help='Path to GDS files for which Magic failed to generate SVG files', + type=Path + ) + + args = parser.parse_args(argv[1:]) + + gdsfiles = list(args.libraries_dir.rglob('*.gds')) + + nc = contextlib.nullcontext() + + with open(args.failed_inputs, 'w') if args.failed_inputs else nc as err: + for gdsfile in gdsfiles: + outdir = (args.output_dir / + gdsfile.parent.resolve() + .relative_to(args.libraries_dir.resolve())) + library = outdir.relative_to(args.output_dir).parts[0] + ver = outdir.relative_to(args.output_dir).parts[1] + if args.libname and args.libname != library: + continue + if args.version and args.version != ver: + continue + print(f'===> {str(gdsfile)}') + try: + if not outdir.exists(): + if args.create_dirs: + outdir.mkdir(parents=True) + else: + print(f'The output directory {str(outdir)} is missing') + 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) + except Exception: + print( + f'Failed to generate cell layout for {str(gdsfile)}', + file=sys.stderr + ) + traceback.print_exc() + err.write(f'{gdsfile}\n') + + print('Finished generating cell layouts') + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) From 873c89fd1368a563d9485514d8d9460c075ae7bd Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Tue, 24 Nov 2020 17:32:31 +0100 Subject: [PATCH 3/8] gds_to_svg: added docstrings Signed-off-by: Grzegorz Latosinski --- .../skywater_pdk/gds_to_svg/gds_to_svg.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) 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 4eee510c1..9e60a998c 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,6 +17,7 @@ # # SPDX-License-Identifier: Apache-2.0 +import sys import os import re import subprocess @@ -30,6 +31,14 @@ def _magic_tcl_header(ofile, gdsfile): + """ + Adds a header to TCL file. + + 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) @@ -45,6 +54,16 @@ def _magic_tcl_header(ofile, gdsfile): 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', @@ -107,6 +126,15 @@ def run_magic(destdir, tcl_path, input_techfile, d="null"): 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 + """ input_gds = os.path.abspath(input_gds) input_techfile = os.path.abspath(input_techfile) destdir, gdsfile = os.path.split(input_gds) @@ -168,6 +196,13 @@ def convert_to_svg(input_gds, input_techfile, output=None): 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) @@ -183,8 +218,8 @@ def run_inkscape(args): return p.returncode -if __name__ == '__main__': - parser = argparse.ArgumentParser() +def main(argv): + parser = argparse.ArgumentParser(argv[0]) parser.add_argument( 'input_gds', help="Path to the input .gds file" @@ -197,5 +232,10 @@ def run_inkscape(args): '--output-svg', help='Path to the output .svg file' ) - args = parser.parse_args() + args = parser.parse_args(argv[1:]) convert_to_svg(args.input_gds, args.input_tech, args.output_svg) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) From 007ecca8f433fc1e3e4667b2e8c4f36db8bde738 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Mon, 30 Nov 2020 19:55:24 +0100 Subject: [PATCH 4/8] gds_to_svg: moved magic- and Inkscape-related methods to separate modules Signed-off-by: Grzegorz Latosinski --- .../skywater_pdk/gds_to_svg/gds_to_svg.py | 323 ++++++++---------- .../gds_to_svg/generate-gds-svgs.py | 14 +- .../skywater_pdk/tools/__init__.py | 0 .../skywater_pdk/tools/draw.py | 60 ++++ .../skywater_pdk/tools/magic.py | 191 +++++++++++ 5 files changed, 407 insertions(+), 181 deletions(-) create mode 100644 scripts/python-skywater-pdk/skywater_pdk/tools/__init__.py create mode 100644 scripts/python-skywater-pdk/skywater_pdk/tools/draw.py create mode 100644 scripts/python-skywater-pdk/skywater_pdk/tools/magic.py 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 9e60a998c..c9483b8ce 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. - 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) - - -def run_magic(destdir, tcl_path, input_techfile, d="null"): - """ - Runs magic to generate layout files. + Generates TCL script for drawing a cell layout in `magic` tool and creates + a SVG file with the diagram. 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 + 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 + + Returns + ------- + int : 0 if finished successfully, error code from `magic` otherwise """ - 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): + input_gds = os.path.abspath(input_gds) + if output_svg: + output_svg = os.path.abspath(output_svg) + destdir, _ = os.path.split(output_svg) + else: + 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: """ - Converts GDS file to a SVG layout image. + Crops and cleans up GDS diagram. Parameters ---------- - input_gds: path to input GDS file - input_techfile: path to the technology file - output: optional path to the final SVG file + 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 """ - 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 - 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( - ']* style="[^"]*fill-opacity:1;[^"]*"/>', - '', - data - ) - with open(tmp2_svg, 'w') as f: - f.write(data) - # Use inkscape to crop - retcode = run_inkscape([ + with open(input_svg, 'r') as f: + data = f.read() + data = re.sub( + ']* 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) - - -def run_inkscape(args): - """ - Run Inkscape with given arguments. + output_svg + ], + 3) + if result[-1] != 0: + return result[-1] - 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 2ab151e22..dada43266 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 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 000000000..e69de29bb 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 000000000..646d46239 --- /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 000000000..4a42cd75b --- /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) From 5ec66d814efc473c8e9eb4d2d357cb494236e553 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Mon, 30 Nov 2020 19:59:40 +0100 Subject: [PATCH 5/8] gds_to_svg: added magic to the environment.yml Signed-off-by: Grzegorz Latosinski --- environment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/environment.yml b/environment.yml index ee13527ba..47d539164 100644 --- a/environment.yml +++ b/environment.yml @@ -17,9 +17,12 @@ name: skywater-pdk-scripts channels: - symbiflow - defaults +- LiteX-Hub dependencies: - python=3.8 - pip +- magic + # Packages installed from PyPI - pip: - -r file:requirements.txt From c4ff80bb417c69434faf2b9256afe03cb8e9a191 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Tue, 1 Dec 2020 10:03:14 +0100 Subject: [PATCH 6/8] gds_to_svg: Reorganized scripts Signed-off-by: Grzegorz Latosinski --- scripts/python-skywater-pdk/skywater_pdk/cell/__init__.py | 0 .../skywater_pdk/cell/generate/__init__.py | 0 .../generate-gds-svgs.py => cell/generate/layout.py} | 7 +++++-- .../skywater_pdk/{gds_to_svg => }/gds_to_svg.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 scripts/python-skywater-pdk/skywater_pdk/cell/__init__.py create mode 100644 scripts/python-skywater-pdk/skywater_pdk/cell/generate/__init__.py rename scripts/python-skywater-pdk/skywater_pdk/{gds_to_svg/generate-gds-svgs.py => cell/generate/layout.py} (96%) rename scripts/python-skywater-pdk/skywater_pdk/{gds_to_svg => }/gds_to_svg.py (98%) diff --git a/scripts/python-skywater-pdk/skywater_pdk/cell/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/cell/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/python-skywater-pdk/skywater_pdk/cell/generate/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py similarity index 96% rename from scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py rename to scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py index dada43266..bef6bb5b5 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py +++ b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py @@ -21,14 +21,17 @@ Creates cell layouts for cells with GDS files in the skywater-pdk libraries. """ +import sys +import os import argparse from pathlib import Path -import sys import contextlib import traceback import errno -from gds_to_svg import convert_gds_to_svg +sys.path.insert(0, os.path.abspath(__file__ + '../../..')) + +from skywater_pdk.gds_to_svg import convert_gds_to_svg # noqa: E402 def main(argv): 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.py similarity index 98% rename from scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py rename to scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py index c9483b8ce..676614baf 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.py @@ -28,7 +28,7 @@ import re import argparse -sys.path.insert(0, os.path.abspath(__file__ + '../../../')) +sys.path.insert(0, os.path.abspath(__file__ + '..')) from skywater_pdk.tools import magic, draw # noqa: E402 From 2fc472dab36fdf88843e2a4d83a2c83109fd4550 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Tue, 1 Dec 2020 10:42:44 +0100 Subject: [PATCH 7/8] gds_to_svg: Added conda-forge to environment.yml Signed-off-by: Grzegorz Latosinski --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 47d539164..7639f2d36 100644 --- a/environment.yml +++ b/environment.yml @@ -18,6 +18,7 @@ channels: - symbiflow - defaults - LiteX-Hub +- conda-forge dependencies: - python=3.8 - pip From d9f25ae20d862c8369ae5b71dece343c77cd0b23 Mon Sep 17 00:00:00 2001 From: Grzegorz Latosinski Date: Tue, 1 Dec 2020 12:34:52 +0100 Subject: [PATCH 8/8] gds_to_svg: Fixed paths for skywater_pdk in gds_to_svg and layout Signed-off-by: Grzegorz Latosinski --- .../python-skywater-pdk/skywater_pdk/cell/generate/layout.py | 2 +- scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py index bef6bb5b5..8686dedc1 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py +++ b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py @@ -29,7 +29,7 @@ import traceback import errno -sys.path.insert(0, os.path.abspath(__file__ + '../../..')) +sys.path.insert(0, os.path.abspath(__file__ + '/../../../../')) from skywater_pdk.gds_to_svg import convert_gds_to_svg # noqa: E402 diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py index 676614baf..3eb4f550b 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py @@ -28,7 +28,7 @@ import re import argparse -sys.path.insert(0, os.path.abspath(__file__ + '..')) +sys.path.insert(0, os.path.abspath(__file__ + '/../../')) from skywater_pdk.tools import magic, draw # noqa: E402