Skip to content

Commit

Permalink
pytest: scripted layout tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasc-ubc committed Dec 11, 2023
1 parent 3d7e5a6 commit f98e7df
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 126 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/run-layout-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Run SiEPIC-Tool Layout tests

on:
workflow_dispatch:
push:
paths:
- '**'
branches:
- '**'
pull_request:
branches:
- main
- master


jobs:
test_layouts:
runs-on: ubuntu-latest

steps:
- name: checkout repo content
uses: actions/checkout@v3

# can also specify python version if needed
- name: setup python
uses: actions/setup-python@v4

- name: install python packages
run: |
python -m pip install --upgrade pip
pip install klayout numpy scipy pytest
- name: Test with pytest
run: pytest klayout_dot_config/tech

- name: run python test script
run: |
python $GITHUB_WORKSPACE/klayout_dot_config/tech/GSiP/pymacros/tests/example_circuit.py
11 changes: 11 additions & 0 deletions klayout_dot_config/tech/GSiP/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
print('SiEPIC-GSiP PDK Python module: siepic_gsip_pdk, KLayout technology: GSipP')

# Load the KLayout technology, when running in Script mode
import pya, os
tech = pya.Technology().create_technology('GSiP')
tech.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'GSiP.lyt'))

# then import all the technology modules
from . import pymacros


