diff --git a/.github/workflows/openlane_ci.yml b/.github/workflows/openlane_ci.yml index 8199e81e..602c6a33 100644 --- a/.github/workflows/openlane_ci.yml +++ b/.github/workflows/openlane_ci.yml @@ -18,6 +18,7 @@ jobs: runs-on: ubuntu-20.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} + issue_regression_matrix: ${{ steps.set-matrix.outputs.issue_regression_matrix }} steps: - uses: actions/checkout@v2 @@ -96,7 +97,57 @@ jobs: else echo "::set-output name=matrix::$(python3 ./.github/test_sets/get_test_matrix.py fastest_test_set)" fi + echo "::set-output name=issue_regression_matrix::$(python3 ./run_issue_regressions.py get_matrix)" + + issue_regression_test: + needs: docker_build + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.docker_build.outputs.issue_regression_matrix) }} + name: Test Issue Regression ${{ matrix.design }} + steps: + - uses: actions/checkout@v2 + # EXPORT BLOCK + - name: Export Repo URL + run: echo "REPO_URL=git://github.com/${{ github.repository }}.git" >> $GITHUB_ENV + - name: Export PDK ROOT + run: echo "PDK_ROOT=/usr/local/pdk" >> $GITHUB_ENV + + - name: Export Branch Name + run: echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + + - name: Export Temp Image Name + run: echo "OPENLANE_IMAGE_NAME=openlane:intermediate" >> $GITHUB_ENV + # END EXPORT BLOCK + + - name: Download Docker Image + uses: actions/download-artifact@v2 + with: + name: docker-image + path: /tmp + + - name: Import Docker Image + run: docker load --input /tmp/image.tar + + - name: Download PDK Tarball + uses: actions/download-artifact@v2 + with: + name: pdk-tarball + path: /tmp + + - name: Unpack PDK Tarball + run: | + sudo mkdir -p ${{ env.PDK_ROOT }}/sky130A + sudo chown -R $USER:$USER ${{ env.PDK_ROOT }} + tar -xf /tmp/sky130A.tar -C $PDK_ROOT/sky130A . + + - name: Get Pyyaml + run: python3 -m pip install pyyaml + + - name: Run Issue Regression Test + run: cd ${GITHUB_WORKSPACE}/ && make run_issue_regression ISSUE_REGRESSION_DESIGN=${{ matrix.design }} # Each test has two components: a fast test set and an extended test set. # The fast test set is run on all PRs, etc. The extended test set runs on schedule. test: @@ -159,7 +210,7 @@ jobs: cleanup_and_deploy: name: Cleanup (and Possibly Deployment) - needs: test + needs: [test, issue_regression_test] if: always() runs-on: ubuntu-20.04 steps: diff --git a/Makefile b/Makefile index 0d47ea9e..a422dfb6 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,16 @@ test_design_list: --benchmark $(BENCHMARK)\ $(DESIGN_LIST)\ " +# -u is needed, as the python buffers the stdout, so no output is generated +run_issue_regression: + cd $(OPENLANE_DIR) && \ + $(ENV_COMMAND) sh -c "\ + python3 -u run_issue_regressions.py run $(ISSUE_REGRESSION_DESIGN)" + +issue_regression_all: + cd $(OPENLANE_DIR) && \ + $(ENV_COMMAND) sh -c "\ + python3 -u run_issue_regressions.py run_all" .PHONY: test test: diff --git a/README.md b/README.md index a5497664..684f6d75 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,8 @@ python3 run_designs.py --tag test --threads 3 spm xtea md5 aes256 For more information on how to run this script, refer to this [file][21] +OpenLane also has flow for issue regression testing. Refer to this [document][38]. + For more information on design configurations, how to update them, and the need for an exploration for each design, refer to this [file](./designs/README.md) # Hardening Macros @@ -406,3 +408,5 @@ To check the original author list of OpenLane, check [this][33]. [35]: https://github.com/KLayout/klayout [36]: https://github.com/cuhk-eda/cu-gr [37]: https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/src/rcx +[38]: ./docs/source/issue_regression_tests.md + diff --git a/designs/def_test/def_test.def b/designs/def_test/def_test.def deleted file mode 100644 index 3a5cf4a7..00000000 --- a/designs/def_test/def_test.def +++ /dev/null @@ -1,21 +0,0 @@ -VERSION 5.8 ; -DIVIDERCHAR "/" ; -BUSBITCHARS "[]" ; -DESIGN def_test ; -UNITS DISTANCE MICRONS 1000 ; -DIEAREA ( 0 0 ) ( 400000 400000 ) ; -PINS 2 ; - - in + NET in + DIRECTION INPUT + USE SIGNAL - + PORT - + LAYER met2 ( -140 -2000 ) ( 140 2000 ) - + PLACED ( 1000 350000 ) N ; - - out + NET out + DIRECTION OUTPUT + USE SIGNAL - + PORT - + LAYER met2 ( -140 -2000 ) ( 140 2000 ) - + PLACED ( 400 2000 ) N ; -END PINS -NETS 2 ; - - in ( PIN in ) ( _0_ A ) + USE SIGNAL ; - - out ( PIN out ) ( _0_ Y ) + USE SIGNAL ; -END NETS -END DESIGN diff --git a/designs/def_test/config.tcl b/designs/issue_892_def_test/config.tcl similarity index 100% rename from designs/def_test/config.tcl rename to designs/issue_892_def_test/config.tcl diff --git a/designs/issue_892_def_test/def_test.def b/designs/issue_892_def_test/def_test.def new file mode 100644 index 00000000..36445ec2 --- /dev/null +++ b/designs/issue_892_def_test/def_test.def @@ -0,0 +1,33 @@ +VERSION 5.8 ; +DIVIDERCHAR "/" ; +BUSBITCHARS "[]" ; +DESIGN def_test ; +UNITS DISTANCE MICRONS 1000 ; +DIEAREA ( 0 0 ) ( 400000 400000 ) ; +PINS 4 ; + - in + NET in + DIRECTION INPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 1000 350000 ) N ; + - out + NET out + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 400 2000 ) N ; + - tied_to_zero + NET zero_ + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 4000 2000 ) N ; + - manufacturing_grid_missaligned_pin + NET manufacturing_grid_missaligned_pin + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 10000 2000 ) N ; + - VPWR + NET VPWR + DIRECTION INOUT + USE POWER + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 20000 2000 ) N ; + - VGND + NET VGND + DIRECTION INOUT + USE GROUND + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 25000 2000 ) N ; +END PINS +END DESIGN diff --git a/designs/def_test/hooks/post_run.py b/designs/issue_892_def_test/hooks/post_run.py similarity index 67% rename from designs/def_test/hooks/post_run.py rename to designs/issue_892_def_test/hooks/post_run.py index e93d3c50..9486dadf 100644 --- a/designs/def_test/hooks/post_run.py +++ b/designs/issue_892_def_test/hooks/post_run.py @@ -9,9 +9,6 @@ import odb import os import sys -print(sys.argv) -print(os.environ) - def extract_pins(db, def_file): odb.read_lef(db, os.environ["TECH_LEF"]) @@ -24,19 +21,28 @@ def extract_pins(db, def_file): result_data = {} for net in nets: - name = net.getName() - print("Net: " + name) + net_name = net.getName() # BTerms = PINS, if it has a pin we need to keep the net bterms = net.getBTerms() if len(bterms) > 0: for port in bterms: + if (port.getSigType() == "POWER") or (port.getSigType() == "GROUND"): + # Ignore power pins + continue name = port.getName() - print("Port: " + name) bbox = port.getBBox() result_data[name] = bbox - print("ll: " + " ".join(str(e) for e in bbox.ll())) - print("ur: " + " ".join(str(e) for e in bbox.ur())) + print( + "Net:", + net_name, + "Port:", + name, + "ll: ", + " ".join(str(e) for e in bbox.ll()), + "ur: ", + " ".join(str(e) for e in bbox.ur()), + ) return result_data @@ -67,4 +73,20 @@ for k, v in ref_data.items(): v.ur() == result_data[k].ur() ), f"For pin {k} upper right rectangle point {result_data[k].ur()} does not match {v.ur()}" +assert ( + result_db.getTech().getManufacturingGrid() + == ref_db.getTech().getManufacturingGrid() +) +assert ( + result_db.getTech().getDbUnitsPerMicron() == ref_db.getTech().getDbUnitsPerMicron() +) +assert ( + result_db.getChip().getBlock().getDieArea().ur() + == ref_db.getChip().getBlock().getDieArea().ur() +) +assert ( + result_db.getChip().getBlock().getDieArea().ll() + == ref_db.getChip().getBlock().getDieArea().ll() +) + sys.exit(0) diff --git a/designs/def_test/sky130A_sky130_fd_sc_hd_config.tcl b/designs/issue_892_def_test/sky130A_sky130_fd_sc_hd_config.tcl similarity index 100% rename from designs/def_test/sky130A_sky130_fd_sc_hd_config.tcl rename to designs/issue_892_def_test/sky130A_sky130_fd_sc_hd_config.tcl diff --git a/designs/def_test/src/def_test.v b/designs/issue_892_def_test/src/def_test.v similarity index 74% rename from designs/def_test/src/def_test.v rename to designs/issue_892_def_test/src/def_test.v index 778a6218..723b707e 100644 --- a/designs/def_test/src/def_test.v +++ b/designs/issue_892_def_test/src/def_test.v @@ -1,4 +1,4 @@ -// Copyright 2020 Matt Venn +// Copyright 2022 Arman Avetisyan // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,7 +15,12 @@ `default_nettype none module def_test ( input wire in, - output out ); + output out, + output tied_to_zero, + output manufacturing_grid_missaligned_pin + ); + // We tie this to one, so if def's pin is tied to zero, it will LVS error +assign tied_to_zero = 1; assign out = !in; diff --git a/designs/issue_912_def_test_missaligned/config.tcl b/designs/issue_912_def_test_missaligned/config.tcl new file mode 120000 index 00000000..dea3e204 --- /dev/null +++ b/designs/issue_912_def_test_missaligned/config.tcl @@ -0,0 +1 @@ +../issue_892_def_test/config.tcl \ No newline at end of file diff --git a/designs/issue_912_def_test_missaligned/def_test.def b/designs/issue_912_def_test_missaligned/def_test.def new file mode 100644 index 00000000..10cdf07e --- /dev/null +++ b/designs/issue_912_def_test_missaligned/def_test.def @@ -0,0 +1,33 @@ +VERSION 5.8 ; +DIVIDERCHAR "/" ; +BUSBITCHARS "[]" ; +DESIGN def_test ; +UNITS DISTANCE MICRONS 1000 ; +DIEAREA ( 0 0 ) ( 400000 400000 ) ; +PINS 4 ; + - in + NET in + DIRECTION INPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 1000 350000 ) N ; + - out + NET out + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 400 2000 ) N ; + - tied_to_zero + NET zero_ + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 4000 2000 ) N ; + - manufacturing_grid_missaligned_pin + NET manufacturing_grid_missaligned_pin + DIRECTION OUTPUT + USE SIGNAL + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 10001 2000 ) N ; + - VPWR + NET VPWR + DIRECTION INOUT + USE POWER + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 20000 2000 ) N ; + - VGND + NET VGND + DIRECTION INOUT + USE GROUND + + PORT + + LAYER met2 ( -140 -2000 ) ( 140 2000 ) + + PLACED ( 25000 2000 ) N ; +END PINS +END DESIGN diff --git a/designs/issue_912_def_test_missaligned/issue_regression.py b/designs/issue_912_def_test_missaligned/issue_regression.py new file mode 100644 index 00000000..0d21ac7b --- /dev/null +++ b/designs/issue_912_def_test_missaligned/issue_regression.py @@ -0,0 +1,32 @@ +# Copyright 2022 Arman Avetisyan +# 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 sys + + +with open(sys.argv[1] + "/openlane.log") as f: + content = f.read() + if ( + content.find( + "Pin coordinate 9861 for pin manufacturing_grid_missaligned_pin does not match the manufacturing grid" + ) + is not -1 + ) and ( + content.find( + "Pin coordinate 10141 for pin manufacturing_grid_missaligned_pin does not match the manufacturing grid" + ) + is not -1 + ): + sys.exit(0) + else: + sys.exit("Didn't match the log") diff --git a/designs/issue_912_def_test_missaligned/src b/designs/issue_912_def_test_missaligned/src new file mode 120000 index 00000000..eab7accc --- /dev/null +++ b/designs/issue_912_def_test_missaligned/src @@ -0,0 +1 @@ +../issue_892_def_test/src \ No newline at end of file diff --git a/docs/source/issue_regression_tests.md b/docs/source/issue_regression_tests.md index 48bde97c..36a3b4b3 100644 --- a/docs/source/issue_regression_tests.md +++ b/docs/source/issue_regression_tests.md @@ -1,5 +1,8 @@ # Issue regression tests -Issue regression tests are used to test issues that happened in the past, that might return. Regression testing also allows to extend the options/configs that is not covered currently. +Issue regression tests are used to test issues that happened in the past, that might return by changes introduced in the future. Regression testing also allows to extend the options/configs that is not covered currently. To run issue regression flow execute `make issue_regression_all` outside of docker image. # Issue regression tests flow -If -run_hooks is specified, after a successful flow run hooks/post_run.py script is run. Script may contain any checks required. Multiple issues can be convered by single script. As a reference def_test design can be used. It's a simple inverter, with pins locations specified in DEF. Script just checks for pins to be in locations in tempalte DEF. + +Another type of checks is intentionally broken test cases which test the OpenLane's ability to warn user about possible issues. Example issue is: `issue_912_def_test_missaligned`. Entry point is `run_issue_regressions.py` which will run all designs matching `designs/issue_*` pattern. After the run was compeleted with or without errors or fails, then for that design `issue_regression.py` is ran in OpenROAD Python environment. If flow failed and `issue_regression.py` does not exist, then issue regression failed. As `-run_hooks` will run only after successful flow, but the `issue_regression.py` will be ran in both failed and successful case. OpenLane users may want to use `-run_hooks`, while issue_regression is designed to be used only by regression flow. + +If `-run_hooks` is specified, after a successful flow run `hooks/post_run.py` script is run. Script may contain any checks required. Multiple issues can be covered by single script. As a reference `issue_892_def_test` can be used. diff --git a/regression_results/README.md b/regression_results/README.md index c5b9f45c..fa9d9f57 100644 --- a/regression_results/README.md +++ b/regression_results/README.md @@ -34,7 +34,7 @@ The script can be used in two ways python3 run_designs.py --threads 4 spm xtea PPU APU ``` - You can run the defualt test set consisting of all designs under [./designs](../designs/) through running the following command along with any of the flags: + You can run the default test set consisting of all designs under [./designs](../designs/) through running the following command along with any of the flags: ```bash python3 run_design.py --defaultTestSet diff --git a/run_designs.py b/run_designs.py index 52d0ea8f..1d2f0a63 100755 --- a/run_designs.py +++ b/run_designs.py @@ -32,7 +32,7 @@ import scripts.utils.utils as utils @click.option("-c", "--config_tag", default="config", help="Configuration file") @click.option("-r", "--regression", default=None, help="Regression file") @click.option("-t", "--tag", default="regression", help="Tag for the log file") -@click.option("-j", "--threads", help="Number of designs in parallel") +@click.option("-j", "--threads", default=1, help="Number of designs in parallel") @click.option( "-p", "--configuration_parameters", diff --git a/run_issue_regressions.py b/run_issue_regressions.py new file mode 100644 index 00000000..8f8c45c4 --- /dev/null +++ b/run_issue_regressions.py @@ -0,0 +1,156 @@ +# Copyright 2022 Arman Avetisyan +# 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 glob # For finding all issue_regression designs +import subprocess # For running the flow +import os # For checking if file exists +import click # For command line parsing +import json + + +test_cases = glob.glob("./designs/issue_*") + +# TODO: If command is get designs +# print(json.dumps({"design": designs})) +# else if design specified +# + + +@click.group() +def cli(): + pass + + +# Note: Following command is executed outside of OpenRoad, so you can't run the ./flow.tcl +@click.command("get_matrix") +def get_matrix(): + print(json.dumps({"design": test_cases})) + + +cli.add_command(get_matrix) + + +@click.command("run") +@click.argument("test_case") +def run_test_case_cmd(test_case): + run_test_case(test_case) + + +cli.add_command(run_test_case_cmd) + + +@click.command("run_all") +def run_all(): + for test_case in test_cases: + run_test_case(test_case) + print("Issue regression flow completed without errors") + + +cli.add_command(run_all) + + +def run_test_case(test_case): + # test_case is the path to design + # test_case_name is name of design to test + # It is assumed that every test case is inside designs path + + # ------------------------------- + # 0. Calculate names + # ------------------------------- + result = "" + test_case_issue_regression_script = test_case + "/issue_regression.py" + script_exists = os.path.isfile(test_case_issue_regression_script) + (_, test_case_name) = os.path.split(test_case) + # print("test_case_name", test_case_name) + logpath = "./regression_results/issue_regression_" + test_case_name + ".log" + logpath_check = ( + "./regression_results/issue_regression_" + test_case_name + "_check.log" + ) + # ------------------------------- + # 1. Run the flow + # ------------------------------- + try: + logfile = open(logpath, "w") + print("Running test case:", test_case_name, "Logfile:", logpath) + result = subprocess.run( + [ + "./flow.tcl", + "-design", + test_case, + "-tag", + "issue_regression_run", + "-run_hooks", + "-overwrite", + ], + stdout=logfile, + stderr=subprocess.STDOUT, + check=True, + ) + except subprocess.CalledProcessError as err: + # ------------------------------- + # 2.1 If run was not successful, then run the issue_regression.py which + # will check if the fail was expected or not and it will also check the logs + # for errors to match + # 2.2 If issue_regression.py does not exist then it's enough for this design to pass LVS + # ------------------------------- + if script_exists: + result = err + print( + "./flow.tcl failed. This might be expected, as issue_regression.py may expect this" + ) + else: + print( + "./flow.tcl failed and issue_regression.py does not exist, therefore test case", + test_case, + "failed. Logfile:", + logpath, + ) + raise err + # ------------------------------- + # 3. Run the issue_regression.py. + # ------------------------------- + if script_exists: + print("Running", test_case_issue_regression_script, "Logfile:", logpath_check) + logfile_check = open(logpath_check, "w") + try: + subprocess.run( + [ + "openroad", + "-python", + test_case_issue_regression_script, + test_case + "/runs/issue_regression_run", + str(result.returncode), + ], + check=True, + stdout=logfile_check, + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as err: + # ------------------------------- + # 4. Run the issue_regression.py. If it errors out, log it and then raise an error + # ------------------------------- + print("Issue regression check failed, check log:", logpath_check) + raise err + print("Completed run successfully:", test_case) + else: + print( + "For design", + test_case, + "no regression script", + test_case_issue_regression_script, + "has been found", + ) + + +if __name__ == "__main__": + cli() diff --git a/scripts/apply_def_template.py b/scripts/apply_def_template.py index 183d3566..1d430c5b 100644 --- a/scripts/apply_def_template.py +++ b/scripts/apply_def_template.py @@ -13,95 +13,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import re import click import shutil -import subprocess +import defutil @click.command() @click.option("-t", "--def-template", "templatedef", required=True, help="Template DEF") +@click.option("-l", "--lef", "lef", required=True, help="LEF file") +@click.option("-lg", "--log", "logfile", required=True, help="Log output file") @click.argument("userdef") -def cli(templatedef, userdef): +def cli(templatedef, userdef, lef, logfile): userDEF = userdef templateDEF = templatedef - scriptsDir = os.path.dirname(__file__) - def remove_power_pins(DEF): - templateDEFOpener = open(DEF, "r") - if templateDEFOpener.mode == "r": - templateDEFSections = templateDEFOpener.read().split("PINS") - templateDEFOpener.close() - PINS = templateDEFSections[1].split("- ") - OUT_PINS = [" ;"] - cnt = 0 - for pin in PINS[1:]: - if pin.find("USE GROUND") + pin.find("USE POWER") == -2: - cnt += 1 - OUT_PINS.append(pin) - OUT_PINS[0] = " " + str(cnt) + OUT_PINS[0] + PINS[0].split(";")[1] - OUT_PINS[-1] = OUT_PINS[-1].replace("END ", "") - OUT_PINS[-1] = OUT_PINS[-1] + "END " - templateDEFSections[1] = "- ".join(OUT_PINS) - templateDEFOpener = open(DEF, "w") - templateDEFOpener.write("PINS".join(templateDEFSections)) - templateDEFOpener.close() + # Removed section to remove the power/ground pins as defutil:replace_pins implements this - newTemplateDEF = f"{userDEF}.template.tmp" - shutil.copy(templateDEF, newTemplateDEF) - templateDEF = newTemplateDEF - - templateDEF = f"{userDEF}.template.tmp" - remove_power_pins(templateDEF) - - subprocess.check_output( - [ - "openroad", - "-python", - f"{scriptsDir}/defutil.py", - "replace_pins", - "--output", - userDEF, - "--input-lef", - "/dev/null", - userDEF, - templateDEF, - ], - stderr=subprocess.PIPE, + defutil.replace_pins( + input_lef=lef, + logpath=logfile, + template_def=templateDEF, + source_def=userDEF, + output_def=f"{userDEF}.replace_pins.tmp", ) - # read template Def - templateDEFOpener = open(templateDEF, "r") - if templateDEFOpener.mode == "r": - templateDEFContent = templateDEFOpener.read() - templateDEFOpener.close() - - # read user Def - userDEFOpener = open(userDEF, "r") - if userDEFOpener.mode == "r": - userDEFContent = userDEFOpener.read() - userDEFOpener.close() - - def copyStringWithWord(word, f_rom, t_o): - pattern = re.compile(r"\b%s\b\s*\([^)]*\)\s*\([^)]*\)" % word) - instances = re.findall(pattern, f_rom) - if len(instances) == 1: - str_from = instances[0] - tmp = re.sub(pattern, str_from, t_o) - return tmp - return None - - # Copy DIEAREA - word = "DIEAREA" - userDEFContent = copyStringWithWord(word, templateDEFContent, userDEFContent) - - if userDEFContent is not None: - userDEFOpener = open(userDEF, "w") - userDEFOpener.write(userDEFContent) - userDEFOpener.close() - else: - raise Exception("DIEAREA not found in DEF") + # Call defutil to move die area + defutil.move_diearea( + template_def=templateDEF, + output_def=f"{userDEF}.replace_pins.tmp", + input_lef=lef, + ) + shutil.copy(f"{userDEF}.replace_pins.tmp", userDEF) if __name__ == "__main__": diff --git a/scripts/defutil.py b/scripts/defutil.py index defb7536..3ff5d688 100644 --- a/scripts/defutil.py +++ b/scripts/defutil.py @@ -1,4 +1,5 @@ # Copyright 2021 Efabless Corporation +# Copyright 2022 Arman Avetisyan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,6 +18,10 @@ import click import odb +import os # For checks of file existance +import shutil # For copy +import sys # For stderr + @click.group() def cli(): @@ -26,12 +31,15 @@ def cli(): class OdbReader(object): def __init__(self, lef_in, def_in): self.db = odb.dbDatabase.create() + self.lef = odb.read_lef(self.db, lef_in) self.sites = self.lef.getSites() + self.df = odb.read_def(self.db, def_in) self.block = self.db.getChip().getBlock() self.rows = self.block.getRows() self.dbunits = self.block.getDefUnits() + self.nets = self.block.getNets() @click.command("extract_core_dims") @@ -152,21 +160,347 @@ def merge_pins(output, input_lef, def_one, def_two): f.write(merge_item_section("PINS", def_one_str, def_two_str)) -@click.command("replace_pins") -@click.option("-o", "--output", default="./out.def") +@click.command("move_diearea") @click.option("-l", "--input-lef", required=True, help="Merged LEF file") -@click.argument("def_one") -@click.argument("def_two") -def replace_pins(output, input_lef, def_one, def_two): - # TODO: Rewrite in OpenDB if possible - def_one_str = open(def_one).read() - def_two_str = open(def_two).read() - - with open(output, "w") as f: - f.write(merge_item_section("PINS", def_one_str, def_two_str, replace_two=True)) +@click.option( + "-o", + "--output-def", + required=True, + help="Output DEF. File should exist. Die area will be applied to this DEF", +) +@click.option("-i", "--template-def", required=True, help="Input DEF") +def move_diearea_command(input_lef, output_def, template_def): + """ + Move die area from input def to output def + """ + move_diearea(input_lef, output_def, template_def) -cli.add_command(replace_pins) +def move_diearea(input_lef, output_def, template_def): + if not os.path.isfile(template_def): + print("LEF file ", input_lef, " not found") + raise FileNotFoundError + if not os.path.isfile(template_def): + print("Input DEF file ", template_def, " not found") + raise FileNotFoundError + if not os.path.isfile(output_def): + print("Output DEF file ", output_def, " not found") + raise FileNotFoundError + + source_db = odb.dbDatabase.create() + destination_db = odb.dbDatabase.create() + odb.read_lef(source_db, input_lef) + odb.read_lef(destination_db, input_lef) + + odb.read_def(source_db, template_def) + odb.read_def(destination_db, output_def) + + assert ( + source_db.getTech().getManufacturingGrid() + == destination_db.getTech().getManufacturingGrid() + ) + assert ( + source_db.getTech().getDbUnitsPerMicron() + == destination_db.getTech().getDbUnitsPerMicron() + ) + + diearea = source_db.getChip().getBlock().getDieArea() + output_block = destination_db.getChip().getBlock() + output_block.setDieArea(diearea) + # print("Applied die area: ", destination_db.getChip().getBlock().getDieArea().ur(), destination_db.getChip().getBlock().getDieArea().ll(), file=sys.stderr) + + assert odb.write_def(output_block, output_def) == 1 + + +def check_pin_grid( + manufacturing_grid, dbu_per_microns, pin_name, pin_coordinate, logfile +): + if (pin_coordinate % manufacturing_grid) != 0: + print( + "[ERROR]: Pin coordinate", + pin_coordinate, + "for pin", + pin_name, + "does not match the manufacturing grid", + file=sys.stderr, + ) + print( + "[ERROR]: Pin coordinate", + pin_coordinate, + "for pin", + pin_name, + "does not match the manufacturing grid", + file=logfile, + ) # IDK how to do this + return True + + +# openroad -python scripts/defutil.py replace_pins -o output.def -l designs/def_test/runs/RUN_2022.01.30_12.32.26/tmp/merged.lef designs/def_test/runs/RUN_2022.01.30_12.32.26/tmp/floorplan/4-io.def designs/def_test/def_test.def + + +@click.command("replace_pins") +@click.option("-o", "--output-def", default="./out.def", help="Destination DEF path") +@click.option("-l", "--input-lef", required=True, help="Merged LEF file") +@click.option("-lg", "--log", "logpath", help="Log output file") +@click.argument("source_def") +@click.argument("template_def") +def replace_pins_command(output_def, input_lef, logpath, source_def, template_def): + """ + Copies source_def to output, then if same pin exists in template def and first def then, it's written to output def + + Example to run: + openroad -python scripts/defutil.py replace_pins -o output.def -l designs/def_test/runs/RUN_2022.01.30_12.32.26/tmp/merged.lef designs/def_test/runs/RUN_2022.01.30_12.32.26/tmp/floorplan/4-io.def designs/def_test/def_test.def --log defutil.log + Note: Assumes that all pins are on metal layers and via pins dont exist. + Note: It assumes that all pins are rectangles, not polygons. + Note: This tool assumes no power pins exist in template def. + Note: It should leave pins in source_def as-is if no pin in template def is found. + Note: It assumes only one port with the same name exist. + Note: It assumes that pin names matches the net names in template DEF. + """ + replace_pins(output_def, input_lef, logpath, source_def, template_def) + + +cli.add_command(replace_pins_command) + +# Note: If you decide to change any parameters, also change replace_pins_command's +def replace_pins(output_def, input_lef, logpath, source_def, template_def): + # -------------------------------- + # 0. Sanity check: Check for all defs and lefs to exist + # I removed the output def to NOT exist check, as it was making testing harder + # -------------------------------- + + if not os.path.isfile(input_lef): + print("LEF file ", input_lef, " not found") + raise FileNotFoundError + if not os.path.isfile(source_def): + print("First DEF file ", source_def, " not found") + raise FileNotFoundError + if not os.path.isfile(template_def): + print("Template DEF file ", template_def, " not found") + raise FileNotFoundError + if logpath is None: + logfile = sys.stdout + else: + logfile = open(logpath, "w+") + + # -------------------------------- + # 1. Copy the one def to second + # -------------------------------- + print( + "[defutil.py:replace_pins] Creating output DEF based on first DEF", file=logfile + ) + shutil.copy(source_def, output_def) + + # -------------------------------- + # 2. Find list of all bterms in first def + # -------------------------------- + source_db = odb.dbDatabase.create() + odb.read_lef(source_db, input_lef) + odb.read_def(source_db, source_def) + source_bterms = source_db.getChip().getBlock().getBTerms() + + manufacturing_grid = source_db.getTech().getManufacturingGrid() + dbu_per_microns = source_db.getTech().getDbUnitsPerMicron() + + print( + "Using manufacturing grid:", + manufacturing_grid, + "Using dbu per mircons: ", + dbu_per_microns, + file=logfile, + ) + + all_bterm_names = set() + + for source_bterm in source_bterms: + source_name = source_bterm.getName() + # TODO: Check for pin name matches net name + # print("Bterm", source_name, "is declared as", source_bterm.getSigType()) + + # -------------------------------- + # 3. Check no bterms should be marked as power, because it is assumed that caller already removed them + # -------------------------------- + if (source_bterm.getSigType() == "POWER") or ( + source_bterm.getSigType() == "GROUND" + ): + print( + "Bterm", + source_name, + "is declared as", + source_bterm.getSigType(), + "ignoring it", + file=logfile, + ) + continue + all_bterm_names.add(source_name) + + print( + "[defutil.py:replace_pins] Found", + len(all_bterm_names), + "block terminals in first def", + file=logfile, + ) + + # -------------------------------- + # 4. Read the template def + # -------------------------------- + template_db = odb.dbDatabase.create() + odb.read_lef(template_db, input_lef) + odb.read_def(template_db, template_def) + template_bterms = template_db.getChip().getBlock().getBTerms() + + assert ( + source_db.getTech().getManufacturingGrid() + == template_db.getTech().getManufacturingGrid() + ) + assert ( + source_db.getTech().getDbUnitsPerMicron() + == template_db.getTech().getDbUnitsPerMicron() + ) + # -------------------------------- + # 5. Create a dict with net -> pin location. Check for only one pin location to exist, overwise return an error + # -------------------------------- + template_bterm_locations = dict() + + for template_bterm in template_bterms: + template_name = template_bterm.getName() + template_pins = template_bterm.getBPins() + + # TODO: Check for pin name matches net name + for template_pin in template_pins: + boxes = template_pin.getBoxes() + + for box in boxes: + layer = box.getTechLayer().getName() + if template_name not in template_bterm_locations: + template_bterm_locations[template_name] = [] + template_bterm_locations[template_name].append( + ( + layer, + box.xMin(), + box.yMin(), + box.xMax(), + box.yMax(), + ) + ) + + print( + "[defutil.py:replace_pins] Found template_bterms: ", + len(template_bterm_locations), + file=logfile, + ) + + for template_bterm_name in template_bterm_locations: + print( + template_bterm_name, + ": ", + template_bterm_locations[template_bterm_name], + file=logfile, + ) + + # -------------------------------- + # 6. Modify the pins in out def, according to dict + # -------------------------------- + output_db = odb.dbDatabase.create() + odb.read_lef(output_db, input_lef) + odb.read_def(output_db, output_def) + output_tech = output_db.getTech() + output_block = output_db.getChip().getBlock() + output_bterms = output_block.getBTerms() + grid_errors = False + for output_bterm in output_bterms: + name = output_bterm.getName() + output_bpins = output_bterm.getBPins() + + if name in template_bterm_locations and name in all_bterm_names: + for output_bpin in output_bpins: + odb.dbBPin.destroy(output_bpin) + + for template_bterm_location_tuple in template_bterm_locations[name]: + layer = output_tech.findLayer(template_bterm_location_tuple[0]) + + # -------------------------------- + # 6.2 Create new pin + # -------------------------------- + + output_new_bpin = odb.dbBPin.create(output_bterm) + + print( + "For:", + name, + "Wrote on layer:", + layer.getName(), + "coordinates: ", + template_bterm_location_tuple[1], + template_bterm_location_tuple[2], + template_bterm_location_tuple[3], + template_bterm_location_tuple[4], + file=logfile, + ) + grid_errors = ( + check_pin_grid( + manufacturing_grid, + dbu_per_microns, + name, + template_bterm_location_tuple[1], + logfile, + ) + or grid_errors + ) + grid_errors = ( + check_pin_grid( + manufacturing_grid, + dbu_per_microns, + name, + template_bterm_location_tuple[2], + logfile, + ) + or grid_errors + ) + grid_errors = ( + check_pin_grid( + manufacturing_grid, + dbu_per_microns, + name, + template_bterm_location_tuple[3], + logfile, + ) + or grid_errors + ) + grid_errors = ( + check_pin_grid( + manufacturing_grid, + dbu_per_microns, + name, + template_bterm_location_tuple[4], + logfile, + ) + or grid_errors + ) + odb.dbBox.create( + output_new_bpin, + layer, + template_bterm_location_tuple[1], + template_bterm_location_tuple[2], + template_bterm_location_tuple[3], + template_bterm_location_tuple[4], + ) + output_new_bpin.setPlacementStatus("PLACED") + else: + print( + "[defutil.py:replace_pins] Not found", + name, + "in template def, but found in output def. Leaving as-is", + file=logfile, + ) + + if grid_errors: + sys.exit("[ERROR]: Grid errors happened. Check log for grid errors.") + # -------------------------------- + # 7. Write back the output def + # -------------------------------- + print("[defutil.py:replace_pins] Writing output def to: ", output_def, file=logfile) + assert odb.write_def(output_block, output_def) == 1 @click.command("remove_components") diff --git a/scripts/tcl_commands/floorplan.tcl b/scripts/tcl_commands/floorplan.tcl index 348433df..9bbd0200 100755 --- a/scripts/tcl_commands/floorplan.tcl +++ b/scripts/tcl_commands/floorplan.tcl @@ -252,13 +252,16 @@ proc chip_floorplan {args} { proc apply_def_template {args} { if { [info exists ::env(FP_DEF_TEMPLATE)] } { - puts_info "Applying DEF template..." + set log [index_file $::env(floorplan_logs)/apply_def_template.log] + set def [index_file $::env(floorplan_tmpfiles)/apply_def_template.def] + puts_info "Applying DEF template. See log: $log" try_catch $::env(OPENROAD_BIN) -python $::env(SCRIPTS_DIR)/apply_def_template.py\ + --lef $::env(MERGED_LEF) \ --def-template $::env(FP_DEF_TEMPLATE)\ + --log $log \ $::env(CURRENT_DEF) } - } proc run_power_grid_generation {args} {