| # 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. |
| # 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 re |
| import click |
| |
| import odb |
| |
| import os # For checks of file existance |
| import shutil # For copy |
| import sys # For stderr |
| |
| |
| @click.group() |
| def cli(): |
| pass |
| |
| |
| 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") |
| @click.option("-o", "--output-data", required=True, help="Output") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("input_def") |
| def extract_core_dims(output_data, input_lef, input_def): |
| reader = OdbReader(input_lef, input_def) |
| core_area = reader.block.getCoreArea() |
| |
| with open(output_data, "w") as f: |
| print( |
| f"{core_area.dx() / reader.dbunits} {core_area.dy() / reader.dbunits}", |
| file=f, |
| ) |
| |
| |
| cli.add_command(extract_core_dims) |
| |
| |
| @click.command("mark_component_fixed") |
| @click.option( |
| "-c", "--cell-name", required=True, help="Cell name of the components to mark fixed" |
| ) |
| @click.option("-o", "--output", default="./out.def") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("input_def") |
| def mark_component_fixed(cell_name, output, input_lef, input_def): |
| reader = OdbReader(input_lef, input_def) |
| instances = reader.block.getInsts() |
| for instance in instances: |
| if instance.getMaster().getName() == cell_name: |
| instance.setPlacementStatus("FIRM") |
| |
| assert odb.write_def(reader.block, output) == 1 |
| |
| |
| cli.add_command(mark_component_fixed) |
| |
| |
| def merge_item_section( |
| item: str, def_one_str: str, def_two_str: str, replace_two: bool = False |
| ) -> str: |
| import re |
| |
| section_start_rx = re.compile(rf"{item}\s+(\d+)\s*;\s*") |
| section_end_rx = re.compile(rf"END\s+{item}") |
| |
| def_one_lines = def_one_str.splitlines() |
| def_two_lines = def_two_str.splitlines() |
| |
| collecting = False |
| def_two_out_lines = [] |
| def_two_count = 0 |
| for line in def_two_lines: |
| start_match = section_start_rx.search(line) |
| end_match = section_end_rx.search(line) |
| if end_match is not None: |
| collecting = False |
| break |
| if collecting: |
| def_two_out_lines.append(line) |
| if start_match is not None: |
| def_two_count = int(start_match[1]) |
| collecting = True |
| |
| # assert(len(def_two_out_lines) == def_two_count) # sanity check |
| final_out_lines = [] |
| |
| def_one_count = 0 |
| for line in def_one_lines: |
| start_match = section_start_rx.search(line) |
| end_match = section_end_rx.search(line) |
| if start_match is not None: |
| def_one_count = int(start_match[1]) |
| final_count = def_one_count |
| if not replace_two: |
| final_count += def_two_count |
| final_out_lines.append(f"{item} {final_count} ;") |
| elif end_match is not None: |
| if not replace_two: |
| final_out_lines += def_two_out_lines |
| final_out_lines.append(f"END {item}") |
| else: |
| final_out_lines.append(line) |
| |
| return "\n".join(final_out_lines) |
| |
| |
| @click.command("merge_components") |
| @click.option("-o", "--output", default="./out.def") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("def_one") |
| @click.argument("def_two") |
| def merge_components(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("COMPONENTS", def_one_str, def_two_str)) |
| |
| |
| cli.add_command(merge_components) |
| |
| |
| @click.command("merge_pins") |
| @click.option("-o", "--output", default="./out.def") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("def_one") |
| @click.argument("def_two") |
| def merge_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)) |
| |
| |
| @click.command("move_diearea") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @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) |
| |
| |
| 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") |
| @click.option( |
| "--rx/--not-rx", default=True, help="Treat instance name as a regular expression" |
| ) |
| @click.option( |
| "-n", |
| "--instance-name", |
| default=".+", |
| help="Instance name to be removed (Default '.+', as a regular expression, removes everything.)", |
| ) |
| @click.option("-o", "--output", default="./out.def") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("input_def") |
| def remove_components(rx, instance_name, output, input_lef, input_def): |
| reader = OdbReader(input_lef, input_def) |
| matcher = re.compile(instance_name if rx else f"^{re.escape(instance_name)}$") |
| instances = reader.block.getInsts() |
| for instance in instances: |
| name = instance.getName() |
| name_m = matcher.search(name) |
| if name_m is not None: |
| odb.dbInst.destroy(instance) |
| |
| assert odb.write_def(reader.block, output) == 1 |
| |
| |
| cli.add_command(remove_components) |
| |
| |
| @click.command("remove_nets") |
| @click.option( |
| "--rx/--not-rx", default=True, help="Treat net name as a regular expression" |
| ) |
| @click.option( |
| "-n", |
| "--net-name", |
| default=".+", |
| help="Net name to be removed (Default '.+', as a regular expression, removes everything.)", |
| ) |
| @click.option("-o", "--output", default="./out.def") |
| @click.option( |
| "--empty-only", is_flag=True, default=False, help="Only remove empty nets." |
| ) |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("input_def") |
| def remove_nets(rx, net_name, output, empty_only, input_lef, input_def): |
| reader = OdbReader(input_lef, input_def) |
| matcher = re.compile(net_name if rx else f"^{re.escape(net_name)}$") |
| nets = reader.block.getNets() |
| for net in nets: |
| name = net.getName() |
| name_m = matcher.search(name) |
| if name_m is not None: |
| if empty_only and len(net.getITerms()) > 0: |
| continue |
| # BTerms = PINS, if it has a pin we need to keep the net |
| if len(net.getBTerms()) > 0: |
| for port in net.getITerms(): |
| odb.dbITerm.disconnect(port) |
| else: |
| odb.dbNet.destroy(net) |
| |
| assert odb.write_def(reader.block, output) == 1 |
| |
| |
| cli.add_command(remove_nets) |
| |
| |
| @click.command("remove_pins") |
| @click.option( |
| "--rx/--not-rx", default=True, help="Treat pin name as a regular expression" |
| ) |
| @click.option( |
| "-n", |
| "--pin-name", |
| default=".+", |
| help="Pin name to be removed (Default '.+', as a regular expression, removes everything.)", |
| ) |
| @click.option("-o", "--output", default="./out.def") |
| @click.option("-l", "--input-lef", required=True, help="Merged LEF file") |
| @click.argument("input_def") |
| def remove_pins(rx, pin_name, output, input_lef, input_def): |
| reader = OdbReader(input_lef, input_def) |
| matcher = re.compile(pin_name if rx else f"^{re.escape(pin_name)}$") |
| pins = reader.block.getBTerms() |
| for pin in pins: |
| name = pin.getName() |
| name_m = matcher.search(name) |
| if name_m is not None: |
| odb.dbBTerm.destroy(pin) |
| |
| assert odb.write_def(reader.block, output) == 1 |
| |
| |
| cli.add_command(remove_pins) |
| |
| |
| if __name__ == "__main__": |
| cli() |