diff --git a/environment.yml b/environment.yml index ee13527ba..7639f2d36 100644 --- a/environment.yml +++ b/environment.yml @@ -17,9 +17,13 @@ name: skywater-pdk-scripts channels: - symbiflow - defaults +- LiteX-Hub +- conda-forge dependencies: - python=3.8 - pip +- magic + # Packages installed from PyPI - pip: - -r file:requirements.txt 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/cell/generate/layout.py b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py new file mode 100644 index 000000000..8686dedc1 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/cell/generate/layout.py @@ -0,0 +1,120 @@ +#!/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 + +""" +Creates cell layouts for cells with GDS files in the skywater-pdk libraries. +""" + +import sys +import os +import argparse +from pathlib import Path +import contextlib +import traceback +import errno + +sys.path.insert(0, os.path.abspath(__file__ + '/../../../../')) + +from skywater_pdk.gds_to_svg import convert_gds_to_svg # noqa: E402 + + +def main(argv): + 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', + 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_gds_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)) diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py new file mode 100644 index 000000000..3eb4f550b --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg.py @@ -0,0 +1,208 @@ +#!/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 + +""" +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 argparse + +sys.path.insert(0, os.path.abspath(__file__ + '/../../')) + +from skywater_pdk.tools import magic, draw # noqa: E402 + + +def convert_gds_to_svg( + input_gds, + input_techfile, + output_svg=None, + tmp_tcl=None, + keep_temporary_files=False) -> int: + """ + 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 + ---------- + 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 + """ + 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: + """ + 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( + ']* 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", + output_svg + ], + 3) + if result[-1] != 0: + return result[-1] + + 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( + prog=argv[0], + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + 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' + ) + 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:]) + + 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__': + sys.exit(main(sys.argv)) 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)