86 changes: 67 additions & 19 deletions klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,45 +33,93 @@
self.cell.shapes(Layerm1N).insert(pya.Polygon(arc(w_m1_in, angle_min_doping, angle_max_doping) + [pya.Point(0, 0)]).transformed(t))
"""

import os
import pcells_GSiP
folder = 'pcells_GSiP'
verbose = False

import os, sys, pathlib

from SiEPIC._globals import KLAYOUT_VERSION, KLAYOUT_VERSION_3
if KLAYOUT_VERSION < 28:
question = pya.QMessageBox()
question.setStandardButtons(pya.QMessageBox.Ok)
question.setText("This PDK is not compatible with older versions (<0.28) of KLayout.")
KLayout_link0='https://www.klayout.de/build.html'
question.setInformativeText("\nThis PDK is not compatible with older versions (<0.28) of KLayout.\nPlease download an install the latest version, from %s" % (KLayout_link0))
pya.QMessageBox_StandardButton(question.exec_())


dir_path = os.path.dirname(os.path.realpath(__file__))
if dir_path not in sys.path:
sys.path.append(dir_path)

files = [f for f in os.listdir(os.path.join(os.path.dirname(
os.path.realpath(__file__)),folder)) if '.py' in pathlib.Path(f).suffixes and '__init__' not in f]
import importlib
pcells_GSiP = importlib.import_module(folder)
importlib.invalidate_caches()
pcells_=[]
for f in files:
module = '%s.%s' % (folder, f.replace('.py','')) ### folder name ###
if verbose:
print(' - found module: %s' % module)
m = importlib.import_module(module)
if verbose:
print(m)
pcells_.append(importlib.reload(m))

import pya

class GSiP(pya.Library):
def __init__(self):
tech_name = "GSiP"
library = tech_name
self.technology=tech_name

print("Initializing '%s' Library." % library)


if verbose:
print("Initializing '%s' Library." % library)

# Set the description
self.description = "SiEPIC Generic SiP"

# Save the path, used for loading WAVEGUIDES.XML
import os
self.path = os.path.dirname(os.path.realpath(__file__))

# Import all the GDS files from the tech folder "gds"
# Import all the GDS files from the tech folder
import os, fnmatch
dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../gds/building_blocks")
search_str = '*' + '.gds'
dir_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../gds/building_blocks"))
if verbose:
print(' library path: %s' % dir_path)
search_str = '*.[Oo][Aa][Ss]' # OAS
for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
for filename in fnmatch.filter([f.lower() for f in filenames], search_str):
for filename in fnmatch.filter(filenames, search_str):
file1=os.path.join(root, filename)
print(" - reading %s" % file1 )
if verbose:
print(" - reading %s" % file1 )
self.layout().read(file1)

for attr, value in pcells_GSiP.__dict__.items():
try:
if value.__module__.split('.')[0] == 'pcells_GSiP':
print('Registered pcell: '+attr)
self.layout().register_pcell(attr, value())
except:
pass

search_str = '*.[Gg][Dd][Ss]' # GDS
for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
for filename in fnmatch.filter(filenames, search_str):
file1=os.path.join(root, filename)
if verbose:
print(" - reading %s" % file1 )
self.layout().read(file1)

# Create the PCell declarations
for m in pcells_:
mm = m.__name__.replace('%s.' % folder,'')
mm2 = m.__name__+'.'+mm+'()'
if verbose:
print(' - register_pcell %s, %s' % (mm,mm2))
self.layout().register_pcell(mm, eval(mm2))

if verbose:
print(' done with pcells')

# Register us the library with the technology name
# If a library with that name already existed, it will be replaced then.
self.register(library)


GSiP()
8 changes: 8 additions & 0 deletions klayout_dot_config/tech/GSiP/pymacros/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
print('SiEPIC-GSiP PDK Python module: pymacros')

from . import GSiP_Library





23 changes: 1 addition & 22 deletions klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,4 @@
import os, sys
import SiEPIC
try:
import siepic_tools
except:
pass

op_tag = "" #operation tag which defines whether we are loading library in script or GUI env

try:
# import pya from klayout
import pya
if("Application" in str(dir(pya))):
from SiEPIC.utils import get_technology_by_name
op_tag = "GUI"
#import pya functions
else:
raise ImportError

except:
import klayout.db as pya
from zeropdk import Tech
op_tag = "script"
lyp_filepath = os.path(str(os.path(os.path.dirname(os.path.realpath(__file__))).parent) + r"/klayout_Layers_GSiP.lyp")
print(lyp_filepath)
from SiEPIC.utils import get_technology_by_name
85 changes: 0 additions & 85 deletions klayout_dot_config/tech/GSiP/pymacros/tests/example_circuit.py

This file was deleted.

102 changes: 102 additions & 0 deletions klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'''
--- Simple MZI ---
by Lukas Chrostowski, 2020-2023
Example simple script to
- create a new layout with a top cell
- create an MZI
- export to OASIS for submission to fabrication
using SiEPIC-Tools function including connect_pins_with_waveguide and connect_cell
usage:
- run this script in KLayout Application
'''

from pya import *

def example_circuit():
designer_name = 'Test'
top_cell_name = 'GSiP_%s' % designer_name

import pya

import SiEPIC
from SiEPIC._globals import Python_Env
from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
from SiEPIC.utils.layout import new_layout, floorplan
from SiEPIC.extend import to_itype
from SiEPIC.verification import layout_check

import os
path = os.path.dirname(os.path.realpath(__file__))

tech_name = 'GSiP'

if SiEPIC.__version__ < '0.5.4':
raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")

if Python_Env == 'Script':
# Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
import sys
sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
import GSiP

'''
Create a new layout, with a top cell, and Draw the floor plan
'''
cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
floorplan(cell, 605e3, 410e3)

dbu = ly.dbu

from SiEPIC.scripts import connect_pins_with_waveguide, connect_cell
waveguide_type='Strip TE 1550 nm'

# Load cells from library
cell_ebeam_gc = ly.create_cell('Grating_Coupler_13deg_TE_1550_Oxide', tech_name)
cell_ebeam_y = ly.create_cell('YBranch_te1550', tech_name)

# grating couplers, place at absolute positions
x,y = 60000, 15000
t = Trans(Trans.R0,x,y)
instGC1 = cell.insert(CellInstArray(cell_ebeam_gc.cell_index(), t))
t = Trans(Trans.R0,x,y+127000)
instGC2 = cell.insert(CellInstArray(cell_ebeam_gc.cell_index(), t))

# automated test label
text = Text ("opt_in_TE_1550_device_%s_MZI1" % designer_name, t)
cell.shapes(ly.layer(ly.TECHNOLOGY['Text'])).insert(text).text_size = 5/dbu

# Y branches:
instY1 = connect_cell(instGC1, 'opt_wg', cell_ebeam_y, 'opt1')
instY1.transform(Trans(20000,0))
instY2 = connect_cell(instGC2, 'opt_wg', cell_ebeam_y, 'opt1')
instY2.transform(Trans(20000,0))

# Waveguides:
connect_pins_with_waveguide(instGC1, 'opt_wg', instY1, 'opt1', waveguide_type=waveguide_type)
connect_pins_with_waveguide(instGC2, 'opt_wg', instY2, 'opt1', waveguide_type=waveguide_type)
connect_pins_with_waveguide(instY1, 'opt2', instY2, 'opt3', waveguide_type=waveguide_type)
connect_pins_with_waveguide(instY1, 'opt3', instY2, 'opt2', waveguide_type=waveguide_type,turtle_B=[25,-90])

# Zoom out
zoom_out(cell)

# Verify
num_errors = layout_check(cell=cell, verbose=True, GUI=True)
print('Number of errors: %s' % num_errors)

# Save
filename = os.path.splitext(os.path.basename(__file__))[0]
file_out = export_layout(cell, path, filename, format='oas', screenshot=True)

return num_errors

def test_example_circuit():
assert example_circuit() == 0

if __name__ == "__main__":
example_circuit()

0 comments on commit f98e7df

Please sign in to comment.