Skip to content

Commit

Permalink
feat(precheck): initial version
Browse files Browse the repository at this point in the history
perform presubmission checks on custom GDS files submitted to Tiny Tapeout
  • Loading branch information
urish committed Nov 11, 2023
1 parent 359dabc commit 5f436d7
Show file tree
Hide file tree
Showing 12 changed files with 1,298 additions and 0 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/test-precheck.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Precheck Tests

on:
push:
pull_request:
workflow_dispatch:

jobs:
test-precheck:
runs-on: ubuntu-latest
env:
PDK_ROOT: /home/runner/pdk
PDK_VERSION: dd7771c384ed36b91a25e9f8b314355fc26561be

steps:
- name: Check out code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
cache: 'pip'

- name: Install dependencies
working-directory: precheck
run: pip install -r requirements.txt

- name: Install Sky130 PDK
uses: TinyTapeout/volare-action@v1
with:
pdk_name: sky130
pdk_version: ${{ env.PDK_VERSION }}
pdk_root: ${{ env.PDK_ROOT }}

- name: Install nix
uses: cachix/install-nix-action@v20

- name: Run tests
working-directory: precheck
run: nix-shell --run "pytest"
2 changes: 2 additions & 0 deletions precheck/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
.pytest_cache
16 changes: 16 additions & 0 deletions precheck/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{ pkgs ? import
(builtins.fetchGit {
url = "https://github.com/NixOS/nixpkgs.git";
rev = "f2bd8adf7b78d7616b52d0ef08865c7c2fcf189d";
})
{ }
, magic ? import ./nix/magic.nix { inherit pkgs; }
,
}:

pkgs.mkShell {
buildInputs = [
pkgs.klayout
magic
];
}
42 changes: 42 additions & 0 deletions precheck/klayout_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import xml.etree.ElementTree as ET
from typing import Dict


class LayerInfo:
def __init__(self, name: str, source: str, layer: int, data_type: int):
self.name = name
self.source = source
self.layer = layer
self.data_type = data_type

def __repr__(self):
return f"LayerInfo(name={self.name}, source={self.source}, layer={self.layer}, data_type={self.data_type})"


def parse_lyp_layers(lyp_file: str):
with open(lyp_file) as f:
xml_content = f.read()
root = ET.fromstring(xml_content)

layers_dict: Dict[str, LayerInfo] = {}

for properties in root.findall("properties"):
name = properties.find("name")
source = properties.find("source")
valid = properties.find("valid")

if valid is not None and valid.text == "false":
continue

if name is not None and source is not None:
name_key = name.text.split("-")[0].strip()
layer, data_type = source.text.split("@")[0].split("/")
# Add the 'source' text as the value in the dictionary
layers_dict[name_key] = LayerInfo(
name=name.text,
source=source.text,
layer=int(layer),
data_type=int(data_type),
)

return layers_dict
77 changes: 77 additions & 0 deletions precheck/magic_drc.tcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: 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.
# SPDX-License-Identifier: Apache-2.0


set GDS_UT_PATH [lindex $argv 6]
set DESIGN_NAME [lindex $argv 7]
set PDK_PATH [lindex $argv 8]
set DRC_REPORT [lindex $argv 9]
set DRC_MAG [lindex $argv 10]

gds read $GDS_UT_PATH

set fout [open $DRC_REPORT w]
set oscale [cif scale out]
set cell_name $DESIGN_NAME
magic::suspendall
puts stdout "\[INFO\]: Loading $cell_name\n"
flush stdout
load $cell_name
select top cell
expand
drc euclidean on
drc style drc(full)
drc check
set drc_result [drc listall why]


set count 0
puts $fout "$cell_name"
puts $fout "----------------------------------------"
foreach {errtype coordlist} $drc_result {
puts $fout $errtype
puts $fout "----------------------------------------"
foreach coord $coordlist {
set bllx [expr {$oscale * [lindex $coord 0]}]
set blly [expr {$oscale * [lindex $coord 1]}]
set burx [expr {$oscale * [lindex $coord 2]}]
set bury [expr {$oscale * [lindex $coord 3]}]
set coords [format " %.3f %.3f %.3f %.3f" $bllx $blly $burx $bury]
puts $fout "$coords"
set count [expr {$count + 1} ]
}
puts $fout "----------------------------------------"
}

puts $fout "\[INFO\]: COUNT: $count"
puts $fout "\[INFO\]: Should be divided by 3 or 4"

puts $fout ""
close $fout

puts stdout "\[INFO\]: DRC Checking DONE ($DRC_REPORT)"
flush stdout

if {$count > 0} {
puts stderr "\[ERROR\]: $count DRC errors found"
puts stdout "\[INFO\]: Saving mag view with DRC errors($DRC_MAG)"
# WARNING: changes the name of the cell; keep as last step
save $DRC_MAG
puts stdout "\[INFO\]: Saved"
flush stdout
exit -1
} else {
exit 0
}
86 changes: 86 additions & 0 deletions precheck/nix/magic.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2023 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.

