diff --git a/.github/workflows/run-layout-tests.yml b/.github/workflows/run-layout-tests.yml new file mode 100644 index 000000000..18a6a9157 --- /dev/null +++ b/.github/workflows/run-layout-tests.yml @@ -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 + + + diff --git a/klayout_dot_config/tech/GSiP/__init__.py b/klayout_dot_config/tech/GSiP/__init__.py new file mode 100644 index 000000000..fa595dd81 --- /dev/null +++ b/klayout_dot_config/tech/GSiP/__init__.py @@ -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 + + diff --git a/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py b/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py index 8ab4ee23f..82c4ff207 100644 --- a/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py +++ b/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py @@ -33,8 +33,40 @@ 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): @@ -42,36 +74,52 @@ 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() \ No newline at end of file diff --git a/klayout_dot_config/tech/GSiP/pymacros/__init__.py b/klayout_dot_config/tech/GSiP/pymacros/__init__.py new file mode 100644 index 000000000..0c62ae9ab --- /dev/null +++ b/klayout_dot_config/tech/GSiP/pymacros/__init__.py @@ -0,0 +1,8 @@ +print('SiEPIC-GSiP PDK Python module: pymacros') + +from . import GSiP_Library + + + + + diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py index 89b2bfb6e..bc72996d9 100644 --- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py +++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py @@ -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 diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/example_circuit.py b/klayout_dot_config/tech/GSiP/pymacros/tests/example_circuit.py deleted file mode 100644 index 10c0daa9a..000000000 --- a/klayout_dot_config/tech/GSiP/pymacros/tests/example_circuit.py +++ /dev/null @@ -1,85 +0,0 @@ -''' ---- 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 -''' - -designer_name = 'Test' -top_cell_name = 'GSiP_%s' % designer_name - -import pya -from pya import * - -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 - -tech_name = 'GSiP' - -if SiEPIC.__version__ < '0.5.4': - raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.") - -''' -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=False, GUI=True) -print('Number of errors: %s' % num_errors) - -# Save -path = os.path.dirname(os.path.realpath(__file__)) -filename = os.path.splitext(os.path.basename(__file__))[0] -file_out = export_layout(cell, path, filename, format='oas', screenshot=True) diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py new file mode 100644 index 000000000..04350510b --- /dev/null +++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py @@ -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() +