diff --git a/configuration/checkers.tcl b/configuration/checkers.tcl index 2ae70df73..5c2bee429 100755 --- a/configuration/checkers.tcl +++ b/configuration/checkers.tcl @@ -16,27 +16,22 @@ set ::env(CHECK_ASSIGN_STATEMENTS) 0 set ::env(CHECK_UNMAPPED_CELLS) 1 -# Static timing analysis +# STA set ::env(QUIT_ON_TIMING_VIOLATIONS) 1 set ::env(QUIT_ON_HOLD_VIOLATIONS) 1 set ::env(QUIT_ON_SETUP_VIOLATIONS) 1 -# Floor Planning - - -# Placement - # Routing set ::env(QUIT_ON_TR_DRC) 1 set ::env(QUIT_ON_LONG_WIRE) 0 # Magic -# This is disabled by default for now until we are 100% sure we want to make this -# shift in flow dynamics, as it will affect the current benchmarks. set ::env(QUIT_ON_MAGIC_DRC) 1 set ::env(QUIT_ON_ILLEGAL_OVERLAPS) 1 -# NetGen -# This is disabled by default as it's the stage before the last, so why not do the last stage anyways. +# Netgen set ::env(QUIT_ON_LVS_ERROR) 1 + +# Klayout +set ::env(QUIT_ON_XOR_ERROR) 0 diff --git a/configuration/general.tcl b/configuration/general.tcl index bf4357503..d1b5ac495 100755 --- a/configuration/general.tcl +++ b/configuration/general.tcl @@ -78,6 +78,7 @@ set ::env(RUN_KLAYOUT) 1 set ::env(RUN_KLAYOUT_DRC) 0 set ::env(KLAYOUT_XOR_GDS) 1 set ::env(KLAYOUT_XOR_XML) 1 +set ::env(KLAYOUT_XOR_THREADS) 1 set ::env(TAKE_LAYOUT_SCROT) 0 set ::env(KLAYOUT_DRC_KLAYOUT_GDS) 0 set ::env(RUN_KLAYOUT_XOR) 1 diff --git a/dependencies/tool_metadata.yml b/dependencies/tool_metadata.yml index 1baabcbb1..6c82755ae 100644 --- a/dependencies/tool_metadata.yml +++ b/dependencies/tool_metadata.yml @@ -53,7 +53,7 @@ make PREFIX=$PREFIX install - name: klayout repo: https://github.com/KLayout/klayout - commit: 428d0fe8c941faece4eceebc54170cc04d916c03 + commit: 8bed8bcc3ca19f7e1a810815541977fd16bc1db5 build: '' in_install: false - name: openroad_app diff --git a/docs/source/reference/configuration.md b/docs/source/reference/configuration.md index e18fb1dfe..74c51475d 100644 --- a/docs/source/reference/configuration.md +++ b/docs/source/reference/configuration.md @@ -297,6 +297,7 @@ These variables worked initially, but they were too sky130 specific and will be | `KLAYOUT_XOR_GDS` | If `RUN_KLAYOUT_XOR` is enabled, this will enable producing a GDS output from the XOR along with it's PNG export. 1 = Enabled, 0 = Disabled
(Default: `1`)| | `KLAYOUT_XOR_XML` | If `RUN_KLAYOUT_XOR` is enabled, this will enable producing an XML output from the XOR. 1 = Enabled, 0 = Disabled
(Default: `1`)| | `TAKE_LAYOUT_SCROT` | Enables running KLayout to take a PNG screenshot of the produced layout (currently configured to run on the results of each stage).1 = Enabled, 0 = Disabled
(Default: `0`)| +| `KLAYOUT_XOR_THREADS` | Specifies number of threads used in klayout xor check
(Default: `1`)| | `DIODE_INSERTION_STRATEGY` | Specifies the insertion strategy of diodes to be used in the flow. | | | 0: No diode insertion. | | | 1: Spray diodes. | @@ -310,6 +311,8 @@ These variables worked initially, but they were too sky130 specific and will be | `MAGIC_CONVERT_DRC_TO_RDB` | **Removed: Will always run** Specifies whether or not generate a Calibre RDB out of the magic.drc report. Result is saved in `/results/magic/`. 1=enabled 0=disabled
Default: `1`| | `TEST_MISMATCHES` | **Removed: See `./flow.tcl -test_mismatches`** Test for mismatches between the OpenLane tool versions and the current environment. `all` tests all mismatches. `tools` tests all except the PDK. `pdk` only tests the PDK. `none` disables the check.
(Default: `all`) | | `QUIT_ON_MISMATCHES` | **Removed: See `./flow.tcl -ignore_mismatches`** Whether to halt the flow execution or not if mismatches are found. (Default: `1`) | +| `KLAYOUT_XOR_GDS` | **Removed: XML always generated** If `RUN_KLAYOUT_XOR` is enabled, this will enable producing a GDS output from the XOR along with it's PNG export. 1 = Enabled, 0 = Disabled
(Default: `1`)| +| `KLAYOUT_XOR_XML` | **Removed: XML always generated** If `RUN_KLAYOUT_XOR` is enabled, this will enable producing an XML output from the XOR. 1 = Enabled, 0 = Disabled
(Default: `1`)| ### Checkers diff --git a/klayoutrc b/klayoutrc new file mode 100644 index 000000000..7d2c67099 --- /dev/null +++ b/klayoutrc @@ -0,0 +1,5 @@ + + + true + only-top-level-shown-by-default=3 + \ No newline at end of file diff --git a/scripts/klayout/Readme.md b/scripts/klayout/Readme.md new file mode 100644 index 000000000..597b68692 --- /dev/null +++ b/scripts/klayout/Readme.md @@ -0,0 +1,2 @@ +## Reference +https://www.klayout.org/downloads/pymod/doc-qt5/code/index.html \ No newline at end of file diff --git a/scripts/klayout/def2gds.py b/scripts/klayout/def2gds.py deleted file mode 100644 index 0f64fccc9..000000000 --- a/scripts/klayout/def2gds.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2021 Efabless Corporation -# -# 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 -# -# http://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. - -# Original Copyright Follows -# -# BSD 3-Clause License -# -# Copyright (c) 2018, The Regents of the University of California -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import pya - -import re -import copy -import json - -print( - """ -Input: {in_def} -Output: {out_gds} -Design: {design_name} -Technology File: {tech_file} -GDS File List: {in_gds} -LEF File: {lef_file} -""".format( - in_def=in_def, - design_name=design_name, - tech_file=tech_file, - in_gds=in_gds.split(), - lef_file=lef_file, - out_gds=out_gds, - ) -) - -try: - # Expand layers in json - def expand_cfg_layers(cfg): - layers = cfg["layers"] - expand = [layer for layer in layers if "layers" in layers[layer]] - for layer in expand: - for i, (name, num) in enumerate( - zip(layers[layer]["names"], layers[layer]["layers"]) - ): - new_layer = copy.deepcopy(layers[layer]) - del new_layer["names"] - new_layer["name"] = name - del new_layer["layers"] - new_layer["layer"] = num - layers[name] = new_layer - del layers[layer] - - def read_cfg(): - print("INFO: Reading config file: " + config_file) - with open(config_file, "r") as f: - cfg = json.load(f) - - expand_cfg_layers(cfg) - cfg = cfg["layers"] # ignore the rest - - # Map gds layers & datatype to KLayout indices - # These are arrays for the different mask numbers - for layer, vals in cfg.items(): - layer = vals["layer"] - for key in ("opc", "non-opc"): - if key not in vals: - continue - data = vals[key] - if isinstance(data["datatype"], int): - data["datatype"] = [data["datatype"]] # convert to array - data["klayout"] = [ - main_layout.find_layer(layer, datatype) - for datatype in data["datatype"] - ] - - return cfg - - # match a line like: - # - LAYER M2 + MASK 2 + OPC RECT ( 3000 3000 ) ( 5000 5000 ) ; - rect_pat = re.compile( - r""" - \s*\-\ LAYER\ (?P\S+) # The layer name - (?: # Non-capturing group - \s+\+\ MASK\ (?P\d+) # Mask, None if absent - )? - (?P # OPC, None if absent - \s+\+\ OPC - )? - \s+RECT\ - \(\ (?P\d+)\ (?P\d+)\ \)\ # rect lower-left pt - \(\ (?P\d+)\ (?P\d+)\ \)\ ; # rect upper-right pt - """, - re.VERBOSE, - ) - - def read_fills(top): - if config_file == "": - print("WARNING: no fill config file specified") - return - # KLayout doesn't support FILL in DEF so we have to side load them :( - cfg = read_cfg() - in_fills = False - units = None - with open(in_def) as fp: - for line in fp: - if in_fills: - if re.match("END FILLS", line): - break # done with fills; don't care what follows - m = re.match(rect_pat, line) - if not m: - raise Exception("Unrecognized fill: " + line) - opc_type = "opc" if m.group("opc") else "non-opc" - mask = m.group("mask") - if not mask: # uncolored just uses first entry - mask = 0 - else: - mask = int(mask) - 1 # DEF is 1-based indexing - layer = cfg[m.group("layer")][opc_type]["klayout"][mask] - xlo = int(m.group("xlo")) / units - ylo = int(m.group("ylo")) / units - xhi = int(m.group("xhi")) / units - yhi = int(m.group("yhi")) / units - top.shapes(layer).insert(pya.DBox(xlo, ylo, xhi, yhi)) - elif re.match("FILLS \d+ ;", line): - in_fills = True - elif not units: - m = re.match("UNITS DISTANCE MICRONS (\d+)", line) - if m: - units = float(m.group(1)) - - # Load technology file - tech = pya.Technology() - tech.load(tech_file) - layoutOptions = tech.load_layout_options - layoutOptions.lefdef_config.macro_resolution_mode = 1 - layoutOptions.lefdef_config.lef_files = [lef_file] - - # Load def file - main_layout = pya.Layout() - main_layout.read(in_def, layoutOptions) - - # Clear cells - top_cell_index = main_layout.cell(design_name).cell_index() - - print("[INFO] Clearing cells...") - for i in main_layout.each_cell(): - if i.cell_index() != top_cell_index: - if not i.name.startswith("VIA"): - # print("\t" + i.name) - i.clear() - - # Load in the gds to merge - print("[INFO] Merging GDS files...") - for gds in in_gds.split(): - print("\t{0}".format(gds)) - main_layout.read(gds) - - # Copy the top level only to a new layout - print("[INFO] Copying toplevel cell '{0}'".format(design_name)) - top_only_layout = pya.Layout() - top_only_layout.dbu = main_layout.dbu - top = top_only_layout.create_cell(design_name) - top.copy_tree(main_layout.cell(design_name)) - - read_fills(top) - - print("[INFO] Checking for missing GDS...") - missing_gds = False - for i in top_only_layout.each_cell(): - if i.is_empty(): - missing_gds = True - print( - "[ERROR] LEF Cell '{0}' has no matching GDS cell. Cell will be empty".format( - i.name - ) - ) - - if not missing_gds: - print("[INFO] All LEF cells have matching GDS cells") - - if seal_gds: - top_cell = top_only_layout.top_cell() - - print("[INFO] Reading seal GDS file...") - print("\t{0}".format(seal_gds)) - top_only_layout.read(seal_gds) - - for cell in top_only_layout.top_cells(): - if cell != top_cell: - print( - "[INFO] Merging '{0}' as child of '{1}'".format( - cell.name, top_cell.name - ) - ) - top.insert(pya.CellInstArray(cell.cell_index(), pya.Trans())) - - # Write out the GDS - print("[INFO] Writing out GDS '{0}'".format(out_gds)) - top_only_layout.write(out_gds) - print("[INFO] Done.") - pya.Application.instance().exit(0) -except Exception as e: - print(e) - pya.Application.instance().exit(1) diff --git a/scripts/klayout/mv_shapes.py b/scripts/klayout/mv_shapes.py deleted file mode 100644 index 15eda01d5..000000000 --- a/scripts/klayout/mv_shapes.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -import pya -from time import sleep -import os - -try: - if output_layout == "": - raise NameError - _output_layout = output_layout -except NameError: - _output_layout = input_layout - print( - "Warning: output_layout was not provided; will do the modifications in place!" - ) - print("Hit CTRL-C to cancel...") - sleep(3) - -print("Starting...") -app = pya.Application.instance() -win = app.main_window() - -# Load technology file -tech = pya.Technology() -tech.load(tech_file) -layoutOptions = tech.load_layout_options - -# Load def/gds file in the main window -cell_view = win.load_layout(input_layout, layoutOptions, 0) -layout_view = cell_view.view() -layout_view.load_layer_props(os.path.splitext(tech_file)[0] + ".lyp") -layout_view.max_hier() - -# gets the corresponding layout object -layout = cell_view.layout() - -# gets the cell to change is "INV2X" -# cell = layout.cell("Active_area") -cell = cell_view.cell - -# finds source layer -layer, purpose = source_layer.split("/") -assert layer and purpose -_source_layer = layout.layer(int(layer), int(purpose)) - -# finds (or creates) target layer -layer, purpose = target_layer.split("/") -assert layer and purpose -_target_layer = layout.layer(int(layer), int(purpose)) - -layout.move_layer(_source_layer, _target_layer) - -layout.write(_output_layout) - -print("Successfully wrote", _output_layout) - -app.exit(0) diff --git a/scripts/klayout/mv_shapes.sh b/scripts/klayout/mv_shapes.sh deleted file mode 100644 index 6e58e444e..000000000 --- a/scripts/klayout/mv_shapes.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -: ${1?"Usage: $0 file.gds src_layer/src_purpose targ_layer/targ_purpose [output.gds]"} -: ${2?"Usage: $0 file.gds src_layer/src_purpose targ_layer/targ_purpose [output.gds]"} -: ${3?"Usage: $0 file.gds src_layer/src_purpose targ_layer/targ_purpose [output.gds]"} -: ${PDK_ROOT?"You need to export PDK_ROOT"} -TECH=${TECH:-sky130A} - -klayout -b\ - -rm $(dirname $0)/mv_shapes.py\ - -rd input_layout=$1\ - -rd tech_file=$PDK_ROOT/$TECH/libs.tech/klayout/$TECH.lyt\ - -rd source_layer=$2\ - -rd target_layer=$3\ - -rd output_layout=$4 diff --git a/scripts/klayout/open_design.py b/scripts/klayout/open_design.py old mode 100644 new mode 100755 index 88039735f..45133b658 --- a/scripts/klayout/open_design.py +++ b/scripts/klayout/open_design.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2020-2022 Efabless Corporation # Copyright 2021 The American University in Cairo and the Cloud V Project # @@ -15,24 +16,70 @@ import os import sys -import pya - -app = pya.Application.instance() +from typing import TYPE_CHECKING try: - win = app.main_window() - - layout = os.getenv("LAYOUT") - if layout is None: - raise Exception("LAYOUT environment variable is not set.") - - pdk_root = os.getenv("PDK_ROOT") - if pdk_root is None: - raise Exception("PDK_ROOT environment variable is not set.") + import pya +except ImportError: + import click + + @click.command() + @click.option( + "-l", + "--input-lef", + required=os.getenv("MERGED_LEF") is None, + default=os.getenv("MERGED_LEF"), + ) + @click.option( + "-T", + "--tech-file", + "lyt", + required=os.getenv("KLAYOUT_TECH") is None, + default=os.getenv("KLAYOUT_TECH"), + help="KLayout .lyt file", + ) + @click.option( + "-P", + "--props-file", + "lyp", + required=os.getenv("KLAYOUT_PROPERTIES") is None, + default=os.getenv("KLAYOUT_PROPERTIES"), + help="KLayout .lyp file", + ) + @click.argument("input_def") + def open_design( + input_lef, + lyt, + lyp, + input_def, + ): + args = [ + "klayout", + "-rm", + __file__, + "-rd", + f"input_lef={os.path.abspath(input_lef)}", + "-rd", + f"tech_file={os.path.abspath(lyt)}", + "-rd", + f"props_file={os.path.abspath(lyp)}", + "-rd", + f"input_def={os.path.abspath(input_def)}", + ] + os.execlp("klayout", *args) + + if __name__ == "__main__": + open_design() + +if TYPE_CHECKING: + # Dummy data for type-checking + input_def: str = "" + tech_file: str = "" + props_file: str = "" + input_lef: str = "" - pdk_name = os.getenv("PDK") - if pdk_name is None: - raise Exception("PDK environment variable is not set.") +try: + main_window = pya.Application.instance().main_window() # Relative to the layout path, ':' delimited. If not provided, all LEFs # in the same folder as the layout will be loaded. @@ -40,27 +87,17 @@ use_explicitly_listed_lefs = explicitly_listed_lefs_raw is not None - tech_file_path = os.path.join( - pdk_root, pdk_name, "libs.tech", "klayout", f"{pdk_name}.lyt" - ) - tech = pya.Technology() - tech.load(tech_file_path) + tech.load(tech_file) layout_options = tech.load_layout_options - layout_options.keep_other_cells = True - - layout_options = tech.load_layout_options layout_options.lefdef_config.macro_resolution_mode = 1 + layout_options.lefdef_config.read_lef_with_def = False + layout_options.lefdef_config.lef_files = [input_lef] - if use_explicitly_listed_lefs: - explicitly_listed_lefs = explicitly_listed_lefs_raw.split(":") - layout_options.lefdef_config.read_lef_with_def = False - layout_options.lefdef_config.lef_files = explicitly_listed_lefs - - cell_view = win.load_layout(layout, layout_options, 0) - + cell_view = main_window.load_layout(input_def, layout_options, 0) + exit(0) except Exception as e: print(e, file=sys.stderr) - app.exit(-1) + exit(-1) diff --git a/scripts/klayout/open_design_cmd.py b/scripts/klayout/open_design_cmd.py deleted file mode 100644 index 69ccf3dce..000000000 --- a/scripts/klayout/open_design_cmd.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2020-2022 Efabless Corporation -# -# 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 -# -# http://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. - -import os -import click -import subprocess - - -@click.command("open_design") -@click.option( - "-l", - "--input-lef", - default=os.getenv("MERGED_LEF"), - help="Input merged technology/cells LEF file", -) -@click.option( - "-P", - "--pdk-root", - default=os.getenv("PDK_ROOT"), - required=not os.getenv("PDK_ROOT"), - help="PDK Root", -) -@click.option("-p", "--pdk", default="sky130A", help="Name of the PDK") -@click.argument("input_def") -def open_design(input_lef, pdk_root, pdk, input_def): - """ - Opens a design in KLayout. - """ - dir = os.path.dirname(__file__) - klayout_script_path = os.path.join(dir, "open_design.py") - env = os.environ.copy() - - env["EXPLICITLY_LISTED_LEFS"] = input_lef - env["PDK_ROOT"] = pdk_root - env["PDK"] = pdk - env["LAYOUT"] = input_def - - subprocess.check_call( - [ - "klayout", - "-rm", - klayout_script_path, - ], - env=env, - ) - - -if __name__ == "__main__": - open_design() diff --git a/scripts/klayout/screenshot_layout.py b/scripts/klayout/screenshot_layout.py new file mode 100755 index 000000000..ffa722a3c --- /dev/null +++ b/scripts/klayout/screenshot_layout.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright 2020-2022 Efabless Corporation +# +# 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 +# +# http://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. + +import os +from typing import TYPE_CHECKING + +try: + import pya +except ImportError: + import click + + @click.command() + @click.option("-o", "--output", required=True) + @click.option( + "-l", + "--input-lef", + required=os.getenv("MERGED_LEF") is None, + default=os.getenv("MERGED_LEF"), + ) + @click.option( + "-T", + "--tech-file", + "lyt", + required=os.getenv("KLAYOUT_TECH") is None, + default=os.getenv("KLAYOUT_TECH"), + help="KLayout .lyt file", + ) + @click.option( + "-P", + "--props-file", + "lyp", + required=os.getenv("KLAYOUT_PROPERTIES") is None, + default=os.getenv("KLAYOUT_PROPERTIES"), + help="KLayout .lyp file", + ) + @click.argument("input_def") + def screenshot_layout( + output, + input_lef, + lyt, + lyp, + input_def, + ): + args = [ + # "xvfb-run", + # "-a", + "klayout", + "-rm", + __file__, + "-rd", + f"out_png={os.path.abspath(output)}", + "-rd", + f"lef_file={os.path.abspath(input_lef)}", + "-rd", + f"tech_file={os.path.abspath(lyt)}", + "-rd", + f"props_file={os.path.abspath(lyp)}", + "-rd", + f"in_def={os.path.abspath(input_def)}", + ] + os.execlp("klayout", *args) + + if __name__ == "__main__": + screenshot_layout() + +if TYPE_CHECKING: + # Dummy data for type-checking + in_def: str = "" + out_png: str = "" + tech_file: str = "" + props_file: str = "" + lef_file: str = "" + +try: + WIDTH = 2048 + HEIGHT = 2048 + + main_window = pya.Application.instance().main_window() + + # Load technology file + print(f"[INFO] Reading tech file: '{str(tech_file)}'...") + tech = pya.Technology() + tech.load(tech_file) + + layout_options = tech.load_layout_options + layout_options.keep_other_cells = True + layout_options.lefdef_config.macro_resolution_mode = 1 + layout_options.lefdef_config.read_lef_with_def = False + layout_options.lefdef_config.lef_files = [lef_file] + + # Load def file in the main window + print(f"[INFO] Reading layout file: '{str(in_def)}'...") + cell_view = main_window.load_layout(in_def, layout_options, 0) + layout_view = cell_view.view() + layout_view.load_layer_props(props_file) + layout_view.max_hier() + + # Hide layers with these purposes + hidden_purposes = [0, 4, 5] + + li = layout_view.begin_layers() + while not li.at_end(): + lp = li.current() + if lp.source_datatype in hidden_purposes: + new_lp = lp.dup() + new_lp.visible = False + layout_view.set_layer_properties(li, new_lp) + + li.next() + + print(f"[INFO] Writing out screenshot to '{out_png}'...") + layout_view.save_image(out_png, WIDTH, HEIGHT) + print("Done.") + exit(0) +except Exception as e: + print(e) + exit(-1) diff --git a/scripts/klayout/scrotLayout.py b/scripts/klayout/scrotLayout.py deleted file mode 100644 index fe5f1dc7e..000000000 --- a/scripts/klayout/scrotLayout.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -import pya -import re -import os - -WIDTH = 2048 -HEIGHT = 2048 - -app = pya.Application.instance() -win = app.main_window() - -# Load technology file -print("[INFO] Reading tech file: " + str(tech_file)) -tech = pya.Technology() -tech.load(tech_file) - -layoutOptions = tech.load_layout_options - -# Load def file in the main window -print("[INFO] Reading Layout file: " + str(input_layout)) -cell_view = win.load_layout(input_layout, layoutOptions, 0) -layout_view = cell_view.view() - -layout_view.load_layer_props(os.path.splitext(tech_file)[0] + ".lyp") - -layout_view.max_hier() -# layout_view.clear_layers() - -# Hide layers with these purposes -hidden_purposes = [0, 4, 5] - -li = layout_view.begin_layers() -while not li.at_end(): - lp = li.current() - if lp.source_datatype in hidden_purposes: - new_lp = lp.dup() - new_lp.visible = False - layout_view.set_layer_properties(li, new_lp) - - li.next() - -print("[INFO] Writing out PNG screenshot '{0}'".format(input_layout + ".png")) -layout_view.save_image(input_layout + ".png", WIDTH, HEIGHT) -print("Done") -app.exit(0) diff --git a/scripts/klayout/scrotLayout.sh b/scripts/klayout/scrotLayout.sh deleted file mode 100644 index d83378397..000000000 --- a/scripts/klayout/scrotLayout.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -set -e - -: ${1?"Usage: $0 tech_file input"} -: ${2?"Usage: $0 tech_file input"} - -echo "Using Techfile: $1" -echo "Using layout file: $2" - -# The -a here is necessary to handle race conditions. -# This limits the max number of possible jobs to 100. -xvfb-run -a klayout -z \ - -rd input_layout=$2 \ - -rd tech_file=$1 \ - -rm $(dirname $0)/scrotLayout.py - -exit 0 diff --git a/scripts/klayout/stream_out.py b/scripts/klayout/stream_out.py new file mode 100755 index 000000000..65c1e18f1 --- /dev/null +++ b/scripts/klayout/stream_out.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021-2022 Efabless Corporation +# +# 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 +# +# http://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. + +# Original Copyright Follows +# +# BSD 3-Clause License +# +# Copyright (c) 2018, The Regents of the University of California +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import os +from typing import TYPE_CHECKING, Optional + +try: + import pya +except ImportError: + import click + + @click.command() + @click.option("-o", "--output", required=True) + @click.option( + "-l", + "--input-lef", + required=os.getenv("MERGED_LEF") is None, + default=os.getenv("MERGED_LEF"), + ) + @click.option( + "-T", + "--tech-file", + "lyt", + required=os.getenv("KLAYOUT_TECH") is None, + default=os.getenv("KLAYOUT_TECH"), + help="KLayout .lyt file", + ) + @click.option( + "-P", + "--props-file", + "lyp", + required=os.getenv("KLAYOUT_PROPERTIES") is None, + default=os.getenv("KLAYOUT_PROPERTIES"), + help="KLayout .lyp file", + ) + @click.option("-w", "--with-gds-file", "input_gds_files", multiple=True, default=[]) + @click.option("-s", "--seal-gds-file", default=None) + @click.option("-t", "--top", required=True, help="Name of the design/top module") + @click.argument("input_def") + def stream_out( + output, + input_lef, + lyt, + lyp, + input_gds_files, + seal_gds_file, + top, + input_def, + ): + args = [ + "klayout", + "-b", + "-rm", + __file__, + "-rd", + f"out_gds={output}", + "-rd", + f"lef_file={input_lef}", + "-rd", + f"tech_file={lyt}", + "-rd", + f"props_file={lyp}", + "-rd", + f"design_name={top}", + "-rd", + f"in_def={input_def}", + "-rd", + f"in_gds={';'.join(list(input_gds_files))}", + "-rd", + f"seal_gds={seal_gds_file}", + ] + os.execlp("klayout", *args) + + if __name__ == "__main__": + stream_out() + + +if TYPE_CHECKING: + # Dummy data for type-checking + in_def: str = "" + out_gds: str = "" + design_name: str = "" + tech_file: str = "" + props_file: str = "" + in_gds: str = "" + lef_file: str = "" + seal_gds: Optional[str] = "" + +if seal_gds == "None": + seal_gds = None + + +try: + # Load technology file + tech = pya.Technology() + tech.load(tech_file) + layout_options = tech.load_layout_options + layout_options.lefdef_config.macro_resolution_mode = 1 + layout_options.lefdef_config.lef_files = [lef_file] + + # Load def file + main_layout = pya.Layout() + # main_layout.load_layer_props(props_file) + main_layout.read(in_def, layout_options) + + # Clear cells + top_cell_index = main_layout.cell(design_name).cell_index() + + print("[INFO] Clearing cells...") + for i in main_layout.each_cell(): + if i.cell_index() != top_cell_index: + if not i.name.startswith("VIA"): + i.clear() + + # Load in the gds to merge + print("[INFO] Merging GDS files...") + for gds in in_gds.split(";"): + print(f"\t{gds}") + main_layout.read(gds) + + # Copy the top level only to a new layout + print(f"[INFO] Copying top level cell '{design_name}'...") + top_only_layout = pya.Layout() + top_only_layout.dbu = main_layout.dbu + top = top_only_layout.create_cell(design_name) + top.copy_tree(main_layout.cell(design_name)) + + print("[INFO] Checking for missing GDS...") + missing_gds = False + for i in top_only_layout.each_cell(): + if i.is_empty(): + missing_gds = True + print(f"[ERROR] LEF Cell '{i.name}' has no matching GDS cell.") + + if missing_gds: + raise Exception("One or more cell GDS files are missing.") + else: + print("[INFO] All LEF cells have matching GDS cells.") + + if seal_gds is not None: + top_cell = top_only_layout.top_cell() + + print(f"[INFO] Reading seal GDS file '{seal_gds}'...") + top_only_layout.read(seal_gds) + + for cell in top_only_layout.top_cells(): + if cell != top_cell: + print(f"[INFO] Merging '{cell.name}' as child of '{top_cell.name}'...") + top.insert(pya.CellInstArray(cell.cell_index(), pya.Trans())) + + # Write out the GDS + print(f"[INFO] Writing out GDS '{out_gds}'...") + top_only_layout.write(out_gds) + print("[INFO] Done.") + pya.Application.instance().exit(0) +except Exception as e: + print(e) + pya.Application.instance().exit(1) diff --git a/scripts/klayout/xor.drc b/scripts/klayout/xor.drc old mode 100644 new mode 100755 index 6caee9162..68f7a1f2a --- a/scripts/klayout/xor.drc +++ b/scripts/klayout/xor.drc @@ -1,28 +1,75 @@ -# A general XOR script -# (https://www.klayout.de/forum/discussion/100/xor-vs-diff-tool) -# This script uses KLayout's DRC language to implement a generic -# XOR between two layouts. The name of the layouts is given -# in $a and $b. +#!/usr/bin/env ruby +# A general XOR script from: https://www.klayout.de/forum/discussion/100/xor-vs-diff-tool -# For layout-to-layout XOR with multiple cores, run this script with -# ./klayout -r xor.drc -rd thr=NUM_CORES -rd top_cell=TOP_CELL_NAME -rd a=a.gds -rd b=b.gds -rd ol=xor.gds -zz -# (replace NUM_CORES by the desired number of cores to utilize +# This script uses KLayout's Ruby-based DRC language to implement a generic +# XOR between two layouts. The names of the layouts are given in global variables +# $a and $b. + +if !defined?(RBA) + require 'optparse' + + options = { + :rpt_out => "/dev/null" + } + optparse = OptionParser.new do |opts| + opts.banner = "Usage: #{File.basename($0)} [options] " + + opts.on("-o", "--output OUTPUT", "Klayout RDB (.xml) output file (required)") do |rdb_out| + options[:rdb_out] = rdb_out + end + opts.on("-R", "--report REPORT_FILE", "Text file (default: #{options[:rpt_out]})") do |rpt_out| + options[:rpt_out] = rpt_out + end + # opts.on("-O", "--gds-out OUTPUT", "GDS output file (required)") do |gds_out| + # options[:gds_out] = gds_out + # end + opts.on("-t", "--top TOP_CELL", "Top cell name (required)") do |top_cell| + options[:top_cell] = top_cell + end + end + optparse.parse! + + if [options[:rdb_out], options[:top_cell]].include?(nil) + puts optparse.help + exit 64 + end + + args = [ + "klayout", "-b", + "-r", $0, + "-rd", "top_cell=#{options[:top_cell]}", + "-rd", "a=#{ARGV[0]}", + "-rd", "b=#{ARGV[1]}", + "-rd", "jobs=1", + "-rd", "rdb_out=#{File.absolute_path(options[:rdb_out])}", + "-rd", "rpt_out=#{File.absolute_path(options[:rpt_out])}", + # "-rd", "gds_out=#{options[:gds_out]}", + ] + puts "Running: '#{args.join(" ")}'..." + exec *args + +end -# enable timing output verbose -# set up input a +# Set up inputs a = source($a, $top_cell) - -# set up input b b = source($b, $top_cell) -$o && $ext != "gds" && report("XOR #{$a} vs. #{$b}", $o) -$ol && $ext == "gds" && target($ol, $co || "XOR") +# Set up output +# target($gds_out, "XOR") +report("XOR #{$a} vs. #{$b}", $rdb_out) + +def write_data(xor_data, layer_info) + # xor_data.output(layer_info.layer, layer_info.datatype, layer_info.name) + xor_data.output(layer_info.to_s, "XOR data for layer #{layer_info.to_s}") +end -$thr && threads($thr) || threads(2) +# Run XOR +$jobs = $jobs.to_i +threads($jobs) unless $jobs <= 1 -# collect all common layers +## Collect all common layers layers = {} [ a.layout, b.layout ].each do |ly| ly.layer_indices.each do |li| @@ -31,12 +78,21 @@ layers = {} end end -# perform the XOR's -layers.keys.sort.each do |l| - i = layers[l] - info("--- Running XOR for #{l} ---") - x = a.input(l) ^ b.input(l) - info("XOR differences: #{x.data.size}") - $o && $ext != "gds" && x.output(l, "XOR results for layer #{l} #{i.name}") - $ol && $ext == "gds" && x.output(i.layer, i.datatype, i.name) +## Perform per-layer XOR +total_xor_differences = 0 +layers.keys.sort.each do |layer_name| + info "--- Running XOR for layer #{layer_name} ---" + + layer_info = layers[layer_name] + xor_data = a.input(layer_name) ^ b.input(layer_name) + total_xor_differences += xor_data.data.size + info "XOR differences: #{xor_data.data.size}" + + write_data xor_data, layer_info end + +info "---" +info "Total XOR differences: #{total_xor_differences}" +File.open($rpt_out, "w") do |f| + f.puts "Total XOR differences = #{total_xor_differences}" +end \ No newline at end of file diff --git a/scripts/klayout/xor.sh b/scripts/klayout/xor.sh deleted file mode 100755 index b71ef7987..000000000 --- a/scripts/klayout/xor.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -: ${1?"Usage: $0 file1.gds file2.gds output.gds|markers.xml"} -: ${2?"Usage: $0 file1.gds file2.gds output.gds|markers.xml"} -: ${3?"Usage: $0 file1.gds file2.gds output.gds|markers.xml"} -: ${4?"Usage: $0 file1.gds file2.gds output.gds|markers.xml"} - - -echo "First Layout: $1" -echo "Second Layout: $2" -echo "Design Name: $3" -echo "Output GDS will be: $4" - -klayout -b\ - -r $(dirname $0)/xor.drc \ - -rd top_cell=$3 \ - -rd a=$1 \ - -rd b=$2 \ - -rd thr=$(nproc) \ - -rd ol=$4 \ - -rd o=$4 \ - -rd ext=${4##*.} diff --git a/scripts/parse_klayout_xor_log.py b/scripts/parse_klayout_xor_log.py deleted file mode 100644 index db3fcb0a4..000000000 --- a/scripts/parse_klayout_xor_log.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2020 Efabless Corporation -# -# 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 -# -# http://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. - -import argparse -import re - -parser = argparse.ArgumentParser( - description="extracts the total xor differnces from an xor log" -) - -parser.add_argument("--log_file", "-l", required=True, help="log file") - -parser.add_argument( - "--output", "-o", required=True, help="output file to store results" -) - -args = parser.parse_args() -log_file_name = args.log_file -out_file_name = args.output - -string = "XOR differences:" -pattern = re.compile(r"\s*%s\s*([\d+]+)" % string) -tot_cnt = 0 -with open(log_file_name, "r") as f: - for line in f: - m = pattern.match(line) - if m: - tot_cnt += int(m.group(1)) - -outFileOpener = open(out_file_name, "w") -outFileOpener.write("Total XOR differences = " + str(tot_cnt)) -outFileOpener.close() diff --git a/scripts/tcl_commands/checkers.tcl b/scripts/tcl_commands/checkers.tcl index c43155691..40f7d7e60 100755 --- a/scripts/tcl_commands/checkers.tcl +++ b/scripts/tcl_commands/checkers.tcl @@ -270,6 +270,24 @@ proc quit_on_lvs_error {args} { } } +proc quit_on_xor_error {args} { + if { [info exists ::env(QUIT_ON_XOR_ERROR)] && $::env(QUIT_ON_XOR_ERROR) } { + set options { + {-log required} + } + parse_key_args "quit_on_xor_error" args arg_values $options + set checker [catch {exec grep -E -o "Total XOR differences = 0" $arg_values(-log)} error] + + if { $checker != 0 } { + set log_relative [relpath . $arg_values(-log)] + puts_err "There are XOR differences in the design: See '$log_relative' for details." + flow_fail + } else { + puts_info "No XOR differences between KLayout and Magic gds." + } + } +} + proc quit_on_illegal_overlaps {args} { if { [info exists ::env(QUIT_ON_ILLEGAL_OVERLAPS)] && $::env(QUIT_ON_ILLEGAL_OVERLAPS) } { set options { diff --git a/scripts/tcl_commands/klayout.tcl b/scripts/tcl_commands/klayout.tcl index 7dad9cc7f..04f89ce8c 100755 --- a/scripts/tcl_commands/klayout.tcl +++ b/scripts/tcl_commands/klayout.tcl @@ -28,17 +28,19 @@ proc run_klayout {args} { set cells_gds $::env(GDS_FILES) } + set gds_file_arg "" + foreach gds_file "$cells_gds $gds_files_in" { + set gds_file_arg "$gds_file_arg --with-gds-file $gds_file" + } set klayout_out $::env(signoff_results)/$::env(DESIGN_NAME).klayout.gds - try_catch klayout -b\ - -rm $::env(SCRIPTS_DIR)/klayout/def2gds.py\ - -rd out_gds=$klayout_out\ - -rd tech_file=$::env(KLAYOUT_TECH)\ - -rd design_name=$::env(DESIGN_NAME)\ - -rd in_def=$::env(CURRENT_DEF)\ - -rd "in_gds=$cells_gds $gds_files_in"\ - -rd "config_file="\ - -rd "seal_gds="\ - -rd lef_file=$::env(MERGED_LEF)\ + try_catch python3 $::env(SCRIPTS_DIR)/klayout/stream_out.py\ + --output $klayout_out\ + --tech-file $::env(KLAYOUT_TECH)\ + --props-file $::env(KLAYOUT_PROPERTIES)\ + --top $::env(DESIGN_NAME)\ + {*}$gds_file_arg \ + --input-lef $::env(MERGED_LEF)\ + $::env(CURRENT_DEF)\ |& tee $::env(TERMINAL_OUTPUT) $log @@ -140,36 +142,28 @@ proc run_klayout_gds_xor {args} { set_if_unset arg_values(-layout2) $::env(signoff_results)/$::env(DESIGN_NAME).klayout.gds set_if_unset arg_values(-output_xml) $::env(signoff_reports)/$::env(DESIGN_NAME).xor.xml set_if_unset arg_values(-output_gds) $::env(signoff_reports)/$::env(DESIGN_NAME).xor.gds + if { [file exists $arg_values(-layout1)]} { if { [file exists $arg_values(-layout2)] } { increment_index TIMER::timer_start set log [index_file $::env(signoff_logs)/xor.log] + set report [index_file $::env(signoff_reports)/xor.rpt] + set db [index_file $::env(signoff_reports)/xor.xml] puts_info "Running XOR on the layouts using KLayout (log: [relpath . $log])..." - - if { $::env(KLAYOUT_XOR_GDS) } { - try_catch bash $::env(SCRIPTS_DIR)/klayout/xor.sh \ - $arg_values(-layout1) $arg_values(-layout2) $::env(DESIGN_NAME) \ - $arg_values(-output_gds) \ - |& tee $::env(TERMINAL_OUTPUT) $log - try_catch python3 $::env(SCRIPTS_DIR)/parse_klayout_xor_log.py \ - -l [index_file $::env(signoff_logs)/xor.log] \ - -o [index_file $::env(signoff_reports)/xor.rpt] - scrot_klayout -layout $arg_values(-output_gds) -log $::env(signoff_logs)/screenshot.klayout.xor.log - } - - if { $::env(KLAYOUT_XOR_XML) } { - try_catch bash $::env(SCRIPTS_DIR)/klayout/xor.sh \ - $arg_values(-layout1) $arg_values(-layout2) $::env(DESIGN_NAME) \ - $arg_values(-output_xml) \ - |& tee $::env(TERMINAL_OUTPUT) [index_file $::env(signoff_logs)/xor.log] - try_catch python3 $::env(SCRIPTS_DIR)/parse_klayout_xor_log.py \ - -l [index_file $::env(signoff_logs)/xor.log] \ - -o [index_file $::env(signoff_reports)/xor.rpt] - } + try_catch klayout \ + -b \ + -r $::env(SCRIPTS_DIR)/klayout/xor.drc \ + -rd a=$arg_values(-layout1) \ + -rd b=$arg_values(-layout2) \ + -rd jobs=$::env(KLAYOUT_XOR_THREADS) \ + -rd rdb_out=$db \ + -rd rpt_out=$report \ + |& tee $::env(TERMINAL_OUTPUT) $log TIMER::timer_stop exec echo "[TIMER::get_runtime]" | python3 $::env(SCRIPTS_DIR)/write_runtime.py "xor - klayout" + quit_on_xor_error -log $report } else { set layout2_rel [relpath . $arg_values(-layout2)] puts_warn "'$layout2_rel' wasn't found. Skipping GDS XOR."