# Copyright (c) 2003-2023 Eelco Dolstra and the Nixpkgs/NixOS contributors

# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{ pkgs ? import <nixpkgs> { }
,
}:

with pkgs; clangStdenv.mkDerivation rec {
name = "magic-vlsi";
rev = "11080465ad3759d4bdb3936026c797644c6f7f0b";

src = fetchFromGitHub {
owner = "RTimothyEdwards";
repo = "magic";
inherit rev;
sha256 = "sha256-zRIS8tFT52soPsySZdxVOX+5Pu8xriZeur0Gh8kdaXk=";
};

nativeBuildInputs = [ python3 tcsh gnused ];

# So here's the situation:
## * Magic will not compile without X11 libraries, even for headless use.
## * Magic with Cairo will use Cairo-X11 headers that are not available on
## pre-built Mac versions of Cairo which presume you're going to use Quartz.
## -> Unintuitively, that means X11 libraries must be present on Mac, but not
## Cairo.
buildInputs = [
xorg.libX11
m4
mesa_glu
ncurses
tcl
tcsh
tk
] ++ lib.optionals stdenv.isLinux [ cairo ];

configureFlags = [
"--with-tcl=${tcl}"
"--with-tk=${tk}"
"--disable-werror"
];

preConfigure = ''
# nix shebang fix
patchShebangs ./scripts
# "Precompute" git rev-parse HEAD
sed -i 's@`git rev-parse HEAD`@${rev}@' ./scripts/defs.mak.in
'';

NIX_CFLAGS_COMPILE = "-Wno-implicit-function-declaration -Wno-parentheses -Wno-macro-redefined";

# Fairly sure this is deprecated?
enableParallelBuilding = true;
}
121 changes: 121 additions & 0 deletions precheck/precheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import subprocess

import klayout.db as pya
import klayout.rdb as rdb
from klayout_tools import parse_lyp_layers

PDK_ROOT = os.getenv("PDK_ROOT")
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp"
REPORTS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "reports")


def magic_drc(gds: str, toplevel: str):
logging.info(f"Running magic DRC on {gds} (module={toplevel})")

magic = subprocess.run(
[
"magic",
"-noconsole",
"-dnull",
"-rcfile",
f"{PDK_ROOT}/{PDK_NAME}/libs.tech/magic/{PDK_NAME}.magicrc",
"magic_drc.tcl",
gds,
toplevel,
PDK_ROOT,
f"{REPORTS_PATH}/magic_drc.txt",
f"{REPORTS_PATH}/magic_drc.mag",
],
)

if magic.returncode != 0:
logging.error("Magic DRC failed")
return False

return True


def klayout_drc(gds: str, check: str):
logging.info(f"Running klayout {check} on {gds}")
report_file = f"{REPORTS_PATH}/drc_{check}.xml"
klayout = subprocess.run(
[
"klayout",
"-b",
"-r",
f"tech-files/{PDK_NAME}_mr.drc",
"-rd",
f"{check}=true",
"-rd",
f"input={gds}",
"-rd",
f"report={report_file}",
],
)
if klayout.returncode != 0:
logging.error(f"Klayout {check} failed")
return False

report = rdb.ReportDatabase().load(report_file)

if report.num_items() > 0:
logging.error(
f"Klayout {check} failed with {report.num_items()} DRC violations"
)
return False

return True


def klayout_checks(gds: str):
layout = pya.Layout()
layout.read(gds)
layers = parse_lyp_layers(LYP_FILE)

logging.info("Running forbidden layer check...")
forbidden_layers = [
"met5.drawing",
"met5.pin",
"met5.label",
]

had_error = False
for layer in forbidden_layers:
layer_info = layers[layer]
logging.info(f"* Checking {layer_info.name}")
layer_index = layout.find_layer(layer_info.layer, layer_info.data_type)
if layer_index is not None:
logging.error(f"Forbidden layer {layer} found in {gds}")
had_error = True

if had_error:
logging.error("Klayout checks failed")
return not had_error


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--gds", required=True)
parser.add_argument("--toplevel", required=True)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info(f"PDK_ROOT: {PDK_ROOT}")

assert magic_drc(args.gds, args.toplevel)

assert klayout_drc(args.gds, "feol")
assert klayout_drc(args.gds, "beol")
assert klayout_drc(args.gds, "offgrid")

assert klayout_checks(args.gds)

logging.info(f"Precheck passed for {args.gds}! 🎉")


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions precheck/reports/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
magic_drc.mag
magic_drc.txt
drc_feol.xml
drc_beol.xml
drc_offgrid.xml
Loading

0 comments on commit 5f436d7

Please sign in to comment.