blob: 28a3c148c980f83df9fe8967c374c28da0286137 [file] [log] [blame]
# Copyright 2022 GlobalFoundries 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
#
# 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.
"""
Run GlobalFoundries 180nm BCDLite DRC.
Usage:
run_drc.py (--help| -h)
run_drc.py (--path=<file_path>) (--variant=<combined_options>) [--verbose] [--table=<table_name>]... [--mp=<num_cores>] [--run_dir=<run_dir_path>] [--topcell=<topcell_name>] [--thr=<thr>] [--run_mode=<run_mode>] [--no_feol] [--no_beol] [--connectivity] [--density] [--density_only] [--antenna] [--antenna_only] [--no_offgrid]
Options:
--help -h Print this help message.
--path=<file_path> The input GDS file path.
--variant=<combined_options> Select combined options of metal_top, mim_option, and metal_level. Allowed values (A, B, C, D, E, F).
variant=A: Select metal_top=30K mim_option=A metal_level=3LM
variant=B: Select metal_top=20K mim_option=B metal_level=4LM
variant=C: Select metal_top=9K mim_option=B metal_level=5LM
variant=D: Select metal_top=9K mim_option=B metal_level=6LM
variant=E: Select metal_top=30K mim_option=B metal_level=6LM gold=false
variant=F: Select metal_top=20K mim_option=B metal_level=6LM
variant=G: Select metal_top=30K mim_option=B metal_level=6LM tanres_option=A
--topcell=<topcell_name> Topcell name to use.
--table=<table_name> Table name to use to run the rule deck.
--mp=<num_cores> Run the rule deck in parts in parallel to speed up the run. [default: 1]
--run_dir=<run_dir_path> Run directory to save all the results [default: pwd]
--thr=<thr> The number of threads used in run.
--run_mode=<run_mode> Select klayout mode Allowed modes (flat , deep, tiling). [default: flat]
--no_feol Turn off FEOL rules from running.
--no_beol Turn off BEOL rules from running.
--connectivity Turn on connectivity rules.
--density Turn on Density rules.
--density_only Turn on Density rules only.
--antenna Turn on Antenna checks.
--antenna_only Turn on Antenna checks only.
--no_offgrid Turn off OFFGRID checking rules.
--verbose Detailed rule execution log for debugging.
"""
from docopt import docopt
import os
import xml.etree.ElementTree as ET
import logging
import klayout.db
import glob
from datetime import datetime
from subprocess import check_call
import shutil
import concurrent.futures
import traceback
def get_rules_with_violations(results_database):
"""
This function will find all the rules that has violated in a database.
Parameters
----------
results_database : string or Path object
Path string to the results file
Returns
-------
set
A set that contains all rules in the database with violations
"""
mytree = ET.parse(results_database)
myroot = mytree.getroot()
all_violating_rules = set()
for z in myroot[7]: # myroot[7] : List rules with viloations
all_violating_rules.add(f"{z[1].text}".replace("'", ""))
return all_violating_rules
def check_drc_results(results_db_files: list):
"""
check_drc_results Checks the results db generated from run and report at the end if the DRC run failed or passed.
This function will exit with 1 if there are violations.
Parameters
----------
results_db_files : list
A list of strings that represent paths to results databases of all the DRC runs.
"""
if len(results_db_files) < 1:
logging.error("Klayout did not generate any rdb results. Please check run logs")
exit(1)
full_violating_rules = set()
for f in results_db_files:
violating_rules = get_rules_with_violations(f)
full_violating_rules.update(violating_rules)
if len(full_violating_rules) > 0:
logging.error("Klayout DRC run is not clean.")
logging.error(f"Violated rules are : {str(full_violating_rules)}\n")
exit(1)
else:
logging.info("Klayout DRC run is clean. GDS has no DRC violations.")
def generate_drc_run_template(drc_dir: str, run_dir: str, run_tables_list: list = []):
"""
generate_drc_run_template will generate the template file to run drc in the run_dir path.
Parameters
----------
drc_dir : str
Path string to the location where the DRC files would be found to get the list of the rule tables.
run_dir : str
Absolute path string to the run location where all the run output will be generated.
deck_name : str, optional
Name of the rule deck to use for generating the template, by default ""
run_tables_list : list, optional
list of target parts of the rule deck, if empty assume all of the rule tables found, by default []
Returns
-------
str
Absolute path to the generated DRC file.
"""
if len(run_tables_list) < 1:
all_tables = [
os.path.basename(f)
for f in glob.glob(os.path.join(drc_dir, "rule_decks", "*.drc"))
if "antenna" not in f
and "density" not in f
and "main" not in f
and "tail" not in f
]
deck_name = "main"
elif len(run_tables_list) == 1:
deck_name = run_tables_list[0]
all_tables = ["{}.drc".format(run_tables_list[0])]
else:
all_tables = ["{}.drc".format(t) for t in run_tables_list]
deck_name = "main"
logging.info(
f"## Generating template with for the following rule tables: {all_tables}"
)
logging.info(f"## Your run dir located at: {run_dir}")
all_tables.insert(0, "main.drc")
all_tables.append("tail.drc")
gen_rule_deck_path = os.path.join(run_dir, "{}.drc".format(deck_name))
with open(gen_rule_deck_path, "wb") as wfd:
for f in all_tables:
with open(os.path.join(drc_dir, "rule_decks", f), "rb") as fd:
shutil.copyfileobj(fd, wfd)
return gen_rule_deck_path
def get_top_cell_names(gds_path):
"""
get_top_cell_names get the top cell names from the GDS file.
Parameters
----------
gds_path : string
Path to the target GDS file.
Returns
-------
List of string
Names of the top cell in the layout.
"""
layout = klayout.db.Layout()
layout.read(gds_path)
top_cells = [t.name for t in layout.top_cells()]
return top_cells
def get_list_of_tables(drc_dir: str):
"""
get_list_of_tables get the list of available tables in the drc
Parameters
----------
drc_dir : str
Path to the DRC folder to get the list of tables from.
"""
return [
os.path.basename(f).replace(".drc", "")
for f in glob.glob(os.path.join(drc_dir, "rule_decks", "*.drc"))
if all(t not in f for t in ("antenna", "density", "main", "tail"))
]
def get_run_top_cell_name(arguments, layout_path):
"""
get_run_top_cell_name Get the top cell name to use for running. If it's provided by the user, we use the user input.
If not, we get it from the GDS file.
Parameters
----------
arguments : dict
Dictionary that holds the user inputs for the script generated by docopt.
layout_path : string
Path to the target layout.
Returns
-------
string
Name of the topcell to use in run.
"""
if arguments["--topcell"]:
topcell = arguments["--topcell"]
else:
layout_topcells = get_top_cell_names(layout_path)
if len(layout_topcells) > 1:
logging.error(
"## Layout has multiple topcells. Please use --topcell to determine which topcell you want to run on."
)
exit(1)
else:
topcell = layout_topcells[0]
return topcell
def generate_klayout_switches(arguments, layout_path):
"""
parse_switches Function that parse all the args from input to prepare switches for DRC run.
Parameters
----------
arguments : dict
Dictionary that holds the arguments used by user in the run command. This is generated by docopt library.
layout_path : string
Path to the layout file that we will run DRC on.
Returns
-------
dict
Dictionary that represent all run switches passed to klayout.
"""
switches = dict()
# No. of threads
thrCount = 2 if arguments["--thr"] is None else int(arguments["--thr"])
switches["thr"] = str(int(thrCount))
if arguments["--run_mode"] in ["flat", "deep", "tiling"]:
switches["run_mode"] = arguments["--run_mode"]
else:
logging.error("Allowed klayout modes are (flat , deep , tiling) only")
exit()
if arguments["--variant"] == "A":
switches["metal_top"] = "30K"
switches["mim_option"] = "A"
switches["metal_level"] = "3LM"
elif arguments["--variant"] == "B":
switches["metal_top"] = "11K"
switches["mim_option"] = "B"
switches["metal_level"] = "4LM"
elif arguments["--variant"] == "C":
switches["metal_top"] = "9K"
switches["mim_option"] = "B"
switches["metal_level"] = "5LM"
elif arguments["--variant"] == "D":
switches["metal_top"] = "9K"
switches["mim_option"] = "B"
switches["metal_level"] = "6LM"
elif arguments["--variant"] == "E":
switches["metal_top"] = "30K"
switches["mim_option"] = "B"
switches["metal_level"] = "6LM"
switches["gold"] = "false"
elif arguments["--variant"] == "F":
switches["metal_top"] = "20K"
switches["mim_option"] = "B"
switches["metal_level"] = "6LM"
elif arguments["--variant"] == "G":
switches["metal_top"] = "30K"
switches["mim_option"] = "B"
switches["metal_level"] = "6LM"
switches["tanres_option"] = "A"
else:
logging.error("variant switch allowed values are (A , B, C) only")
exit(1)
if arguments["--verbose"]:
switches["verbose"] = "true"
else:
switches["verbose"] = "false"
if arguments["--no_feol"]:
switches["feol"] = "false"
else:
switches["feol"] = "true"
if arguments["--no_beol"]:
switches["beol"] = "false"
else:
switches["beol"] = "true"
if arguments["--no_offgrid"]:
switches["offgrid"] = "false"
else:
switches["offgrid"] = "true"
if arguments["--connectivity"]:
switches["conn_drc"] = "true"
else:
switches["conn_drc"] = "false"
if arguments["--density"]:
switches["density"] = "true"
else:
switches["density"] = "false"
switches["topcell"] = get_run_top_cell_name(arguments, layout_path)
switches["input"] = layout_path
return switches
def check_klayout_version():
"""
check_klayout_version checks klayout version and makes sure it would work with the DRC.
"""
# ======= Checking Klayout version =======
klayout_v_ = os.popen("klayout -b -v").read()
klayout_v_ = klayout_v_.split("\n")[0]
klayout_v_list = []
if klayout_v_ == "":
logging.error(
f"Klayout is not found. Please make sure klayout is installed. Current version: {klayout_v_}"
)
exit(1)
else:
klayout_v_list = [int(v) for v in klayout_v_.split(" ")[-1].split(".")]
if len(klayout_v_list) < 1 or len(klayout_v_list) > 3:
logging.error(
f"Was not able to get klayout version properly. Current version: {klayout_v_}"
)
exit(1)
elif len(klayout_v_list) >= 2 and len(klayout_v_list) <= 3:
if klayout_v_list[1] < 28:
logging.error("Prerequisites at a minimum: KLayout 0.28.0")
logging.error(
"Using this klayout version has not been assesed in this development. Limits are unknown"
)
exit(1)
logging.info(f"Your Klayout version is: {klayout_v_}")
def check_layout_path(layout_path):
"""
check_layout_type checks if the layout provided is GDS or OAS. Otherwise, kill the process. We only support GDS or OAS now.
Parameters
----------
layout_path : string
string that represent the path of the layout.
Returns
-------
string
string that represent full absolute layout path.
"""
if not os.path.isfile(layout_path):
logging.error(
f"## GDS file path {layout_path} provided doesn't exist or not a file."
)
exit(1)
if ".gds" not in layout_path and ".oas" not in layout_path:
logging.error(
f"## Layout {layout_path} is not in GDSII or OASIS format. Please use gds format."
)
exit(1)
return os.path.abspath(layout_path)
def build_switches_string(sws: dict):
"""
build_switches_string Build swtiches string from dictionary.
Parameters
----------
sws : dict
Dictionary that holds the Antenna swithces.
"""
return " ".join(f"-rd {k}={v}" for k, v in sws.items())
def run_check(drc_file: str, drc_name: str, path: str, run_dir: str, sws: dict):
"""
run_antenna_check run DRC check based on DRC file provided.
Parameters
----------
drc_file : str
String that has the file full path to run.
path : str
String that holds the full path of the layout.
run_dir : str
String that holds the full path of the run location.
sws : dict
Dictionary that holds all switches that needs to be passed to the antenna checks.
Returns
-------
string
string that represent the path to the results output database for this run.
"""
## Using print because of the multiprocessing
logging.info(
"Running Global Foundries 180nm BCDLite {} checks on design {} on cell {}:".format(
path, drc_name, sws["topcell"]
)
)
layout_base_name = os.path.basename(path).split(".")[0]
new_sws = sws.copy()
report_path = os.path.join(
run_dir, "{}_{}.lyrdb".format(layout_base_name, drc_name)
)
new_sws["report"] = report_path
sws_str = build_switches_string(new_sws)
sws_str += f" -rd table_name={drc_name}"
run_str = f"klayout -b -r {drc_file} {sws_str}"
check_call(run_str, shell=True)
return report_path
def run_parallel_run(
arguments: dict,
rule_deck_full_path: str,
layout_path: str,
switches: dict,
drc_run_dir: str,
):
"""
run_single_processor run the drc checks as in a multi-processing.
Parameters
----------
arguments : dict
Dictionary that holds the arguments passed to the run_drc script.
rule_deck_full_path : str
String that holds the path of the rule deck files.
layout_path : str
Path to the target layout.
switches : dict
Dictionary that holds all the switches that will be passed to klayout run.
drc_run_dir : str
Path to the run location.
"""
list_rule_deck_files = dict()
## Run Antenna if required.
if arguments["--antenna"]:
drc_path = os.path.join(rule_deck_full_path, "rule_decks", "antenna.drc")
list_rule_deck_files["antenna"] = drc_path
## Run Density if required.
if arguments["--density"]:
drc_path = os.path.join(rule_deck_full_path, "rule_decks", "density.drc")
list_rule_deck_files["density"] = drc_path
if not arguments["--table"]:
list_of_tables = get_list_of_tables(rule_deck_full_path)
else:
list_of_tables = arguments["--table"]
## Generate run rule deck from template.
for t in list_of_tables:
drc_file = generate_drc_run_template(rule_deck_full_path, drc_run_dir, [t])
list_rule_deck_files[t] = drc_file
## Run All DRC files.
list_res_db_files = []
with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
future_to_run_name = dict()
for n in list_rule_deck_files:
future_to_run_name[
executor.submit(
run_check,
list_rule_deck_files[n],
n,
layout_path,
drc_run_dir,
switches,
)
] = n
for future in concurrent.futures.as_completed(future_to_run_name):
run_name = future_to_run_name[future]
try:
list_res_db_files.append(future.result())
except Exception as exc:
logging.error("%s generated an exception: %s" % (run_name, str(exc)))
traceback.print_exc()
## Check run
check_drc_results(list_res_db_files)
def run_single_processor(
arguments: dict,
rule_deck_full_path: str,
layout_path: str,
switches: dict,
drc_run_dir: str,
):
"""
run_single_processor run the drc checks as single run.
Parameters
----------
arguments : dict
Dictionary that holds the arguments passed to the run_drc script.
rule_deck_full_path : str
String that holds the path of the rule deck files.
layout_path : str
Path to the target layout.
switches : dict
Dictionary that holds all the switches that will be passed to klayout run.
drc_run_dir : str
Path to the run location.
"""
list_res_db_files = []
## Run Antenna if required.
if arguments["--antenna"] or arguments["--antenna_only"]:
drc_path = os.path.join(rule_deck_full_path, "rule_decks", "antenna.drc")
list_res_db_files.append(
run_check(drc_path, "antenna", layout_path, drc_run_dir, switches)
)
if arguments["--antenna_only"]:
logging.info("## Completed running Antenna checks only.")
exit()
## Run Density if required.
if arguments["--density"] or arguments["--density_only"]:
drc_path = os.path.join(rule_deck_full_path, "rule_decks", "density.drc")
list_res_db_files.append(
run_check(drc_path, "density", layout_path, drc_run_dir, switches)
)
if arguments["--density_only"]:
logging.info("## Completed running density checks only.")
exit()
## Generate run rule deck from template.
if not arguments["--table"]:
drc_file = generate_drc_run_template(rule_deck_full_path, drc_run_dir)
else:
drc_file = generate_drc_run_template(
rule_deck_full_path, drc_run_dir, arguments["--table"]
)
## Run Main DRC
list_res_db_files.append(
run_check(drc_file, "main", layout_path, drc_run_dir, switches)
)
## Check run
check_drc_results(list_res_db_files)
def main(drc_run_dir: str, arguments: dict):
"""
main function to run the DRC.
Parameters
----------
drc_run_dir : str
String with absolute path of the full run dir.
arguments : dict
Dictionary that holds the arguments used by user in the run command. This is generated by docopt library.
"""
# Check gds file existance
if not os.path.exists(arguments["--path"]):
file_path = arguments["--path"]
logging.error(
f"The input GDS file path {file_path} doesn't exist, please recheck."
)
exit(1)
rule_deck_full_path = os.path.dirname(os.path.abspath(__file__))
## Check Klayout version
check_klayout_version()
## Check if there was a layout provided.
if not arguments["--path"]:
logging.error("No provided gds file, please add one")
exit(1)
## Check layout type
layout_path = arguments["--path"]
layout_path = check_layout_path(layout_path)
## Get run switches
switches = generate_klayout_switches(arguments, layout_path)
if (
int(arguments["--mp"]) == 1
or arguments["--antenna_only"]
or arguments["--density_only"]
):
run_single_processor(
arguments, rule_deck_full_path, layout_path, switches, drc_run_dir
)
else:
run_parallel_run(
arguments, rule_deck_full_path, layout_path, switches, drc_run_dir
)
# ================================================================
# -------------------------- MAIN --------------------------------
# ================================================================
if __name__ == "__main__":
# arguments
arguments = docopt(__doc__, version="RUN DRC: 1.0")
# logs format
now_str = datetime.utcnow().strftime("drc_run_%Y_%m_%d_%H_%M_%S")
if (
arguments["--run_dir"] == "pwd"
or arguments["--run_dir"] == ""
or arguments["--run_dir"] is None
):
drc_run_dir = os.path.join(os.path.abspath(os.getcwd()), now_str)
else:
drc_run_dir = os.path.abspath(arguments["--run_dir"])
os.makedirs(drc_run_dir, exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
handlers=[
logging.FileHandler(os.path.join(drc_run_dir, "{}.log".format(now_str))),
logging.StreamHandler(),
],
format="%(asctime)s | %(levelname)-7s | %(message)s",
datefmt="%d-%b-%Y %H:%M:%S",
)
# Calling main function
main(drc_run_dir, arguments)