blob: 8923512cffd44d1e86f9667eb40a0a0f38cf106e [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 ULL LVS Regression.
Usage:
run_regression.py (--help| -h)
run_regression.py [--device_name=<device_name>] [--mp=<num>] [--run_name=<run_name>]
Options:
--help -h Print this help message.
--device_name=<device_name> Name of device that we want to run regression for, Allowed values (MOS, BJT, DIODE, RES, MIMCAP, MOSCAP, PISCAP, VARACTOR).
--mp=<num> The number of threads used in run.
--run_name=<run_name> Select your run name.
"""
from subprocess import check_call
from subprocess import Popen, PIPE
import concurrent.futures
import traceback
import yaml
from docopt import docopt
import os
from datetime import datetime
import xml.etree.ElementTree as ET
import time
import pandas as pd
import logging
import glob
from pathlib import Path
from tqdm import tqdm
import re
import errno
import numpy as np
from collections import defaultdict
import shutil
SUPPORTED_TC_EXT = "gds"
SUPPORTED_SPICE_EXT = "cdl"
SUPPORTED_SW_EXT = "yaml"
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("Klayout is not found. Please make sure klayout is installed.")
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("Was not able to get klayout version properly.")
exit(1)
elif len(klayout_v_list) >= 2 or len(klayout_v_list) <= 3:
if klayout_v_list[1] < 28 or (klayout_v_list[1] == 28 and klayout_v_list[2] <= 3):
logging.error("Prerequisites at a minimum: KLayout 0.28.4")
logging.error(
"Using this klayout version is not supported in this development."
)
exit(1)
logging.info(f"Your Klayout version is: {klayout_v_}")
def parse_existing_devices(rule_deck_path, output_path, target_device_group=None):
"""
This function collects the rule names from the existing drc rule decks.
Parameters
----------
rule_deck_path : string or Path object
Path string to the LVS directory where all the LVS files are located.
output_path : string or Path
Path of the run location to store the output analysis file.
target_device_group : string Optional
Name of the device group to be in testing
Returns
-------
pd.DataFrame
A pandas DataFrame with the rule and rule deck used.
"""
if target_device_group is None:
lvs_files = glob.glob(os.path.join(rule_deck_path, "rule_decks", "*_extraction.lvs"))
else:
table_device_file = os.path.join(
rule_deck_path, "rule_decks", f"{str(target_device_group).lower()}_extraction.lvs"
)
if not os.path.isfile(table_device_file):
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), table_device_file
)
lvs_files = [table_device_file]
rules_data = list()
for runset in lvs_files:
with open(runset, "r") as f:
for line in f:
if "extract_devices" in line:
line_list = line.split("'")
rule_info = dict()
rule_info["device_group"] = os.path.basename(runset).replace(
"_extraction.lvs", ""
).upper()
rule_info["device_name"] = line_list[1]
rule_info["in_rule_deck"] = 1
rules_data.append(rule_info)
df = pd.DataFrame(rules_data)
df.drop_duplicates(inplace=True)
df.to_csv(os.path.join(output_path, "rule_deck_rules.csv"), index=False)
return df
def build_tests_dataframe(unit_test_cases_dir, target_device_group):
"""
This function is used for getting all test cases available in a formated dataframe before running.
Parameters
----------
unit_test_cases_dir : str
Path string to the location of unit test cases path.
target_device_group : str or None
Name of device group that we want to run regression for. If None, run all found.
Returns
-------
pd.DataFrame
A DataFrame that has all the targetted test cases that we need to run.
"""
all_unit_test_cases_layout = sorted(
Path(unit_test_cases_dir).rglob("*.{}".format(SUPPORTED_TC_EXT))
)
logging.info(
"## Total number of gds files test cases found: {}".format(len(all_unit_test_cases_layout))
)
all_unit_test_cases_netlist = sorted(
Path(unit_test_cases_dir).rglob("*.{}".format(SUPPORTED_SPICE_EXT))
)
logging.info(
"## Total number of spice files test cases found: {}".format(len(all_unit_test_cases_netlist))
)
if len(all_unit_test_cases_netlist) != len(all_unit_test_cases_layout):
logging.error(
"## Each testcase should have Layout and Netlist file"
)
exit(1)
# Get test cases df from test cases
tc_df = pd.DataFrame({"test_layout_path": all_unit_test_cases_layout , "test_netlist_path": all_unit_test_cases_netlist})
tc_df["device_name"] = tc_df["test_layout_path"].apply(lambda x: x.name.replace(".gds", ""))
tc_df["device_group"] = tc_df["test_layout_path"].apply(lambda x: x.parent.parent.name.replace("_devices", "").upper())
if target_device_group is not None:
tc_df = tc_df[tc_df["device_group"] == target_device_group]
if len(tc_df) < 1:
logging.error("No test cases remaining after filtering.")
exit(1)
tc_df["run_id"] = range(len(tc_df))
return tc_df
def get_switches(yaml_file, rule_name):
"""Parse yaml file and extract switches data
Parameters
----------
yaml_file : str
yaml config file path given py the user.
Returns
-------
yaml_dic : dictionary
dictionary containing switches data.
"""
# load yaml config data
with open(yaml_file, "r") as stream:
try:
yaml_dic = yaml.safe_load(stream)
except yaml.YAMLError as exc:
print(exc)
return [f"{param}={value}" for param, value in yaml_dic[rule_name].items()]
def run_test_case(
lvs_dir,
layout_path,
netlist_path,
run_dir,
device_name,
):
"""
This function run a single test case using the correct DRC file.
Parameters
----------
lvs_dir : string or Path
Path to the location where all runsets exist.
layout_path : stirng or Path object
Path string to the layout of the test pattern we want to test.
netlist_path : stirng or Path object
Path string to the netlist of the test pattern we want to test.
run_dir : stirng or Path object
Path to the location where is the regression run is done.
device_name : string
Device name that we are running on.
Returns
-------
dict
A dict with all rule counts
"""
# Get switches used for each run
sw_file = os.path.join(
Path(layout_path.parent).absolute(), f"{device_name}.{SUPPORTED_SW_EXT}"
)
if os.path.exists(sw_file):
switches = " ".join(get_switches(sw_file, device_name))
else:
# Get switches
switches = " -rd lvs_sub=sub!" if device_name == "sample_ggnfet_06v0_dss" else " -rd lvs_sub=vdd!" # default switch
# Creating run folder structure and copy testcases in it
pattern_clean = ".".join(os.path.basename(layout_path).split(".")[:-1])
output_loc = os.path.join(run_dir, device_name)
pattern_log = os.path.join(output_loc, f"{pattern_clean}_lvs.log")
os.makedirs(output_loc, exist_ok=True)
layout_path_run = os.path.join(run_dir, device_name, f"{device_name}.gds")
netlist_path_run = os.path.join(run_dir, device_name, f"{device_name}.cdl")
shutil.copyfile(layout_path, layout_path_run)
shutil.copyfile(netlist_path, netlist_path_run)
# command to run drc
call_str = f"klayout -b -r {lvs_dir}/gf180ULL.lvs -rd input={layout_path_run} -rd schematic={device_name}.cdl -rd report={device_name}.lvsdb -rd target_netlist={device_name}_extracted.cir {switches} > {pattern_log} 2>&1"
# Starting klayout run
try:
check_call(call_str, shell=True)
except Exception as e:
pattern_results = glob.glob(os.path.join(output_loc, f"{pattern_clean}*.lvsdb"))
if len(pattern_results) < 1:
logging.error("%s generated an exception: %s" % (pattern_clean, e))
traceback.print_exc()
raise Exception("Failed DRC run.")
# dumping log into output to make CI have the log
if os.path.isfile(pattern_log):
with open(pattern_log, "r") as f:
result = f.read()
for line in f:
line = line.strip()
logging.info(f"{line}")
# checking device status
device_status = 'Failed'
if "Congratulations! Netlists match" in result:
logging.info(f"{device_name} testcase passed")
device_status = 'Passed'
else:
logging.error(f"{device_name} testcase failed.")
logging.error(f"Please recheck {layout_path} file.")
else:
logging.error("Klayout LVS run failed, there is no log file is generated")
exit(1)
return device_status
def run_all_test_cases(tc_df, lvs_dir, run_dir, num_workers):
"""
This function run all test cases from the input dataframe.
Parameters
----------
tc_df : pd.DataFrame
DataFrame that holds all the test cases information for running.
lvs_dir : string or Path
Path string to the location of the lvs runsets.
run_dir : string or Path
Path string to the location of the testing code and output.
num_workers : int
Number of workers to use for running the regression.
Returns
-------
pd.DataFrame
A pandas DataFrame with all test cases information post running.
"""
tc_df["device_status"] = "no status"
with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
future_to_run_id = dict()
for i, row in tc_df.iterrows():
future_to_run_id[
executor.submit(
run_test_case,
lvs_dir,
row["test_layout_path"],
row["test_netlist_path"],
run_dir,
row["device_name"],
)
] = row["run_id"]
for future in concurrent.futures.as_completed(future_to_run_id):
run_id = future_to_run_id[future]
try:
tc_df.loc[tc_df["run_id"] == run_id, "device_status"] = future.result()
except Exception as exc:
logging.error("%d generated an exception: %s" % (run_id, exc))
traceback.print_exc()
tc_df.loc[tc_df["run_id"] == run_id, "device_status"] = "exception"
return tc_df
def aggregate_results(
results_df: pd.DataFrame, devices_df: pd.DataFrame
):
"""
aggregate_results Aggregate the results for all runs.
Parameters
----------
results_df : pd.DataFrame
Dataframe that holds the information about the unit test rules.
devices_df : pd.DataFrame
Dataframe that holds the information about all the devices implemented in the rule deck.
Returns
-------
pd.DataFrame
A DataFrame that has all data analysis aggregated into one.
"""
if len(devices_df) < 1 and len(results_df) < 1:
logging.error("## There are no rules for analysis or run.")
exit(1)
elif len(devices_df) < 1 and len(results_df) > 0:
df = results_df
elif len(devices_df) > 0 and len(results_df) < 1:
df = devices_df
else:
df = results_df.merge(devices_df, how="outer", on=["device_group", "device_name"])
df.loc[(df["device_status"] != 'Passed'), "device_status"] = "Failed"
return df
def run_regression(lvs_dir, output_path, target_device_group, cpu_count):
"""
Running Regression Procedure.
This function runs the full regression on all test cases.
Parameters
----------
lvs_dir : string
Path string to the LVS directory where all the LVS files are located.
output_path : str
Path string to the location of the output results of the run.
target_device_group : str or None
Name of device group that we want to run regression for. If None, run all found.
cpu_count : int
Number of cpus to use in running testcases.
Returns
-------
bool
If all regression passed, it returns true. If any of the rules failed it returns false.
"""
## Parse Existing Rules
devices_df = parse_existing_devices(lvs_dir, output_path, target_device_group)
logging.info(
"## Total number of devices found in rule decks: {}".format(len(devices_df))
)
logging.info("## Parsed devices: \n" + str(devices_df))
## Get all test cases available in the repo.
test_cases_path = os.path.join(lvs_dir, "testing/testcases")
unit_test_cases_path = os.path.join(test_cases_path, "unit")
tc_df = build_tests_dataframe(unit_test_cases_path, target_device_group)
logging.info("## Total table gds files found: {}".format(len(tc_df)))
logging.info("## Found testcases: \n" + str(tc_df))
## Run all test cases.
results_df = run_all_test_cases(tc_df, lvs_dir, output_path, cpu_count)
logging.info("## Testcases found results: \n" + str(results_df))
## Aggregate all dataframes into one
df = aggregate_results(results_df, devices_df)
df.drop_duplicates(inplace=True)
df.drop('run_id', inplace=True, axis=1)
logging.info("## Final analysis table: \n" + str(df))
## Generate error if there are any missing info or fails.
df.to_csv(os.path.join(output_path, "all_test_cases_results.csv"), index=False)
## Check if there any rules that generated false positive or false negative
failing_results = df[~df["device_status"].isin(["Passed"])]
logging.info("## Failing test cases: \n" + str(failing_results))
if len(failing_results) > 0:
logging.error("## Some test cases failed .....")
return False
else:
logging.info("## All testcases passed.")
return True
def main(lvs_dir, output_path, target_device_group):
"""
Main Procedure.
This function is the main execution procedure
Parameters
----------
lvs_dir : str
Path string to the LVS directory where all the LVS files are located.
output_path : str
Path string to the location of the output results of the run.
target_device_group : str or None
Name of device group that we want to run regression for. If None, run all found.
Returns
-------
bool
If all regression passed, it returns true. If any of the rules failed it returns false.
"""
# No. of threads
cpu_count = os.cpu_count() if args["--mp"] is None else int(args["--mp"])
# Pandas printing setup
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)
pd.set_option("max_colwidth", None)
pd.set_option("display.width", 1000)
# info logs for args
logging.info("## Run folder is: {}".format(run_name))
logging.info("## Target device is: {}".format(target_device_group))
# Start of execution time
t0 = time.time()
## Check Klayout version
check_klayout_version()
# Calling regression function
run_status = run_regression(lvs_dir, output_path, target_device_group, cpu_count)
# End of execution time
logging.info("Total execution time {}s".format(time.time() - t0))
if run_status:
logging.info("Test completed successfully.")
else:
logging.error("Test failed.")
exit(1)
if __name__ == "__main__":
# docopt reader
args = docopt(__doc__, version="LVS Regression: 0.2")
# arguments
run_name = args["--run_name"]
# run name
if run_name is None:
run_name = datetime.utcnow().strftime("unit_tests_%Y_%m_%d_%H_%M_%S")
# Paths of regression dirs
testing_dir = os.path.dirname(os.path.abspath(__file__))
lvs_dir = os.path.dirname(testing_dir)
output_path = os.path.join(testing_dir, run_name)
# Creating output dir
os.makedirs(output_path, exist_ok=True)
# logs format
logging.basicConfig(
level=logging.DEBUG,
handlers=[
logging.FileHandler(os.path.join(output_path, "{}.log".format(run_name))),
logging.StreamHandler(),
],
format="%(asctime)s | %(levelname)-7s | %(message)s",
datefmt="%d-%b-%Y %H:%M:%S",
)
## selected device
allowed_devices = ["MOS", "BJT", "DIODE", "RES", "MIMCAP", "MOSCAP", "PISCAP", "VARACTOR"]
target_device_group = args["--device_name"]
if target_device_group and target_device_group not in allowed_devices:
logging.error("Allowed devices are (MOS, BJT, DIODE, RES, MIMCAP, MOSCAP, PISCAP, VARACTOR) only")
exit(1)
# Calling main function
run_status = main(lvs_dir, output_path, target_device_group)