├── img └── flayout_gf_mzi.png ├── docs ├── assets │ └── images │ │ ├── favicon.ico │ │ ├── company_logo.png │ │ └── favicon.svg ├── Gemfile ├── _data │ └── topnav.yml ├── sidebar.json ├── _config.yml ├── _includes │ └── head.html └── index.html ├── Dockerfile ├── .bumpversion.cfg ├── pyproject.toml ├── environment.yml ├── .pre-commit-config.yaml ├── settings.ini ├── flayout ├── __init__.py ├── io.py ├── example_lib.py ├── gui.py ├── notebook.py ├── _nbdev.py ├── factories.py ├── pcell.py ├── cell.py └── bokeh.py ├── setup.cfg ├── Makefile ├── .gitignore ├── source ├── 04_io.ipynb ├── 92_notebook.ipynb ├── 99_example_lib.ipynb ├── 50_gui.ipynb ├── 03_pcell.ipynb ├── 01_factories.ipynb ├── 02_cell.ipynb └── 91_bokeh.ipynb ├── README.md ├── .github └── workflows │ └── main.yml └── index.ipynb /img/flayout_gf_mzi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flaport/flayout/HEAD/img/flayout_gf_mzi.png -------------------------------------------------------------------------------- /docs/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flaport/flayout/HEAD/docs/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/company_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flaport/flayout/HEAD/docs/assets/images/company_logo.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM condaforge/mambaforge 2 | ADD . /flayout 3 | RUN pip install /flayout[dev] 4 | RUN pip install gdsfactory 5 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem 'github-pages', group: :jekyll_plugins 3 | gem "jekyll", ">= 3.7" 4 | gem "kramdown", ">= 2.3.1" 5 | gem "jekyll-remote-theme" 6 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.1 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.cfg] 8 | 9 | [bumpversion:file:settings.ini] 10 | -------------------------------------------------------------------------------- /docs/_data/topnav.yml: -------------------------------------------------------------------------------- 1 | topnav: 2 | - title: Topnav 3 | items: 4 | - title: github 5 | external_url: https://github.com/flaport/flayout/ 6 | 7 | #Topnav dropdowns 8 | topnav_dropdowns: 9 | - title: Topnav dropdowns 10 | folders: 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "pip", "build", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 88 7 | target-version = ['py38'] 8 | include = '\.pyi?$' 9 | 10 | [tool.pyright] 11 | reportPrivateImportUsage = false 12 | -------------------------------------------------------------------------------- /docs/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "FLayout": { 3 | "Overview": "index", 4 | "Factories": "factories", 5 | "Cell Utilities": "cell", 6 | "PCell Decorator": "pcell", 7 | "Read & Write": "io" 8 | }, 9 | "Extensions": { 10 | "Bokeh Visualization": "bokeh", 11 | "Notebook": "notebook" 12 | }, 13 | "Libraries": { 14 | "Example Library": "example_lib" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: fl 2 | channels: 3 | - conda-forge 4 | - fastai 5 | - flaport 6 | dependencies: 7 | - build 8 | - python=3.8 9 | - pip 10 | - klayout 11 | - klayout-gui 12 | - black 13 | - bokeh 14 | - bump2version 15 | - ipykernel 16 | - isort 17 | - lxml 18 | - nbdev<2 19 | - nbstripout 20 | - numpy 21 | - papermill 22 | - pre-commit 23 | - pytest 24 | - pyyaml 25 | - tqdm 26 | - gdsfactory # optional dependency 27 | prefix: /home/flaport/.anaconda/envs/mb 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: nbdev_clean_nbs 5 | name: nbdev_clean_nbs 6 | entry: nbdev_clean_nbs 7 | language: python 8 | pass_filenames: false 9 | additional_dependencies: 10 | - nbdev 11 | - repo: local 12 | hooks: 13 | - id: nbstripout 14 | name: nbstripout 15 | entry: nbstripout 16 | language: python 17 | pass_filenames: true 18 | types: [jupyter] 19 | - repo: local 20 | hooks: 21 | - id: nbdev_diff_nbs 22 | name: nbdev_diff_nbs 23 | entry: bash -c 'if [ ! -z "$(nbdev_diff_nbs)" ]; then nbdev_build_lib; fi' 24 | language: python 25 | pass_filenames: false 26 | additional_dependencies: 27 | - nbdev 28 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | audience = Developers 3 | author = Floris Laporte 4 | author_email = floris.laporte@gmail.com 5 | branch = master 6 | copyright = Floris Laporte 7 | custom_sidebar = True 8 | description = FLayout 9 | doc_baseurl = /flayout/ 10 | doc_host = https://flaport.github.io/flaport 11 | doc_path = docs 12 | git_url = https://github.com/flaport/flayout/tree/master/ 13 | host = github 14 | jekyll_styles = note,warning,tip,important 15 | keywords = flayout 16 | language = English 17 | lib_name = flayout 18 | lib_path = flayout 19 | license = GPLv3 20 | min_python = 3.9 21 | monospace_docstrings = True 22 | nbs_path = . 23 | recursive = True 24 | repo_name = flayout 25 | status = 1 26 | title = FLayout 27 | user = flaport 28 | version = 0.0.1 29 | -------------------------------------------------------------------------------- /flayout/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: index.ipynb (unless otherwise specified). 2 | 3 | __all__ = [] 4 | 5 | # Internal Cell 6 | __version__ = "0.0.0" 7 | __author__ = "Floris Laporte" 8 | 9 | from .cell import copy_tree as copy_tree 10 | from .cell import reference as reference 11 | from .factories import box as box 12 | from .factories import cell as cell 13 | from .factories import layer as layer 14 | from .factories import layout as layout 15 | from .factories import library as library 16 | from .factories import path as path 17 | from .factories import point as point 18 | from .factories import polygon as polygon 19 | from .factories import transform as transform 20 | from .factories import vector as vector 21 | from .io import read_gds as read_gds 22 | from .io import save_gds as save_gds 23 | from .pcell import pcell as pcell -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = flayout 3 | version = 0.0.1 4 | description = Thin functional/declarative wrapper around the KLayout Python API 5 | author = Floris Laporte 6 | author_email = floris.laporte@gmail.com 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/flaport/flayout 10 | classifiers = 11 | Development Status :: 3 - Alpha 12 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 13 | Operating System :: OS Independent 14 | Programming Language :: Python :: 3 15 | Topic :: Scientific/Engineering 16 | 17 | [options] 18 | packages = 19 | flayout 20 | install_requires = 21 | klayout 22 | 23 | [options.extras_require] 24 | dev = 25 | black 26 | bokeh 27 | bump2version 28 | ipykernel 29 | isort 30 | lxml 31 | nbdev 32 | nbstripout 33 | numpy 34 | papermill 35 | pre-commit 36 | pytest 37 | pyyaml 38 | tqdm 39 | 40 | [options.package_data] 41 | * = 42 | README.md 43 | LICENSE 44 | flayout = 45 | layers.lyp 46 | -------------------------------------------------------------------------------- /flayout/io.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/04_io.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['read_gds', 'save_gds'] 4 | 5 | # Internal Cell 6 | import os 7 | 8 | import pya 9 | 10 | # Cell 11 | def read_gds(fp: str) -> pya.Layout: 12 | """load a layout 13 | 14 | Args: 15 | fp: the path to the layout to load 16 | 17 | Returns: 18 | the loaded layout 19 | """ 20 | if isinstance(fp, str): 21 | fp = os.path.abspath(os.path.expanduser(fp)) 22 | layout = pya.Layout() 23 | layout.read(fp) 24 | return layout 25 | 26 | # Cell 27 | def save_gds(layout: pya.Layout, fp: str): 28 | """save a layout 29 | 30 | Args: 31 | layout: the layout to save 32 | fp: the path where to save the layout 33 | """ 34 | if isinstance(fp, str): 35 | fp = os.path.abspath(os.path.expanduser(fp)) 36 | if isinstance(layout, pya.Layout): 37 | layout.write(fp) 38 | else: 39 | raise ValueError(f"Don't know how to save type {layout.__class__.__name__}.") -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | repository: https://github.com/flaport/flayout 2 | output: web 3 | topnav_title: FLayout 4 | site_title: FLayout 5 | company_name: Floris Laporte 6 | description: FLayout 7 | use_math: true 8 | google_analytics: 9 | google_search: 10 | host: 127.0.0.1 11 | port: 4000 12 | 13 | exclude: 14 | - .idea/ 15 | - .vscode/ 16 | - .gitignore 17 | - vendor 18 | 19 | highlighter: rouge 20 | markdown: kramdown 21 | kramdown: 22 | input: GFM 23 | auto_ids: true 24 | hard_wrap: false 25 | syntax_highlighter: rouge 26 | 27 | collections: 28 | tooltips: 29 | output: false 30 | 31 | defaults: 32 | - 33 | scope: 34 | path: "" 35 | type: "pages" 36 | values: 37 | layout: "page" 38 | comments: true 39 | search: true 40 | sidebar: home_sidebar 41 | topnav: topnav 42 | - 43 | scope: 44 | path: "" 45 | type: "tooltips" 46 | values: 47 | layout: "page" 48 | comments: true 49 | search: true 50 | tooltip: true 51 | 52 | sidebars: 53 | - home_sidebar 54 | 55 | plugins: 56 | - jekyll-remote-theme 57 | 58 | remote_theme: fastai/nbdev-jekyll-theme 59 | baseurl: /flayout/ 60 | -------------------------------------------------------------------------------- /flayout/example_lib.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/99_example_lib.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['rectangle', 'square', 'gf_pcells', 'example_lib'] 4 | 5 | # Internal Cell 6 | import pya 7 | import flayout as fl 8 | 9 | # Cell 10 | @fl.pcell 11 | def rectangle( 12 | name="rectangle", 13 | width: float = 1.0, 14 | height: float = 0.5, 15 | layer: pya.LayerInfo = pya.LayerInfo(1, 0), 16 | ): 17 | rect = fl.polygon(hull=[(-width/2, -height/2), (width/2, -height/2), (width/2, height/2), (-width/2, height/2)]) 18 | cell = fl.cell(name, shapes={layer: [rect]}) 19 | return cell 20 | 21 | # Cell 22 | @fl.pcell 23 | def square( 24 | name="square", 25 | width: float = 0.5, 26 | layer: pya.LayerInfo = pya.LayerInfo(1, 0), 27 | ): 28 | square = fl.polygon(hull=[(-width/2, -width/2), (width/2, -width/2), (width/2, width/2), (-width/2, width/2)]) 29 | cell = fl.cell(name, shapes={layer: [square]}) 30 | return cell 31 | 32 | # Cell 33 | gf_pcells = [] 34 | 35 | import flayout as fl 36 | import gdsfactory.components as gfc 37 | 38 | gf_pcells += [ 39 | mzi := fl.pcell(gfc.mzi, on_error="ignore"), 40 | bend_euler := fl.pcell(gfc.bend_euler, on_error="ignore") 41 | ] 42 | 43 | # Cell 44 | example_lib = fl.library( 45 | "F. E. L.", 46 | pcells=[rectangle, square, *gf_pcells], 47 | cells=[], 48 | description="FLayout Example Library", 49 | ) -------------------------------------------------------------------------------- /flayout/gui.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/50_gui.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['instance', 'example_lib'] 4 | 5 | # Internal Cell 6 | import pya 7 | import flayout as fl 8 | import numpy as np 9 | 10 | # Cell 11 | @fl.pcell 12 | def instance( 13 | name="instance", 14 | width: float = 1.0, 15 | height: float = 1.0, 16 | num_inputs: int = 3, 17 | num_outputs: int = 1, 18 | port_size=0.1, 19 | ): 20 | if (width < 2 * port_size) or (height < 2 * port_size): 21 | raise ValueError(f"width/height should be bigger than port size. Got: {width:.2f} < {port_size:.2f}") 22 | 23 | rect = np.array([(-width/2, -height/2), (width/2, -height/2), (width/2, height/2), (-width/2, height/2)]) 24 | port = np.array([(-port_size/2, -port_size/2), (port_size/2, -port_size/2), (port_size/2, port_size/2), (-port_size/2, port_size/2)]) 25 | input_ports = np.array([port + np.array([-width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_inputs + 2)[1:-1])]) 26 | output_ports = np.array([port + np.array([width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_inputs + 2)[1:-1])]) 27 | 28 | 29 | cell = fl.cell( 30 | name, 31 | shapes={ 32 | (1, 0): [rect], 33 | (1, 1): [*input_ports, *output_ports], 34 | } 35 | ) 36 | return cell 37 | 38 | # Cell 39 | example_lib = fl.library( 40 | "F. E. L.", 41 | pcells=[instance], 42 | cells=[], 43 | description="FLayout Example Library", 44 | ) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | SHELL := /bin/bash 3 | SRC = $(wildcard nbs/*.ipynb) 4 | 5 | all: lib docs 6 | 7 | .SILENT: docker 8 | docker: 9 | docker build . -t fl 10 | 11 | lib: 12 | nbdev_build_lib 13 | 14 | sync: 15 | nbdev_update_lib 16 | 17 | serve: 18 | cd docs && bundle-2.7 exec jekyll serve 19 | 20 | .PHONY: docs 21 | docs: 22 | papermill index.ipynb index.ipynb --cwd . --kernel fl 23 | nbdev_build_docs 24 | 25 | run: 26 | find source -name "*.ipynb" | grep -v .ipynb_checkpoints | xargs -I {} papermill {} {} --cwd source --kernel fl 27 | 28 | test: 29 | nbdev_test_nbs 30 | 31 | release: pypi conda_release 32 | nbdev_bump_version 33 | 34 | conda_release: 35 | fastrelease_conda_package 36 | 37 | pypi: dist 38 | twine upload --repository pypi dist/* 39 | 40 | dist: clean 41 | python -m build --sdist --wheel 42 | 43 | install: clean lib clean 44 | python -m pip uninstall -y flayout 45 | python -m pip install --upgrade -e . 46 | 47 | user_install: clean lib clean 48 | python -m pip uninstall -y flayout 49 | python -m pip install --user --upgrade . 50 | 51 | fast_user_install: lib 52 | python -m pip uninstall -y flayout 53 | python -m pip install --user --upgrade --no-deps . 54 | 55 | clean: 56 | nbdev_clean_nbs 57 | find . -name "*.ipynb" | xargs nbstripout 58 | find . -name "dist" | xargs rm -rf 59 | find . -name "build" | xargs rm -rf 60 | find . -name "builds" | xargs rm -rf 61 | find . -name "__pycache__" | xargs rm -rf 62 | find . -name "*.so" | xargs rm -rf 63 | find . -name "*.egg-info" | xargs rm -rf 64 | find . -name ".ipynb_checkpoints" | xargs rm -rf 65 | find . -name ".pytest_cache" | xargs rm -rf 66 | 67 | reset: 68 | rm -rf flayout 69 | rm -rf docs 70 | git checkout -- docs 71 | nbdev_build_lib 72 | make clean 73 | -------------------------------------------------------------------------------- /flayout/notebook.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/92_notebook.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['html_repr', 'auto_expand'] 4 | 5 | # Internal Cell 6 | from functools import wraps 7 | 8 | import bokeh.plotting as bp 9 | import flayout.bokeh as flbk 10 | import pya 11 | from bokeh.embed import file_html 12 | from bokeh.io import output_notebook 13 | from bokeh.resources import CDN 14 | 15 | try: 16 | IPYTHON = get_ipython() # type: ignore 17 | output_notebook(hide_banner=True) 18 | except NameError: 19 | IPYTHON = None 20 | 21 | # Cell 22 | def html_repr(func, types): 23 | for cls in types: 24 | 25 | def registered(obj): 26 | plot = func(None, obj) 27 | html = file_html(plot, CDN) 28 | return html 29 | 30 | if IPYTHON is not None: 31 | fmtr = IPYTHON.display_formatter.formatters["text/html"] 32 | fmtr.for_type(cls, registered) 33 | 34 | # Cell 35 | def _expand_fig(fig): 36 | try: 37 | w = (fig.x_range.end - fig.x_range.start) 38 | h = (fig.y_range.end - fig.y_range.start) 39 | fig.x_range.start -= w/10 40 | fig.x_range.end += w/10 41 | fig.y_range.start -= h/10 42 | fig.y_range.end += h/10 43 | except Exception as e: 44 | pass 45 | return fig 46 | 47 | def auto_expand(func): 48 | @wraps(func) 49 | def new_func(*args, **kwargs): 50 | fig = func(*args, **kwargs) 51 | if isinstance(fig, bp.Column): 52 | fig = bp.Column(*[_expand_fig(f) for f in fig.children]) 53 | else: 54 | fig = _expand_fig(fig) 55 | return fig 56 | return new_func 57 | 58 | # Cell 59 | html_repr(auto_expand(flbk.draw_point), [pya.DPoint, pya.Point]) 60 | html_repr(auto_expand(flbk.draw_vector), [pya.DVector, pya.Vector]) 61 | html_repr(auto_expand(flbk.draw_path), [pya.Path, pya.DPath]) 62 | html_repr(auto_expand(flbk.draw_poly), [pya.DSimplePolygon, pya.SimplePolygon, pya.DPolygon, pya.Polygon]) 63 | html_repr(auto_expand(flbk.draw_box), [pya.Box, pya.DBox]) 64 | html_repr(auto_expand(flbk.draw_cell), [pya.Cell]) 65 | html_repr(auto_expand(flbk.draw_inst), [pya.Instance]) 66 | html_repr(auto_expand(flbk.draw_layout), [pya.Layout]) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # repo specific 2 | docs 3 | 4 | # IDE settings 5 | .vscode/ 6 | .idea/ 7 | 8 | # nbdev 9 | *.bak 10 | *.log 11 | *~ 12 | .gitattributes 13 | .gitconfig 14 | .jekyll-cache/ 15 | .last_checked 16 | Gemfile.lock 17 | _tmp* 18 | docs/ 19 | tags 20 | tmp* 21 | ~* 22 | 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | env/ 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | *.egg-info/ 47 | .installed.cfg 48 | *.egg 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .coverage 64 | .coverage.* 65 | .cache 66 | nosetests.xml 67 | coverage.xml 68 | *.cover 69 | .hypothesis/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # dotenv 105 | .env 106 | 107 | # virtualenv 108 | .venv 109 | venv/ 110 | ENV/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | 125 | .vscode 126 | *.swp 127 | 128 | # osx generated files 129 | .DS_Store 130 | .DS_Store? 131 | .Trashes 132 | ehthumbs.db 133 | Thumbs.db 134 | .idea 135 | 136 | # pytest 137 | .pytest_cache 138 | 139 | # tools/trust-doc-nbs 140 | docs_src/.last_checked 141 | 142 | # symlinks to fastai 143 | docs_src/fastai 144 | tools/fastai 145 | 146 | # link checker 147 | checklink/cookies.txt 148 | 149 | # .gitconfig is now autogenerated 150 | .gitconfig 151 | 152 | -------------------------------------------------------------------------------- /source/04_io.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "893fd908-aa0b-4daa-ac08-16c7782101f6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp io" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "d816dbc2-a09f-4e7a-b173-680c9fb05d38", 16 | "metadata": {}, 17 | "source": [ 18 | "# FLayout IO\n", 19 | "> read and save KLayout layouts" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "2a7e3589-2b55-4791-838b-2798b4a7648d", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "import os\n", 31 | "\n", 32 | "import pya" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "e8bfc909-072a-4fef-83a2-14ecc564181a", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# export\n", 43 | "def read_gds(fp: str) -> pya.Layout:\n", 44 | " \"\"\"load a layout\n", 45 | "\n", 46 | " Args:\n", 47 | " fp: the path to the layout to load\n", 48 | "\n", 49 | " Returns:\n", 50 | " the loaded layout\n", 51 | " \"\"\"\n", 52 | " if isinstance(fp, str):\n", 53 | " fp = os.path.abspath(os.path.expanduser(fp))\n", 54 | " layout = pya.Layout()\n", 55 | " layout.read(fp)\n", 56 | " return layout" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "54754b17-afb8-4e2c-9256-db991e74a161", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "# export\n", 67 | "def save_gds(layout: pya.Layout, fp: str):\n", 68 | " \"\"\"save a layout\n", 69 | "\n", 70 | " Args:\n", 71 | " layout: the layout to save\n", 72 | " fp: the path where to save the layout\n", 73 | " \"\"\"\n", 74 | " if isinstance(fp, str):\n", 75 | " fp = os.path.abspath(os.path.expanduser(fp))\n", 76 | " if isinstance(layout, pya.Layout):\n", 77 | " layout.write(fp)\n", 78 | " else:\n", 79 | " raise ValueError(f\"Don't know how to save type {layout.__class__.__name__}.\")" 80 | ] 81 | } 82 | ], 83 | "metadata": { 84 | "kernelspec": { 85 | "display_name": "fl", 86 | "language": "python", 87 | "name": "fl" 88 | } 89 | }, 90 | "nbformat": 4, 91 | "nbformat_minor": 5 92 | } 93 | -------------------------------------------------------------------------------- /docs/assets/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 40 | 42 | 46 | 51 | 55 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FLayout 2 | > My personal KLayout wrappers and utility functions 3 | 4 | 5 | ## Archived 6 | 7 | **Consider this project archived as I don't have time to maintain it. Feel free to use whatever you deem useful in your own klayout-based projects.** 8 | 9 | ## Installation 10 | 11 | ```sh 12 | pip install flayout 13 | ``` 14 | 15 | ### Development installation 16 | 17 | ```sh 18 | git clone https://github.com/flaport/flayout 19 | cd flayout 20 | conda env update # create conda environment 'fl' with klayout (python library) and klayout-gui (GUI) 21 | pip install -e . 22 | ``` 23 | 24 | It would be cool if a [salt](https://www.klayout.de/package_cookbook.html) package for this library would exist. 25 | 26 | Note that this package might not work on Windwos... I never took the time to test it there... 27 | 28 | ## Easy PCells 29 | 30 | This library offers a klayout [PCell decorator](https://flaport.github.io/flayout/pcell), which is much easier to use than the default PCell offered by the KLayout api. Moreover, the PCell decorator can be with a [GDSFactory](https://github.com/gdsfactory/gdsfactory) component function (or any function that produces a [gdspy](https://github.com/heitzmann/gdspy) cell) as well! 31 | 32 | ## Use with KLayout 33 | 34 | First install flayout as a system python package: 35 | 36 | ```sh 37 | cd /path/to/flayout 38 | pip install --user . 39 | ``` 40 | 41 | Alternatively, you can also open the klayout gui from *within* the `fl` python environment (linux only) 42 | 43 | Then within klayout add a simple macro to import the flayout example library 44 | 45 | ```python 46 | from flayout.example_lib import * 47 | ``` 48 | 49 | Note that running this macro takes a while in KLayout (about 10 seconds). 50 | 51 | After running the macro, open a new gdsfile and find the Flayout PCells in the "F.E.L - Flayout Example Library", which supplies two gdsfactory-defined components: the mzi and euler bend. These components are now imported as PCells! 52 | 53 | ![gdsfactory_klayout_pcell](img/flayout_gf_mzi.png) 54 | 55 | You can have a look at how the [example library](flayout/example_lib.py) is implemented and try something similar for yourself! 56 | 57 | ## Quick Docs 58 | 59 | 60 | 61 | 62 | - **FLayout** 63 | - Overview 64 | - [Factories](https://flaport.github.io/flayout/factories) ⬅ start here. 65 | - [Cell Utilities](https://flaport.github.io/flayout/cell) 66 | - [PCell Decorator](https://flaport.github.io/flayout/pcell) 67 | - [Read & Write](https://flaport.github.io/flayout/io) 68 | - **Extensions** 69 | - [Bokeh Visualization](https://flaport.github.io/flayout/bokeh) 70 | - [Notebook](https://flaport.github.io/flayout/notebook) 71 | - **Libraries** 72 | - [Example Library](https://flaport.github.io/flayout/example_lib) 73 | 74 | 75 | 76 | 77 | ## NBDev 78 | 79 | This project was created using [nbdev1](https://nbdev1.fast.ai/). An awesome [literate programming](https://en.wikipedia.org/wiki/Literate_programming) environment for python. 80 | 81 | ## License 82 | 83 | As this library can be considered a light wrapper for the KLayout python api, I chose license it under [GPLv3](LICENSE). 84 | -------------------------------------------------------------------------------- /flayout/_nbdev.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED BY NBDEV! DO NOT EDIT! 2 | 3 | __all__ = ["index", "modules", "custom_doc_links", "git_url"] 4 | 5 | index = {"__version__": "index.ipynb", 6 | "__author__": "index.ipynb", 7 | "layer": "01_factories.ipynb", 8 | "point": "01_factories.ipynb", 9 | "vector": "01_factories.ipynb", 10 | "box": "01_factories.ipynb", 11 | "polygon": "01_factories.ipynb", 12 | "Point": "01_factories.ipynb", 13 | "Points": "01_factories.ipynb", 14 | "path": "01_factories.ipynb", 15 | "cell": "01_factories.ipynb", 16 | "Shape": "01_factories.ipynb", 17 | "transform": "01_factories.ipynb", 18 | "layout": "01_factories.ipynb", 19 | "library": "01_factories.ipynb", 20 | "DoesNotExist": "02_cell.ipynb", 21 | "COPY_IMPLEMENTATIONS": "02_cell.ipynb", 22 | "MAX_DEPTH": "02_cell.ipynb", 23 | "ON_SAME_NAME_DOC": "02_cell.ipynb", 24 | "Reference": "02_cell.ipynb", 25 | "LibReference": "02_cell.ipynb", 26 | "PCellReference": "02_cell.ipynb", 27 | "PCellLibReference": "02_cell.ipynb", 28 | "reference": "02_cell.ipynb", 29 | "copy_tree": "02_cell.ipynb", 30 | "add_cells_to_layout": "02_cell.ipynb", 31 | "add_pcells_to_layout": "02_cell.ipynb", 32 | "CELL_CONVERTERS": "03_pcell.ipynb", 33 | "pcell": "03_pcell.ipynb", 34 | "read_gds": "04_io.ipynb", 35 | "save_gds": "04_io.ipynb", 36 | "instance": "50_gui.ipynb", 37 | "example_lib": "99_example_lib.ipynb", 38 | "ALPHA": "91_bokeh.ipynb", 39 | "RED": "91_bokeh.ipynb", 40 | "GREEN": "91_bokeh.ipynb", 41 | "BLUE": "91_bokeh.ipynb", 42 | "C0": "91_bokeh.ipynb", 43 | "C1": "91_bokeh.ipynb", 44 | "C2": "91_bokeh.ipynb", 45 | "C3": "91_bokeh.ipynb", 46 | "C4": "91_bokeh.ipynb", 47 | "C5": "91_bokeh.ipynb", 48 | "C6": "91_bokeh.ipynb", 49 | "C7": "91_bokeh.ipynb", 50 | "C8": "91_bokeh.ipynb", 51 | "C9": "91_bokeh.ipynb", 52 | "get_lyp_path": "91_bokeh.ipynb", 53 | "LayerProperty": "91_bokeh.ipynb", 54 | "LayerProperties": "91_bokeh.ipynb", 55 | "read_lyp": "91_bokeh.ipynb", 56 | "get_lyp": "91_bokeh.ipynb", 57 | "new_plot": "91_bokeh.ipynb", 58 | "adjust_plot": "91_bokeh.ipynb", 59 | "draw_polys": "91_bokeh.ipynb", 60 | "draw_poly": "91_bokeh.ipynb", 61 | "draw_path": "91_bokeh.ipynb", 62 | "draw_point": "91_bokeh.ipynb", 63 | "draw_vector": "91_bokeh.ipynb", 64 | "draw_box": "91_bokeh.ipynb", 65 | "draw_cell": "91_bokeh.ipynb", 66 | "draw_inst": "91_bokeh.ipynb", 67 | "draw_layout": "91_bokeh.ipynb", 68 | "html_repr": "92_notebook.ipynb", 69 | "auto_expand": "92_notebook.ipynb", 70 | "rectangle": "99_example_lib.ipynb", 71 | "square": "99_example_lib.ipynb", 72 | "gf_pcells": "99_example_lib.ipynb"} 73 | 74 | modules = ["__init__.py", 75 | "factories.py", 76 | "cell.py", 77 | "pcell.py", 78 | "io.py", 79 | "gui.py", 80 | "bokeh.py", 81 | "notebook.py", 82 | "example_lib.py"] 83 | 84 | doc_url = "https://flaport.github.io/flaport/flayout/" 85 | 86 | git_url = "https://github.com/flaport/flayout/tree/master/" 87 | 88 | def custom_doc_links(name): return None 89 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - ".github/**" 10 | - "!.github/workflows/main.yml" 11 | - "docs/**" 12 | - "*.md" 13 | - ".git*" 14 | 15 | jobs: 16 | # clean: 17 | # runs-on: ubuntu-latest 18 | # timeout-minutes: 5 19 | # container: 20 | # image: condaforge/mambaforge:4.10.3-10 21 | # volumes: 22 | # - ${{ github.workspace }}:/github/workspace 23 | # steps: 24 | # - name: Checkout 25 | # uses: actions/checkout@master 26 | # - name: Install dependencies 27 | # run: mamba install -c fastai -c conda-forge nbdev\<2 nbstripout 28 | # - name: Clean notebooks 29 | # run: nbdev_clean_nbs 30 | # - name: Strip notebook outputs 31 | # run: find . -name "*.ipynb" | xargs nbstripout 32 | # - name: Check if git status is clean 33 | # run: | 34 | # if [ ! -z "$(git status --porcelain)" ]; then 35 | # echo "Notebooks are not cleaned! They still contain outputs and/or metadata." 36 | # echo "You probably forgot to clear the notebook outputs before pushing." 37 | # echo "Please make sure all pre-commit hooks are properly installed to prevent this issue." 38 | # false; 39 | # fi 40 | 41 | # diff: 42 | # runs-on: ubuntu-latest 43 | # timeout-minutes: 5 44 | # container: 45 | # image: condaforge/mambaforge:4.10.3-10 46 | # volumes: 47 | # - ${{ github.workspace }}:/github/workspace 48 | # steps: 49 | # - name: Checkout 50 | # uses: actions/checkout@master 51 | # - name: Install dependencies 52 | # run: mamba install -c fastai -c conda-forge nbdev\<2 53 | # - name: Check for no diff between notebooks and library 54 | # run: | 55 | # if [ ! -z "$(nbdev_diff_nbs)" ]; then 56 | # echo "Python library does not match backing notebooks (diff between notebooks and library detected). " 57 | # echo "You probably forgot to re-generate the library before pushing." 58 | # echo "Please make sure all pre-commit hooks are properly installed to prevent this issue." 59 | # false; 60 | # fi 61 | 62 | docs: 63 | # needs: 64 | # - clean 65 | # - diff 66 | runs-on: ubuntu-latest 67 | timeout-minutes: 5 68 | container: 69 | image: condaforge/mambaforge:4.10.3-10 70 | volumes: 71 | - ${{ github.workspace }}:/github/workspace 72 | steps: 73 | - name: Checkout 74 | uses: actions/checkout@master 75 | - name: Install dependencies 76 | run: mamba env update -n base -f environment.yml 77 | - name: Create kernel 78 | run: python -m ipykernel install --user --name fl 79 | - name: Run notebooks 80 | run: find source -name "*.ipynb" | grep -v '.ipynb_checkpoints' | xargs -I {} papermill {} {} -k fl 81 | - name: Run index.ipynb 82 | run: papermill index.ipynb index.ipynb --cwd . --kernel fl 83 | - name: Build docs 84 | run: nbdev_build_docs 85 | - name: Store artifacts 86 | uses: actions/upload-artifact@master 87 | with: 88 | name: docs 89 | path: /github/workspace/docs 90 | 91 | pages: 92 | needs: 93 | - docs 94 | runs-on: ubuntu-latest 95 | timeout-minutes: 5 96 | container: 97 | image: condaforge/mambaforge:4.10.3-10 98 | volumes: 99 | - ${{ github.workspace }}:/github/workspace 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@master 103 | - name: Install dependencies 104 | run: mamba install -c fastai -c conda-forge rsync 105 | - name: Download 'docs' artifact 106 | uses: actions/download-artifact@master 107 | with: 108 | name: docs 109 | path: /github/workspace/docs 110 | - name: Deploy 111 | uses: JamesIves/github-pages-deploy-action@4.1.7 112 | with: 113 | branch: pages 114 | folder: docs 115 | -------------------------------------------------------------------------------- /source/92_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "1ebbed71-340c-4363-ad7c-4d852c3658e9", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp notebook" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "94d088e4-fbfd-400c-84de-3c5531be520a", 16 | "metadata": {}, 17 | "source": [ 18 | "# Notebook\n", 19 | "> Importing this library yields nice bokeh-powered notebook previews of your cells and polygons." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "e79cbfb8-1dfb-4e9e-9566-732677ce222f", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "from functools import wraps\n", 31 | "\n", 32 | "import bokeh.plotting as bp\n", 33 | "import flayout.bokeh as flbk\n", 34 | "import pya\n", 35 | "from bokeh.embed import file_html\n", 36 | "from bokeh.io import output_notebook\n", 37 | "from bokeh.resources import CDN\n", 38 | "\n", 39 | "try:\n", 40 | " IPYTHON = get_ipython() # type: ignore\n", 41 | " output_notebook(hide_banner=True)\n", 42 | "except NameError:\n", 43 | " IPYTHON = None" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "5bce77e1-0f3f-46ae-b8ae-062960719d69", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# export\n", 54 | "def html_repr(func, types):\n", 55 | " for cls in types:\n", 56 | "\n", 57 | " def registered(obj):\n", 58 | " plot = func(None, obj)\n", 59 | " html = file_html(plot, CDN)\n", 60 | " return html\n", 61 | "\n", 62 | " if IPYTHON is not None:\n", 63 | " fmtr = IPYTHON.display_formatter.formatters[\"text/html\"]\n", 64 | " fmtr.for_type(cls, registered)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "d8f360fe-e397-478d-ada6-608b2731c5b2", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# export\n", 75 | "def _expand_fig(fig):\n", 76 | " try:\n", 77 | " w = (fig.x_range.end - fig.x_range.start)\n", 78 | " h = (fig.y_range.end - fig.y_range.start)\n", 79 | " fig.x_range.start -= w/10\n", 80 | " fig.x_range.end += w/10\n", 81 | " fig.y_range.start -= h/10\n", 82 | " fig.y_range.end += h/10\n", 83 | " except Exception as e:\n", 84 | " pass\n", 85 | " return fig\n", 86 | "\n", 87 | "def auto_expand(func):\n", 88 | " @wraps(func)\n", 89 | " def new_func(*args, **kwargs):\n", 90 | " fig = func(*args, **kwargs)\n", 91 | " if isinstance(fig, bp.Column):\n", 92 | " fig = bp.Column(*[_expand_fig(f) for f in fig.children])\n", 93 | " else:\n", 94 | " fig = _expand_fig(fig)\n", 95 | " return fig\n", 96 | " return new_func" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "de00a860-be44-42a4-9e21-200c0e7997db", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "# export\n", 107 | "html_repr(auto_expand(flbk.draw_point), [pya.DPoint, pya.Point])\n", 108 | "html_repr(auto_expand(flbk.draw_vector), [pya.DVector, pya.Vector])\n", 109 | "html_repr(auto_expand(flbk.draw_path), [pya.Path, pya.DPath])\n", 110 | "html_repr(auto_expand(flbk.draw_poly), [pya.DSimplePolygon, pya.SimplePolygon, pya.DPolygon, pya.Polygon])\n", 111 | "html_repr(auto_expand(flbk.draw_box), [pya.Box, pya.DBox])\n", 112 | "html_repr(auto_expand(flbk.draw_cell), [pya.Cell])\n", 113 | "html_repr(auto_expand(flbk.draw_inst), [pya.Instance])\n", 114 | "html_repr(auto_expand(flbk.draw_layout), [pya.Layout])" 115 | ] 116 | } 117 | ], 118 | "metadata": { 119 | "kernelspec": { 120 | "display_name": "fl", 121 | "language": "python", 122 | "name": "fl" 123 | } 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 5 127 | } 128 | -------------------------------------------------------------------------------- /source/99_example_lib.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "831fd803-f817-4b13-94b9-c97d5fd9b667", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp example_lib" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "a8f0486c-2290-4ec4-b63f-63987110c7a1", 16 | "metadata": {}, 17 | "source": [ 18 | "# Example Library\n", 19 | "> An example library of PCells importable in klayout." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "055bfd2c-4968-4b16-b4e4-65a7ee48381e", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "import pya\n", 31 | "import flayout as fl" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "60d0fcaf-79fd-41ab-a658-52f101b384ef", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "# hide\n", 42 | "import flayout.notebook" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "8f9ece43-492a-4f59-b16b-0eecc60b002f", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "# export\n", 53 | "@fl.pcell\n", 54 | "def rectangle(\n", 55 | " name=\"rectangle\",\n", 56 | " width: float = 1.0,\n", 57 | " height: float = 0.5,\n", 58 | " layer: pya.LayerInfo = pya.LayerInfo(1, 0),\n", 59 | "):\n", 60 | " rect = fl.polygon(hull=[(-width/2, -height/2), (width/2, -height/2), (width/2, height/2), (-width/2, height/2)])\n", 61 | " cell = fl.cell(name, shapes={layer: [rect]})\n", 62 | " return cell" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "56580594-751c-453f-918c-5241c681bda8", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "rectangle()" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "a61f7b10-44c4-4599-8f85-b13925bcb612", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "# export\n", 83 | "@fl.pcell\n", 84 | "def square(\n", 85 | " name=\"square\",\n", 86 | " width: float = 0.5,\n", 87 | " layer: pya.LayerInfo = pya.LayerInfo(1, 0),\n", 88 | "):\n", 89 | " square = fl.polygon(hull=[(-width/2, -width/2), (width/2, -width/2), (width/2, width/2), (-width/2, width/2)])\n", 90 | " cell = fl.cell(name, shapes={layer: [square]})\n", 91 | " return cell" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "id": "73b1fc34-fc98-4b63-acb7-6cacb5d96145", 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "square()" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "id": "e724cad8-ad67-473f-9482-60b09202ac4d", 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "# export\n", 112 | "gf_pcells = []\n", 113 | "\n", 114 | "import flayout as fl\n", 115 | "import gdsfactory.components as gfc\n", 116 | "\n", 117 | "gf_pcells += [\n", 118 | " mzi := fl.pcell(gfc.mzi, on_error=\"ignore\"),\n", 119 | " bend_euler := fl.pcell(gfc.bend_euler, on_error=\"ignore\")\n", 120 | "]" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "id": "8271e1ed-a6b3-469f-9ee9-e2f2431a03f4", 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "display(mzi())\n", 131 | "display(bend_euler())" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "id": "c51c0904-300a-4490-b640-42cdde4a2aa6", 137 | "metadata": {}, 138 | "source": [ 139 | "## Library" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "id": "71b00ca4-04de-449c-882d-6760ffdde495", 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "# exports\n", 150 | "example_lib = fl.library(\n", 151 | " \"F. E. L.\",\n", 152 | " pcells=[rectangle, square, *gf_pcells],\n", 153 | " cells=[],\n", 154 | " description=\"FLayout Example Library\",\n", 155 | ")" 156 | ] 157 | } 158 | ], 159 | "metadata": { 160 | "kernelspec": { 161 | "display_name": "fl", 162 | "language": "python", 163 | "name": "fl" 164 | } 165 | }, 166 | "nbformat": 4, 167 | "nbformat_minor": 5 168 | } 169 | -------------------------------------------------------------------------------- /source/50_gui.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "831fd803-f817-4b13-94b9-c97d5fd9b667", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp gui" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "a8f0486c-2290-4ec4-b63f-63987110c7a1", 16 | "metadata": {}, 17 | "source": [ 18 | "# GUI\n", 19 | "> An example KLayout-base GUI" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "055bfd2c-4968-4b16-b4e4-65a7ee48381e", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "import pya\n", 31 | "import flayout as fl\n", 32 | "import numpy as np" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "60d0fcaf-79fd-41ab-a658-52f101b384ef", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# hide\n", 43 | "import flayout.notebook" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "8f9ece43-492a-4f59-b16b-0eecc60b002f", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# export\n", 54 | "@fl.pcell\n", 55 | "def instance(\n", 56 | " name=\"instance\",\n", 57 | " width: float = 1.0,\n", 58 | " height: float = 1.0,\n", 59 | " num_inputs: int = 3,\n", 60 | " num_outputs: int = 1,\n", 61 | " port_size=0.1,\n", 62 | "):\n", 63 | " if (width < 2 * port_size) or (height < 2 * port_size):\n", 64 | " raise ValueError(f\"width/height should be bigger than port size. Got: {width:.2f} < {port_size:.2f}\")\n", 65 | " \n", 66 | " rect = np.array([(-width/2, -height/2), (width/2, -height/2), (width/2, height/2), (-width/2, height/2)])\n", 67 | " port = np.array([(-port_size/2, -port_size/2), (port_size/2, -port_size/2), (port_size/2, port_size/2), (-port_size/2, port_size/2)])\n", 68 | " input_ports = np.array([port + np.array([-width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_inputs + 2)[1:-1])])\n", 69 | " output_ports = np.array([port + np.array([width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_inputs + 2)[1:-1])])\n", 70 | " \n", 71 | " \n", 72 | " cell = fl.cell(\n", 73 | " name, \n", 74 | " shapes={\n", 75 | " (1, 0): [rect],\n", 76 | " (1, 1): [*input_ports, *output_ports],\n", 77 | " }\n", 78 | " )\n", 79 | " return cell" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "id": "56580594-751c-453f-918c-5241c681bda8", 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "instance()" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "id": "d448a5b3-b1c2-4310-99ed-33cd50681440", 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "@fl.pcell\n", 100 | "def connection(\n", 101 | " name=\"connection\",\n", 102 | " input=\"\",\n", 103 | " output=\"\",\n", 104 | "):\n", 105 | " rect = np.array([(-width/2, -height/2), (width/2, -height/2), (width/2, height/2), (-width/2, height/2)])\n", 106 | " port = np.array([(-port_size/2, -port_size/2), (port_size/2, -port_size/2), (port_size/2, port_size/2), (-port_size/2, port_size/2)])\n", 107 | " input_ports = np.array([port + np.array([-width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_inputs + 2)[1:-1])])\n", 108 | " output_ports = np.array([port + np.array([width/2, y]) for i, y in enumerate(np.linspace(-width/2, width/2, num_outputs + 2)[1:-1])])\n", 109 | " \n", 110 | " \n", 111 | " cell = fl.cell(\n", 112 | " name, \n", 113 | " shapes={\n", 114 | " (1, 0): [rect],\n", 115 | " (1, 1): [*input_ports, *output_ports],\n", 116 | " }\n", 117 | " )\n", 118 | " return cell" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "71b00ca4-04de-449c-882d-6760ffdde495", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "# exports\n", 129 | "example_lib = fl.library(\n", 130 | " \"F. E. L.\",\n", 131 | " pcells=[instance],\n", 132 | " cells=[],\n", 133 | " description=\"FLayout Example Library\",\n", 134 | ")" 135 | ] 136 | } 137 | ], 138 | "metadata": { 139 | "kernelspec": { 140 | "display_name": "fl", 141 | "language": "python", 142 | "name": "fl" 143 | } 144 | }, 145 | "nbformat": 4, 146 | "nbformat_minor": 5 147 | } 148 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ page.title }} | {{ site.site_title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% if site.use_math %} 25 | 26 | 27 | 28 | 29 | 40 | {% endif %} 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | {% if site.twitter_username %} 68 | 69 | 70 | 71 | {% endif %} 72 | 73 | {% if page.summary %} 74 | 75 | {% else %} 76 | 77 | {% endif %} 78 | 79 | {% if page.image %} 80 | 81 | 82 | {% else %} 83 | 84 | {% if site.title_image %} 85 | 86 | {% endif %} 87 | {% endif %} 88 | 89 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: FLayout 4 | 5 | 6 | keywords: fastai 7 | sidebar: home_sidebar 8 | 9 | summary: "My personal KLayout wrappers and utility functions" 10 | description: "My personal KLayout wrappers and utility functions" 11 | nb_path: "index.ipynb" 12 | --- 13 | 22 | 23 |
24 | 25 | {% raw %} 26 | 27 |
28 | 29 |
30 | {% endraw %} 31 | 32 |
33 |
34 |

Archived

Consider this project archived as I don't have time to maintain it. Feel free to use whatever you deem useful in your own klayout-based projects.

35 | 36 |
37 |
38 |
39 |
40 |
41 |

Installation

git clone https://github.com/flaport/flayout
 42 | cd flayout
 43 | conda env update # create conda environment 'fl' with klayout (python library) and klayout-gui (GUI)
 44 | pip install -e .
 45 | 
46 |

It would be cool if a salt package for this library would exist.

47 |

Note that this package might not work on Windwos... I never took the time to test it there...

48 | 49 |
50 |
51 |
52 |
53 |
54 |

Easy PCells

This library offers a klayout PCell decorator, which is much easier to use than the default PCell offered by the KLayout api. Moreover, the PCell decorator can be with a GDSFactory component function (or any function that produces a gdspy cell) as well!

55 | 56 |
57 |
58 |
59 |
60 |
61 |

Use with KLayout

First install flayout as a system python package:

62 |
cd /path/to/flayout
 63 | pip install --user .
 64 | 
65 |

Alternatively, you can also open the klayout gui from within the fl python environment (linux only)

66 |

Then within klayout add a simple macro to import the flayout example library

67 |
from flayout.example_lib import *
 68 | 
69 |

Note that running this macro takes a while in KLayout (about 10 seconds).

70 |

After running the macro, open a new gdsfile and find the Flayout PCells in the "F.E.L - Flayout Example Library", which supplies two gdsfactory-defined components: the mzi and euler bend. These components are now imported as PCells!

71 |

gdsfactory_klayout_pcell

72 |

You can have a look at how the example library is implemented and try something similar for yourself!

73 | 74 |
75 |
76 |
77 |
78 |
79 |

Quick Docs

80 |
81 |
82 |
83 | {% raw %} 84 | 85 |
86 | 87 |
88 |
89 | 90 |
91 | 92 | 93 |
94 | 113 | 114 |
115 | 116 |
117 | 118 |
119 |
120 | 121 |
122 | {% endraw %} 123 | 124 |
125 |
126 |

NBDev

This project was created using nbdev1. An awesome literate programming environment for python.

127 | 128 |
129 |
130 |
131 |
132 |
133 |

License

As this library can be considered a light wrapper for the KLayout python api, I chose license it under GPLv3.

134 | 135 |
136 |
137 |
138 |
139 | 140 | 141 | -------------------------------------------------------------------------------- /index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "d2084e67-8453-43e2-b864-007987072521", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp __init__" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "a059ef95", 16 | "metadata": {}, 17 | "source": [ 18 | "# FLayout\n", 19 | "> My personal KLayout wrappers and utility functions" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "45ada385-89ae-4efd-9f4f-4553d75dd231", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "__version__ = \"0.0.0\"\n", 31 | "__author__ = \"Floris Laporte\"\n", 32 | "\n", 33 | "from flayout.cell import copy_tree as copy_tree\n", 34 | "from flayout.cell import reference as reference\n", 35 | "from flayout.factories import box as box\n", 36 | "from flayout.factories import cell as cell\n", 37 | "from flayout.factories import layer as layer\n", 38 | "from flayout.factories import layout as layout\n", 39 | "from flayout.factories import library as library\n", 40 | "from flayout.factories import path as path\n", 41 | "from flayout.factories import point as point\n", 42 | "from flayout.factories import polygon as polygon\n", 43 | "from flayout.factories import transform as transform\n", 44 | "from flayout.factories import vector as vector\n", 45 | "from flayout.io import read_gds as read_gds\n", 46 | "from flayout.io import save_gds as save_gds\n", 47 | "from flayout.pcell import pcell as pcell" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "id": "0813fb22", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "# hide\n", 58 | "import json\n", 59 | "from nbdev.showdoc import Markdown" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "id": "165ba04d-d5b2-477b-a289-22a672044011", 65 | "metadata": {}, 66 | "source": [ 67 | "## Archived\n", 68 | "\n", 69 | "**Consider this project archived as I don't have time to maintain it. Feel free to use whatever you deem useful in your own klayout-based projects.**" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "cc6cb4d6-d3a9-4f98-9612-6f9ce302f4c8", 75 | "metadata": {}, 76 | "source": [ 77 | "## Installation\n", 78 | "\n", 79 | "```sh\n", 80 | "git clone https://github.com/flaport/flayout\n", 81 | "cd flayout\n", 82 | "conda env update # create conda environment 'fl' with klayout (python library) and klayout-gui (GUI)\n", 83 | "pip install -e .\n", 84 | "```\n", 85 | "\n", 86 | "It would be cool if a [salt](https://www.klayout.de/package_cookbook.html) package for this library would exist.\n", 87 | "\n", 88 | "Note that this package might not work on Windwos... I never took the time to test it there..." 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "9c6e3721-5933-4c40-9c3c-1b657cbd7cd9", 94 | "metadata": {}, 95 | "source": [ 96 | "## Easy PCells\n", 97 | "\n", 98 | "This library offers a klayout [PCell decorator](https://flaport.github.io/flayout/pcell), which is much easier to use than the default PCell offered by the KLayout api. Moreover, the PCell decorator can be with a [GDSFactory](https://github.com/gdsfactory/gdsfactory) component function (or any function that produces a [gdspy](https://github.com/heitzmann/gdspy) cell) as well!" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "id": "eed1770d-ceb9-41fc-b9d0-f6cea14746b0", 104 | "metadata": {}, 105 | "source": [ 106 | "## Use with KLayout\n", 107 | "\n", 108 | "First install flayout as a system python package:\n", 109 | "\n", 110 | "```sh\n", 111 | "cd /path/to/flayout\n", 112 | "pip install --user .\n", 113 | "```\n", 114 | "\n", 115 | "Alternatively, you can also open the klayout gui from *within* the `fl` python environment (linux only)\n", 116 | "\n", 117 | "Then within klayout add a simple macro to import the flayout example library\n", 118 | "\n", 119 | "```python\n", 120 | "from flayout.example_lib import *\n", 121 | "```\n", 122 | "\n", 123 | "Note that running this macro takes a while in KLayout (about 10 seconds).\n", 124 | "\n", 125 | "After running the macro, open a new gdsfile and find the Flayout PCells in the \"F.E.L - Flayout Example Library\", which supplies two gdsfactory-defined components: the mzi and euler bend. These components are now imported as PCells!\n", 126 | "\n", 127 | "![gdsfactory_klayout_pcell](img/flayout_gf_mzi.png)\n", 128 | "\n", 129 | "You can have a look at how the [example library](flayout/example_lib.py) is implemented and try something similar for yourself!" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "id": "cd2bd927", 135 | "metadata": {}, 136 | "source": [ 137 | "## Quick Docs" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "id": "3db036f5", 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "# hide_input\n", 148 | "\n", 149 | "md = \"\"\n", 150 | "sidebar = json.load(open(\"docs/sidebar.json\"))\n", 151 | "for key, section in sidebar.items():\n", 152 | " md += f\"- **{key}**\\n\"\n", 153 | " for name, url in section.items():\n", 154 | " if url == \"index\":\n", 155 | " md += f\" - {name}\\n\"\n", 156 | " else:\n", 157 | " md += f\" - [{name}](https://flaport.github.io/flayout/{url})\\n\"\n", 158 | " if url == \"factories\":\n", 159 | " md = md[:-1] + \" ⬅ start here.\\n\"\n", 160 | " \n", 161 | "Markdown(md)" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "id": "5577e5ff-9423-4346-bb21-7fbfb257f2d7", 167 | "metadata": {}, 168 | "source": [ 169 | "## NBDev\n", 170 | "\n", 171 | "This project was created using [nbdev1](https://nbdev1.fast.ai/). An awesome [literate programming](https://en.wikipedia.org/wiki/Literate_programming) environment for python." 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "id": "d53d3c2a-2a56-4f61-b543-c22c0da82b52", 177 | "metadata": {}, 178 | "source": [ 179 | "## License\n", 180 | "\n", 181 | "As this library can be considered a light wrapper for the KLayout python api, I chose license it under [GPLv3](LICENSE)." 182 | ] 183 | } 184 | ], 185 | "metadata": { 186 | "kernelspec": { 187 | "display_name": "fl", 188 | "language": "python", 189 | "name": "fl" 190 | } 191 | }, 192 | "nbformat": 4, 193 | "nbformat_minor": 5 194 | } 195 | -------------------------------------------------------------------------------- /flayout/factories.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/01_factories.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['layer', 'point', 'vector', 'box', 'polygon', 'polygon', 'polygon', 'Point', 'Points', 'path', 'cell', 4 | 'Shape', 'transform', 'layout', 'library'] 5 | 6 | # Internal Cell 7 | from typing import Dict, List, Optional, Tuple, Union, overload 8 | 9 | import pya 10 | from .cell import ( 11 | LibReference, 12 | PCellLibReference, 13 | PCellReference, 14 | Reference, 15 | _add_cell_to_layout, 16 | _add_lib_cell_to_layout, 17 | _add_lib_pcell_to_layout, 18 | add_cells_to_layout, 19 | add_pcells_to_layout, 20 | reference, 21 | ) 22 | 23 | # Cell 24 | 25 | 26 | # Cell 27 | def layer(lr: int, dt: int, name: str = "") -> pya.LayerInfo: 28 | layer = pya.LayerInfo(int(lr), int(dt)) 29 | if name: 30 | layer.name = name 31 | return layer 32 | 33 | # Cell 34 | def point(x: Union[float, int], y: Union[float, int]) -> pya.DPoint: 35 | """create a KLayout DPoint 36 | 37 | Args: 38 | x: the x-value of the point 39 | y: the y-value of the point 40 | 41 | Returns: 42 | the point 43 | """ 44 | return pya.DPoint(float(x), float(y)) 45 | 46 | # Cell 47 | def vector(x: Union[float, int], y: Union[float, int]) -> pya.DVector: 48 | """create a KLayout DVector 49 | 50 | Args: 51 | x: the x-value of the vector 52 | y: the y-value of the vector 53 | 54 | Returns: 55 | the vector 56 | """ 57 | return pya.DVector(float(x), float(y)) 58 | 59 | # Cell 60 | 61 | def _validate_point(p): 62 | if isinstance(p, pya.Point): 63 | p = p.to_dtype() 64 | if not isinstance(p, pya.DPoint): 65 | p = pya.DPoint(*(float(x) for x in p)) 66 | return p 67 | 68 | def box(p1: Union[pya.Point, pya.DPoint], p2: Union[pya.Point, pya.DPoint]) -> pya.DBox: 69 | p1 = _validate_point(p1) 70 | p2 = _validate_point(p2) 71 | return pya.DBox(p1, p2) 72 | 73 | # Cell 74 | 75 | Point = Union[pya.DPoint, Tuple[int, int], Tuple[float, float], 'np.ndarray'] 76 | Points = Union[List[Point], 'np.ndarray'] 77 | 78 | @overload 79 | def polygon( 80 | hull: Points, 81 | *, 82 | holes: None = None, 83 | raw: bool = False, 84 | ) -> pya.DSimplePolygon: 85 | ... 86 | 87 | 88 | @overload 89 | def polygon( 90 | hull: Points, 91 | *, 92 | holes: List[Points] = None, 93 | raw: bool = False, 94 | ) -> pya.DPolygon: 95 | ... 96 | 97 | 98 | def polygon( 99 | hull: Points, 100 | *, 101 | holes: Optional[List[Points]] = None, 102 | raw: bool = False, 103 | ) -> Union[pya.DSimplePolygon, pya.DPolygon]: 104 | """create a KLayout polygon 105 | 106 | Args: 107 | hull: the points that make the polygon hull 108 | holes: the collection of points that make the polygon holes 109 | raw: if true, the points won't be compressed. 110 | 111 | Returns: 112 | the polygon 113 | """ 114 | hull = [(p if isinstance(p, (pya.Point, pya.DPoint)) else point(*p)) for p in hull] 115 | if holes is None: 116 | poly = pya.DSimplePolygon(hull, raw) 117 | else: 118 | poly = pya.DPolygon(hull, raw) 119 | for hole in holes: 120 | hole = [(p if isinstance(p, pya.DPoint) else point(*p)) for p in hole] 121 | poly.insert_hole(hole) 122 | return poly 123 | 124 | # Cell 125 | def path( 126 | pts: Points, 127 | width: float, 128 | *, 129 | bgn_ext: float = 0.0, 130 | end_ext: float = 0.0, 131 | round: bool = False, 132 | ) -> pya.DPath: 133 | """create a KLayout path 134 | 135 | Args: 136 | pts: the points that make the path 137 | width: the width of the path 138 | bgn_ext: the begin extension of the path 139 | end_ext: the end extension of the path 140 | round: round the ends of the path 141 | 142 | Returns: 143 | the path 144 | """ 145 | pts = [(p if isinstance(p, (pya.Point, pya.DPoint)) else point(*p)) for p in pts] 146 | print(pts) 147 | return pya.DPath(pts, width, bgn_ext, end_ext, round) 148 | 149 | # Cell 150 | Shape = Union[pya.DBox, pya.DSimplePolygon, pya.DPolygon, pya.DPath, pya.Box, pya.SimplePolygon, pya.Polygon, pya.Path] 151 | def cell( 152 | name: str, 153 | *, 154 | shapes: Optional[Dict[pya.LayerInfo, Shape]] = None, 155 | child_cells: Optional[List[Union[pya.Cell, Tuple[pya.Cell, pya.CplxTrans]]]] = None, 156 | ) -> pya.Cell: 157 | """create a KLayout cell 158 | 159 | Args: 160 | name: the name of the cell 161 | shapes: the shapes to add to the cell 162 | child_cells: the child cells to add to the cell 163 | 164 | Returns: 165 | the cell 166 | """ 167 | layout = pya.Layout() 168 | cell = layout.create_cell(name) 169 | cell._layout = layout # make sure layout does not get destroyed 170 | if shapes: 171 | for lr, lrshapes in shapes.items(): 172 | if not isinstance(lr, pya.LayerInfo): 173 | lr = layer(*lr) 174 | lr = layout.layer(lr) 175 | for i, shape in enumerate(lrshapes): 176 | 177 | # if isinstance(shape, np.ndarray): 178 | if type(shape).__name__ == 'ndarray': # yeah... I don't want to import numpy... 179 | lrshapes[i] = polygon(shape) 180 | 181 | for i, shape in enumerate(lrshapes): # TODO: insert all at once? 182 | cell.shapes(lr).insert(shape) 183 | if child_cells: 184 | cell_idxs = {} 185 | for ref in child_cells: 186 | if isinstance(ref, (str, pya.Cell, pya.PCellDeclaration)): 187 | ref = reference(ref) 188 | else: 189 | ref = reference(*ref) 190 | if isinstance(ref, Reference): 191 | if ref.cell not in cell_idxs: 192 | cell_idxs[ref.cell] = _add_cell_to_layout(layout, ref.cell) 193 | cell.insert(pya.CellInstArray(cell_idxs[ref.cell], ref.trans)) 194 | elif isinstance(ref, LibReference): 195 | # don't store index in cell_idxs... Cell belongs to library and will not be copied. 196 | idx = _add_lib_cell_to_layout(layout, ref.lib, ref.cell) 197 | cell.insert(pya.CellInstArray(idx, ref.trans)) 198 | elif isinstance(ref, PCellReference): 199 | raise ValueError( 200 | f"One can only add pcells belonging to a library to to a new cell. " 201 | f"Add a pcell reference using the following string format to " 202 | f"represent the cell: '.'." 203 | ) 204 | elif isinstance(ref, PCellLibReference): 205 | # don't store index in cell_idxs... PCell belongs to library and will not be copied. 206 | idx = _add_lib_pcell_to_layout(layout, ref.lib, ref.cell, ref.params) 207 | cell.insert(pya.CellInstArray(idx, ref.trans)) 208 | 209 | return cell 210 | 211 | # Cell 212 | def transform( 213 | mag: float = 1.0, 214 | rot: float = 0.0, 215 | mirrx: bool = False, 216 | x: float = 0.0, 217 | y: float = 0.0, 218 | ) -> pya.CplxTrans: 219 | """create a KLayout Transformation 220 | 221 | Args: 222 | mag: the magnitude of the transformation 223 | rot: the rotation of the transformation 224 | mirrx: mirror over the x-axis (x=0 line). 225 | x: translation distance in the x-direction 226 | y: translation distance in the y-direction 227 | 228 | Returns: 229 | the cell reference 230 | """ 231 | return pya.CplxTrans(mag, rot, mirrx, int(1000 * x), int(1000 * y)) 232 | 233 | # Cell 234 | def layout(*, cells: Optional[List[pya.Cell]] = None) -> pya.Layout: 235 | """create a KLayout Layout 236 | 237 | Args: 238 | cells: the cells to add to the layout 239 | 240 | Returns: 241 | the KLayout layout 242 | """ 243 | layout = pya.Layout() 244 | layout.dbu = 0.001 # hard coded for now 245 | if cells: 246 | add_cells_to_layout(layout, cells) 247 | return layout 248 | 249 | # Cell 250 | def library( 251 | name: str, 252 | *, 253 | cells: Optional[List[pya.Cell]] = None, 254 | pcells: Optional[List[pya.Cell]] = None, 255 | description: str = "", 256 | ) -> pya.Library: 257 | """create a KLayout Library 258 | 259 | Args: 260 | name: the name of the library 261 | cells: the cells to add to the library 262 | pcells: the pcells to add to the library 263 | 264 | Returns: 265 | the KLayout library 266 | """ 267 | lib = pya.Library() 268 | if description: 269 | lib.description = description 270 | layout = lib.layout() 271 | layout.dbu = 0.001 # hard coded for now 272 | if cells: 273 | add_cells_to_layout(layout, cells) 274 | if pcells: 275 | add_pcells_to_layout(layout, pcells) 276 | lib.register(name) 277 | return lib -------------------------------------------------------------------------------- /flayout/pcell.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/03_pcell.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['pcell'] 4 | 5 | # Internal Cell 6 | from functools import partial 7 | from inspect import Parameter, Signature, signature 8 | from typing import Callable, Optional 9 | 10 | import pya 11 | from .cell import copy_tree 12 | 13 | CELL_CONVERTERS = {} 14 | 15 | # Internal Cell 16 | def _klayout_type(param: Parameter): 17 | type_map = { 18 | pya.PCellDeclarationHelper.TypeInt: pya.PCellDeclarationHelper.TypeInt, 19 | "TypeInt": pya.PCellDeclarationHelper.TypeInt, 20 | "int": pya.PCellDeclarationHelper.TypeInt, 21 | int: pya.PCellDeclarationHelper.TypeInt, 22 | Optional[int]: pya.PCellDeclarationHelper.TypeInt, 23 | pya.PCellDeclarationHelper.TypeDouble: pya.PCellDeclarationHelper.TypeDouble, 24 | "TypeDouble": pya.PCellDeclarationHelper.TypeDouble, 25 | "float": pya.PCellDeclarationHelper.TypeDouble, 26 | float: pya.PCellDeclarationHelper.TypeDouble, 27 | Optional[float]: pya.PCellDeclarationHelper.TypeDouble, 28 | pya.PCellDeclarationHelper.TypeString: pya.PCellDeclarationHelper.TypeString, 29 | "TypeString": pya.PCellDeclarationHelper.TypeString, 30 | "str": pya.PCellDeclarationHelper.TypeString, 31 | str: pya.PCellDeclarationHelper.TypeString, 32 | Optional[str]: pya.PCellDeclarationHelper.TypeString, 33 | pya.PCellDeclarationHelper.TypeBoolean: pya.PCellDeclarationHelper.TypeBoolean, 34 | "TypeBoolean": pya.PCellDeclarationHelper.TypeBoolean, 35 | "bool": pya.PCellDeclarationHelper.TypeBoolean, 36 | bool: pya.PCellDeclarationHelper.TypeBoolean, 37 | Optional[bool]: pya.PCellDeclarationHelper.TypeBoolean, 38 | pya.PCellDeclarationHelper.TypeLayer: pya.PCellDeclarationHelper.TypeLayer, 39 | "TypeLayer": pya.PCellDeclarationHelper.TypeLayer, 40 | "LayerInfo": pya.PCellDeclarationHelper.TypeLayer, 41 | pya.LayerInfo: pya.PCellDeclarationHelper.TypeLayer, 42 | pya.PCellDeclarationHelper.TypeShape: pya.PCellDeclarationHelper.TypeShape, 43 | "TypeShape": pya.PCellDeclarationHelper.TypeShape, 44 | "Shape": pya.PCellDeclarationHelper.TypeShape, 45 | pya.Shape: pya.PCellDeclarationHelper.TypeShape, 46 | pya.PCellDeclarationHelper.TypeList: pya.PCellDeclarationHelper.TypeList, 47 | "TypeList": pya.PCellDeclarationHelper.TypeList, 48 | "list": pya.PCellDeclarationHelper.TypeList, 49 | list: pya.PCellDeclarationHelper.TypeList, 50 | Optional[list]: pya.PCellDeclarationHelper.TypeList, 51 | } 52 | try: 53 | annotation = param.annotation 54 | if annotation is Parameter.empty: 55 | annotation = type(param.default) 56 | except AttributeError: 57 | annotation = param 58 | if not annotation in type_map: 59 | raise ValueError( 60 | f"Cannot create pcell. Parameter {param.name!r} has unsupported type: {annotation!r}" 61 | ) 62 | return type_map[annotation] 63 | 64 | # Internal Cell 65 | def _python_type(param: Parameter): 66 | type_map = { 67 | pya.PCellDeclarationHelper.TypeInt: int, 68 | "TypeInt": int, 69 | "int": int, 70 | int: int, 71 | pya.PCellDeclarationHelper.TypeDouble: float, 72 | "TypeDouble": float, 73 | "float": float, 74 | float: float, 75 | pya.PCellDeclarationHelper.TypeString: str, 76 | "TypeString": str, 77 | "str": str, 78 | str: str, 79 | pya.PCellDeclarationHelper.TypeBoolean: bool, 80 | "TypeBoolean": bool, 81 | "bool": bool, 82 | bool: bool, 83 | pya.PCellDeclarationHelper.TypeLayer: pya.LayerInfo, 84 | "TypeLayer": pya.LayerInfo, 85 | "LayerInfo": pya.LayerInfo, 86 | pya.LayerInfo: pya.LayerInfo, 87 | pya.PCellDeclarationHelper.TypeShape: pya.Shape, 88 | "TypeShape": pya.Shape, 89 | "Shape": pya.Shape, 90 | pya.Shape: pya.Shape, 91 | pya.PCellDeclarationHelper.TypeList: list, 92 | "TypeList": list, 93 | "list": list, 94 | list: list, 95 | } 96 | try: 97 | annotation = param.annotation 98 | if annotation is Parameter.empty: 99 | annotation = type(param.default) 100 | except AttributeError: 101 | annotation = param 102 | if not annotation in type_map: 103 | raise ValueError( 104 | f"Cannot create pcell. Parameter {param.name!r} has unsupported type: {annotation!r}" 105 | ) 106 | return type_map[annotation] 107 | 108 | # Internal Cell 109 | 110 | def _validate_on_error(on_error): 111 | on_error = on_error.lower() 112 | if not on_error in ["raise", "ignore"]: 113 | raise ValueError("on_error should be 'raise' or 'ignore'.") 114 | return on_error 115 | 116 | def _validate_parameter(name, param): 117 | if param.kind == Parameter.VAR_POSITIONAL: 118 | raise ValueError( 119 | f"Cannot create pcell from functions with var positional [*args] arguments." 120 | ) 121 | elif param.kind == Parameter.VAR_KEYWORD: 122 | raise ValueError( 123 | f"Cannot create pcell from functions with var keyword [**kwargs] arguments." 124 | ) 125 | elif param.kind == Parameter.POSITIONAL_ONLY: 126 | raise ValueError( 127 | f"Cannot create pcell from functions with positional arguments. Please use keyword arguments." 128 | ) 129 | elif (param.kind == Parameter.POSITIONAL_OR_KEYWORD) and (param.default is Parameter.empty): 130 | raise ValueError( 131 | f"Cannot create pcell from functions with positional arguments. Please use keyword arguments." 132 | ) 133 | annotation = _python_type(_klayout_type(_python_type(param))) 134 | default = param.default 135 | try: 136 | default = annotation(default) 137 | except Exception: 138 | pass 139 | return Parameter( 140 | name, 141 | kind=Parameter.KEYWORD_ONLY, 142 | default=default, 143 | annotation=annotation, 144 | ) 145 | 146 | def _pcell_parameters(func: Callable, on_error="raise"): 147 | sig = signature(func) 148 | params = sig.parameters 149 | on_error = _validate_on_error(on_error) 150 | new_params = { 151 | "name": Parameter( 152 | "name", kind=Parameter.KEYWORD_ONLY, default=func.__name__, annotation=str 153 | ) 154 | } 155 | for name, param in params.items(): 156 | try: 157 | new_params[name] = _validate_parameter(name, param) 158 | except ValueError: 159 | if on_error == "raise": 160 | raise 161 | return new_params 162 | 163 | # Cell 164 | 165 | def pcell(func=None, on_error="raise"): 166 | """create a KLayout PCell from a native python function 167 | 168 | Args: 169 | func: the function creating a KLayout cell 170 | 171 | Returns: 172 | the Klayout PCell 173 | """ 174 | if func is None: 175 | return partial(pcell, on_error=on_error) 176 | 177 | params = _pcell_parameters(func, on_error=on_error) 178 | 179 | 180 | def init(self): 181 | pya.PCellDeclarationHelper.__init__(self) 182 | self._params = {} 183 | for name, param in params.items(): 184 | self.param( 185 | name=name, 186 | value_type=_klayout_type(param), 187 | description=name.replace("_", " "), 188 | default=param.default, 189 | ) 190 | self._params[name] = param 191 | self.func = func 192 | 193 | def call(self, **kwargs): 194 | name = kwargs.pop("name", (self.name or func.__name__)) 195 | try: 196 | obj = self.func(**kwargs, name=name) 197 | except TypeError: 198 | obj = self.func(**kwargs) 199 | obj.name = name 200 | return obj 201 | 202 | def produce_impl(self): 203 | kwargs = {} 204 | for name, param in self._params.items(): 205 | v = getattr(self, name) 206 | if v is None: 207 | v = param.default 208 | if v is Parameter.empty: 209 | continue 210 | kwargs[name] = v 211 | print(kwargs) 212 | cell = self(**kwargs) 213 | copy_tree(cell, self.cell, on_same_name="replace") 214 | 215 | def display_text_impl(self): 216 | return f"{self.name}<{self.__class__.__name__}>" 217 | 218 | DynamicPCell = type( 219 | func.__name__, 220 | (pya.PCellDeclarationHelper,), 221 | { 222 | "__init__": init, 223 | "__call__": call, 224 | "__doc__": func.__doc__ 225 | if func.__doc__ is not None 226 | else f"a {func.__name__} PCell.", 227 | "produce_impl": produce_impl, 228 | "display_text_impl": display_text_impl, 229 | }, 230 | ) 231 | pcell = DynamicPCell() 232 | pcell.__signature__ = Signature(list(params.values()), return_annotation=pya.Cell) 233 | pcell.name = func.__name__ 234 | return pcell -------------------------------------------------------------------------------- /flayout/cell.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/02_cell.ipynb (unless otherwise specified). 2 | 3 | __all__ = ['reference', 'copy_tree', 'add_cells_to_layout', 'add_pcells_to_layout'] 4 | 5 | # Internal Cell 6 | import sys 7 | from typing import Any, Dict, List, NamedTuple, Union 8 | 9 | import flayout as fl 10 | import pya 11 | 12 | 13 | class DoesNotExist: 14 | pass 15 | 16 | try: 17 | from gdspy import Cell as GdsPyCell 18 | except ImportError: 19 | GdsPyCell = DoesNotExist 20 | 21 | # Internal Cell 22 | COPY_IMPLEMENTATIONS = {} 23 | MAX_DEPTH = 2 * sys.getrecursionlimit() 24 | ON_SAME_NAME_DOC = """what to do when the layout containing the destination cell 25 | already contains a cell with the same name. 'skip' (default): don't 26 | add the new child cell, use the one already present in the layout 27 | in stead. This will make the instances in the new cell point to the 28 | cell already in the layout. 'replace': replace the cell 29 | already present in the layout. This will make the instances in all 30 | cells alread present in the target cell's layout point to the newly 31 | added cell. 'add_suffix': add the new cell, but change its name by 32 | adding a suffix (this is the default behavior of KLayout's native 33 | cell.copy_tree method.)""" 34 | 35 | # Internal Cell 36 | class Reference(NamedTuple): 37 | cell: pya.Cell 38 | trans: pya.CplxTrans 39 | 40 | 41 | class LibReference(NamedTuple): 42 | lib: pya.Library 43 | cell: pya.Cell 44 | trans: pya.CplxTrans 45 | 46 | 47 | class PCellReference(NamedTuple): 48 | cell: pya.PCellDeclaration 49 | trans: pya.CplxTrans 50 | params: Dict 51 | 52 | 53 | class PCellLibReference(NamedTuple): 54 | lib: pya.Library 55 | cell: pya.PCellDeclaration 56 | trans: pya.CplxTrans 57 | params: Dict 58 | 59 | # Cell 60 | def reference(*args) -> Union[Reference, LibReference, PCellReference, PCellLibReference]: 61 | """create a cell reference 62 | 63 | Note: 64 | this is not a native KLayout datastructure, 65 | but rather a convenience wrapper to ease the cell constructor API. 66 | 67 | Args: 68 | cell: the cell to create a reference for 69 | trans: the transformation to apply to the cell upon adding 70 | 71 | Returns: 72 | the cell reference 73 | """ 74 | cell_str = _get_object_from_type(args, str) 75 | if cell_str is not None: 76 | num_dots = cell_str.count(".") 77 | if num_dots != 1: 78 | raise ValueError( 79 | "Reference to cell by string name should have format '.'." 80 | ) 81 | lib_name, cell_name = cell_str.split(".") 82 | lib = pya.Library.library_by_name(lib_name) 83 | if lib is None: 84 | raise ValueError( 85 | f"Cannot use cell {cell_str!r}. Library {lib_name!r} does not exist (or is not registered)." 86 | ) 87 | 88 | # search for pcell in library 89 | cell = lib.layout().pcell_declaration(cell_name) 90 | 91 | # search for normal cell in library 92 | if cell is None: 93 | try: 94 | cell_idx = lib.layout().cell_by_name(cell_name) 95 | cell = lib.layout().cell(cell_idx) 96 | except RuntimeError: 97 | raise ValueError( 98 | f"Cannot use cell {cell_str!r}. Cell {cell_name!r} does not exist " 99 | f"in library {lib_name!r}." 100 | ) 101 | 102 | args = ( 103 | arg 104 | for arg in args 105 | if not (str(arg) == cell_str or arg is cell or arg is lib) 106 | ) 107 | return reference(cell, lib, *args) 108 | 109 | lib = _get_object_from_type(args, pya.Library) 110 | cell = _get_object_from_type(args, (pya.Cell, pya.PCellDeclaration)) 111 | trans = _get_object_from_type( 112 | args, 113 | (pya.CplxTrans, pya.ICplxTrans, pya.DCplxTrans), 114 | default=pya.CplxTrans(1.0, 0.0, False, 0, 0), 115 | ) 116 | 117 | if isinstance(cell, pya.Cell): 118 | if lib is None: 119 | return Reference(cell, trans) 120 | else: 121 | return LibReference(lib, cell, trans) 122 | elif isinstance(cell, pya.PCellDeclaration): 123 | params = _get_object_from_type(args, dict, default={}) 124 | if lib is None: 125 | return PCellReference(cell, trans, params) 126 | else: 127 | return PCellLibReference(lib, cell, trans, params) 128 | else: 129 | raise ValueError(f"No cell found in reference tuple: {tuple(args)}.") 130 | 131 | def _get_object_from_type(objs, cls, default=None): 132 | selected = [obj for obj in objs if isinstance(obj, cls)] 133 | if len(selected) > 1: 134 | raise ValueError( 135 | f"Only one argument of type {cls.__name__!r} expected. " 136 | f"Got: {', '.join(repr(s) for s in selected)}." 137 | ) 138 | if not selected: 139 | return default 140 | return selected[0] 141 | 142 | # Internal Cell 143 | def _copy_klayout(source, dest, on_same_name, depth=0): 144 | if depth > MAX_DEPTH: 145 | return # basically just to make type checker happy 146 | 147 | # if on_same_name == "add_suffix": 148 | # return dest.copy_tree(source) 149 | # # default klayout behavior. Not used because our implementation is 150 | # # slightly different in the order cells are added 151 | 152 | dest_layout = dest.layout() 153 | source_layout = source.layout() 154 | for layer in source_layout.layer_infos(): 155 | source_idx = source_layout.layer(layer) 156 | dest_idx = dest_layout.layer(layer) 157 | for shape in source.each_shape(source_idx): 158 | dest.shapes(dest_idx).insert(shape) 159 | 160 | source_cells_map = {} 161 | for idx in source.each_child_cell(): 162 | current_cell = source_layout.cell(idx) 163 | source_cells_map[idx] = _add_cell_to_layout( 164 | dest_layout, current_cell, on_same_name, depth 165 | ) 166 | 167 | for inst in source.each_inst(): 168 | dest_ref = pya.CellInstArray(source_cells_map[inst.cell_index], inst.cplx_trans) 169 | dest.insert(dest_ref) 170 | 171 | COPY_IMPLEMENTATIONS[pya.Cell] = _copy_klayout 172 | 173 | # Internal Cell 174 | def _copy_gdspy(source, dest, on_same_name, depth=0): 175 | if depth > MAX_DEPTH: 176 | return # basically just to make type checker happy 177 | dest_layout = dest.layout() 178 | for lr, dt in source.get_layers(): 179 | dest_idx = dest_layout.layer(pya.LayerInfo(lr, dt)) 180 | for arr in source.get_polygons(by_spec=(lr, dt), depth=1): 181 | dest.shapes(dest_idx).insert( 182 | pya.DPolygon([pya.DPoint(x, y) for x, y in arr]) 183 | ) 184 | for arr in source.get_paths(depth=1): 185 | raise NotImplementedError("Cannot convert native gdspy paths (yet).") 186 | 187 | source_cells_map = {} 188 | child_cells = sorted( 189 | set(ref.ref_cell for ref in source.references), key=lambda c: c.name 190 | ) 191 | for current_cell in child_cells: 192 | if on_same_name == "skip" and dest_layout.has_cell(current_cell.name): 193 | source_cells_map[current_cell.name] = dest_layout.cell_by_name( 194 | current_cell.name 195 | ) 196 | continue 197 | elif on_same_name == "replace" and dest_layout.has_cell(current_cell.name): 198 | dest_layout.delete_cell(dest_layout.cell_by_name(current_cell.name)) 199 | new_cell = dest_layout.create_cell(current_cell.name) 200 | _copy_gdspy(current_cell, new_cell, on_same_name, depth=depth + 1) 201 | new_cell._layout = dest_layout 202 | source_cells_map[current_cell.name] = dest_layout.cell_by_name( 203 | current_cell.name 204 | ) 205 | 206 | for ref in source.references: 207 | dest_cell = source_cells_map[ref.ref_cell.name] 208 | mag = ref.magnification if ref.magnification is not None else 1.0 209 | rot = ref.rotation if ref.rotation is not None else 0.0 210 | x, y = ref.origin 211 | dest_trans = pya.CplxTrans( 212 | mag, rot, ref.x_reflection, int(1000.0 * x), int(1000.0 * y) 213 | ) 214 | dest_ref = pya.CellInstArray(dest_cell, dest_trans) 215 | dest.insert(dest_ref) 216 | 217 | if GdsPyCell is not DoesNotExist: 218 | COPY_IMPLEMENTATIONS[GdsPyCell] = _copy_gdspy 219 | 220 | # Cell 221 | 222 | def copy_tree(source: pya.Cell, dest: pya.Cell, on_same_name: str = "skip"): 223 | f"""Copy the contents of a cell into another cell 224 | 225 | Args: 226 | source: the source cell to copy the contents from 227 | dest: the destination cell to copy the contents into 228 | on_same_name: {ON_SAME_NAME_DOC} 229 | """ 230 | on_same_name = _validate_on_same_name(on_same_name) 231 | for cls, _copy_impl in COPY_IMPLEMENTATIONS.items(): 232 | if isinstance(source, cls): 233 | break 234 | else: 235 | raise TypeError( 236 | f"Error in copy_tree: source is not a supported cell type. " 237 | f"Got: {type(source)}. Expected: {', '.join(t.__name__ for t in COPY_IMPLEMENTATIONS)}." 238 | ) 239 | 240 | _copy_impl(source, dest, on_same_name) 241 | return dest 242 | 243 | def _validate_on_same_name(on_same_name): 244 | on_same_name = on_same_name.lower() 245 | allowed_on_same_name = ["skip", "replace", "add_suffix"] 246 | if not on_same_name in allowed_on_same_name: 247 | raise ValueError( 248 | "on_same_name should be one of the following: " 249 | f"{', '.join(repr(key) for key in allowed_on_same_name)}." 250 | ) 251 | return on_same_name 252 | 253 | # Cell 254 | def add_cells_to_layout( 255 | layout: pya.Layout, 256 | cells: List[pya.Cell], 257 | on_same_name: str = "skip", 258 | depth: int = 0, 259 | ): 260 | f"""Add multiple cells to a layout 261 | 262 | Args: 263 | layout: The layout to add the cell into 264 | cells: the cells to add into the layout 265 | on_same_name: {ON_SAME_NAME_DOC} 266 | """ 267 | cells = sorted(cells, key=lambda c: c.hierarchy_levels()) 268 | for cell in cells: 269 | _add_cell_to_layout(layout, cell, on_same_name, depth) 270 | return layout 271 | 272 | def _add_lib_cell_to_layout(layout: pya.Layout, lib: pya.Library, cell: pya.Cell): 273 | """Add a library Cell to a layout 274 | 275 | Args: 276 | layout: The layout to add the cell into 277 | lib: The library to which the cell belongs 278 | cell: the cell to add into the layout 279 | """ 280 | pcell = cell.pcell_declaration() 281 | if pcell is not None: 282 | return _add_lib_pcell_to_layout(layout, lib, pcell, cell.pcell_parameters()) 283 | else: 284 | cell_idx = lib.layout().cell_by_name(cell.name) 285 | return layout.add_lib_cell(lib, cell_idx) 286 | 287 | 288 | def _add_cell_to_layout( 289 | layout: pya.Layout, cell: pya.Cell, on_same_name: str = "skip", depth: int = 0 290 | ): 291 | f"""Add a cell to a layout 292 | 293 | Args: 294 | layout: The layout to add the cell into 295 | cell: the cell to add into the layout 296 | on_same_name: {ON_SAME_NAME_DOC} 297 | """ 298 | if cell.is_proxy(): 299 | _add_lib_cell_to_layout(layout, cell.library(), cell) 300 | if on_same_name == "skip" and layout.has_cell(cell.name): 301 | return layout.cell_by_name(cell.name) 302 | elif on_same_name == "replace" and layout.has_cell(cell.name): 303 | layout.delete_cell(layout.cell_by_name(cell.name)) 304 | new_cell = layout.create_cell(cell.name) 305 | _copy_klayout(cell, new_cell, on_same_name, depth=depth + 1) 306 | new_cell._layout = layout 307 | return layout.cell_by_name(new_cell.name) 308 | 309 | # Cell 310 | 311 | def add_pcells_to_layout(layout, pcells): 312 | for pcell in pcells: 313 | func = pcell 314 | while hasattr(func, "func"): 315 | func = func.func 316 | layout.register_pcell(func.__name__, pcell) 317 | 318 | def _get_pcell_param_value(params, param): 319 | value = params.get(param.name, param.default) 320 | if param.type == pya.PCellDeclarationHelper.TypeLayer: 321 | if not isinstance(value, pya.LayerInfo): 322 | value = pya.LayerInfo(*value) 323 | return value 324 | 325 | def _add_lib_pcell_to_layout( 326 | layout: pya.Layout, 327 | lib: pya.Library, 328 | pcell: pya.PCellDeclaration, 329 | params: Dict[str, Any], 330 | ): 331 | """Add a library PCell to a layout 332 | 333 | Args: 334 | layout: The layout to add the cell into 335 | lib: The library to which the cell belongs 336 | cell: the cell to add into the layout 337 | params: the parameters to instantiate the pcell with 338 | """ 339 | if isinstance(params, dict): 340 | params_list = [ 341 | _get_pcell_param_value(params, p) for p in pcell.get_parameters() 342 | ] 343 | else: 344 | params_list = params 345 | return layout.add_pcell_variant(lib, pcell.id(), params_list) -------------------------------------------------------------------------------- /source/03_pcell.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "43b948a8-2cf4-4b94-87e6-875151816a83", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp pcell" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "5a690dd2-e96d-4a34-be54-56a11e25cbf1", 16 | "metadata": {}, 17 | "source": [ 18 | "# PCells\n", 19 | "> Friendly PCell definitions" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "b8de1094-781a-4529-ae23-374bf46b063b", 25 | "metadata": {}, 26 | "source": [ 27 | "This library offers a klayout [PCell decorator](https://flaport.github.io/flayout/pcell), which is much easier to use than the default PCell offered by the KLayout api. Moreover, the PCell decorator can be with a [GDSFactory](https://github.com/gdsfactory/gdsfactory) component function (or any function that produces a [gdspy](https://github.com/heitzmann/gdspy) cell) as well!\n", 28 | "\n", 29 | "To use FLayout PCells, first install flayout as a system python package (so that you can access it through the klayout gui):\n", 30 | "\n", 31 | "```sh\n", 32 | "cd /path/to/flayout\n", 33 | "pip install --user .\n", 34 | "```\n", 35 | "\n", 36 | "Alternatively, you can also open the klayout gui from *within* the `fl` python environment (linux only)\n", 37 | "\n", 38 | "Then within klayout add a simple macro to import the flayout example library\n", 39 | "\n", 40 | "```python\n", 41 | "from flayout.example_lib import *\n", 42 | "```\n", 43 | "\n", 44 | "Note that running this macro takes a while in KLayout (about 10 seconds).\n", 45 | "\n", 46 | "After running the macro, open a new gdsfile and find the Flayout PCells in the \"F.E.L - Flayout Example Library\", which supplies two gdsfactory-defined components: the mzi and euler bend. These components are now imported as PCells!\n", 47 | "\n", 48 | "You can have a look at how the [example library](example_lib.py) is implemented and try something similar for yourself!" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "420feb72-c2b2-4b99-be1f-b3ce33c36d24", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "# exporti\n", 59 | "from functools import partial\n", 60 | "from inspect import Parameter, Signature, signature\n", 61 | "from typing import Callable, Optional\n", 62 | "\n", 63 | "import pya\n", 64 | "from flayout.cell import copy_tree\n", 65 | "\n", 66 | "CELL_CONVERTERS = {}" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "id": "51fc2ddf-b7bd-44c5-9fe6-157134a03950", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "# exporti\n", 77 | "def _klayout_type(param: Parameter):\n", 78 | " type_map = {\n", 79 | " pya.PCellDeclarationHelper.TypeInt: pya.PCellDeclarationHelper.TypeInt,\n", 80 | " \"TypeInt\": pya.PCellDeclarationHelper.TypeInt,\n", 81 | " \"int\": pya.PCellDeclarationHelper.TypeInt,\n", 82 | " int: pya.PCellDeclarationHelper.TypeInt,\n", 83 | " Optional[int]: pya.PCellDeclarationHelper.TypeInt,\n", 84 | " pya.PCellDeclarationHelper.TypeDouble: pya.PCellDeclarationHelper.TypeDouble,\n", 85 | " \"TypeDouble\": pya.PCellDeclarationHelper.TypeDouble,\n", 86 | " \"float\": pya.PCellDeclarationHelper.TypeDouble,\n", 87 | " float: pya.PCellDeclarationHelper.TypeDouble,\n", 88 | " Optional[float]: pya.PCellDeclarationHelper.TypeDouble,\n", 89 | " pya.PCellDeclarationHelper.TypeString: pya.PCellDeclarationHelper.TypeString,\n", 90 | " \"TypeString\": pya.PCellDeclarationHelper.TypeString,\n", 91 | " \"str\": pya.PCellDeclarationHelper.TypeString,\n", 92 | " str: pya.PCellDeclarationHelper.TypeString,\n", 93 | " Optional[str]: pya.PCellDeclarationHelper.TypeString,\n", 94 | " pya.PCellDeclarationHelper.TypeBoolean: pya.PCellDeclarationHelper.TypeBoolean,\n", 95 | " \"TypeBoolean\": pya.PCellDeclarationHelper.TypeBoolean,\n", 96 | " \"bool\": pya.PCellDeclarationHelper.TypeBoolean,\n", 97 | " bool: pya.PCellDeclarationHelper.TypeBoolean,\n", 98 | " Optional[bool]: pya.PCellDeclarationHelper.TypeBoolean,\n", 99 | " pya.PCellDeclarationHelper.TypeLayer: pya.PCellDeclarationHelper.TypeLayer,\n", 100 | " \"TypeLayer\": pya.PCellDeclarationHelper.TypeLayer,\n", 101 | " \"LayerInfo\": pya.PCellDeclarationHelper.TypeLayer,\n", 102 | " pya.LayerInfo: pya.PCellDeclarationHelper.TypeLayer,\n", 103 | " pya.PCellDeclarationHelper.TypeShape: pya.PCellDeclarationHelper.TypeShape,\n", 104 | " \"TypeShape\": pya.PCellDeclarationHelper.TypeShape,\n", 105 | " \"Shape\": pya.PCellDeclarationHelper.TypeShape,\n", 106 | " pya.Shape: pya.PCellDeclarationHelper.TypeShape,\n", 107 | " pya.PCellDeclarationHelper.TypeList: pya.PCellDeclarationHelper.TypeList,\n", 108 | " \"TypeList\": pya.PCellDeclarationHelper.TypeList,\n", 109 | " \"list\": pya.PCellDeclarationHelper.TypeList,\n", 110 | " list: pya.PCellDeclarationHelper.TypeList,\n", 111 | " Optional[list]: pya.PCellDeclarationHelper.TypeList,\n", 112 | " }\n", 113 | " try:\n", 114 | " annotation = param.annotation\n", 115 | " if annotation is Parameter.empty:\n", 116 | " annotation = type(param.default)\n", 117 | " except AttributeError:\n", 118 | " annotation = param\n", 119 | " if not annotation in type_map:\n", 120 | " raise ValueError(\n", 121 | " f\"Cannot create pcell. Parameter {param.name!r} has unsupported type: {annotation!r}\"\n", 122 | " )\n", 123 | " return type_map[annotation]" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "id": "99fddcfe-a532-4b5c-9ddb-b4f8016d515f", 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "# exporti\n", 134 | "def _python_type(param: Parameter):\n", 135 | " type_map = {\n", 136 | " pya.PCellDeclarationHelper.TypeInt: int,\n", 137 | " \"TypeInt\": int,\n", 138 | " \"int\": int,\n", 139 | " int: int,\n", 140 | " pya.PCellDeclarationHelper.TypeDouble: float,\n", 141 | " \"TypeDouble\": float,\n", 142 | " \"float\": float,\n", 143 | " float: float,\n", 144 | " pya.PCellDeclarationHelper.TypeString: str,\n", 145 | " \"TypeString\": str,\n", 146 | " \"str\": str,\n", 147 | " str: str,\n", 148 | " pya.PCellDeclarationHelper.TypeBoolean: bool,\n", 149 | " \"TypeBoolean\": bool,\n", 150 | " \"bool\": bool,\n", 151 | " bool: bool,\n", 152 | " pya.PCellDeclarationHelper.TypeLayer: pya.LayerInfo,\n", 153 | " \"TypeLayer\": pya.LayerInfo,\n", 154 | " \"LayerInfo\": pya.LayerInfo,\n", 155 | " pya.LayerInfo: pya.LayerInfo,\n", 156 | " pya.PCellDeclarationHelper.TypeShape: pya.Shape,\n", 157 | " \"TypeShape\": pya.Shape,\n", 158 | " \"Shape\": pya.Shape,\n", 159 | " pya.Shape: pya.Shape,\n", 160 | " pya.PCellDeclarationHelper.TypeList: list,\n", 161 | " \"TypeList\": list,\n", 162 | " \"list\": list,\n", 163 | " list: list,\n", 164 | " }\n", 165 | " try:\n", 166 | " annotation = param.annotation\n", 167 | " if annotation is Parameter.empty:\n", 168 | " annotation = type(param.default)\n", 169 | " except AttributeError:\n", 170 | " annotation = param\n", 171 | " if not annotation in type_map:\n", 172 | " raise ValueError(\n", 173 | " f\"Cannot create pcell. Parameter {param.name!r} has unsupported type: {annotation!r}\"\n", 174 | " )\n", 175 | " return type_map[annotation]" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "id": "79e106f8-d68c-479c-82de-918edacca56b", 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "# exporti\n", 186 | "\n", 187 | "def _validate_on_error(on_error):\n", 188 | " on_error = on_error.lower()\n", 189 | " if not on_error in [\"raise\", \"ignore\"]:\n", 190 | " raise ValueError(\"on_error should be 'raise' or 'ignore'.\")\n", 191 | " return on_error\n", 192 | "\n", 193 | "def _validate_parameter(name, param):\n", 194 | " if param.kind == Parameter.VAR_POSITIONAL:\n", 195 | " raise ValueError(\n", 196 | " f\"Cannot create pcell from functions with var positional [*args] arguments.\"\n", 197 | " )\n", 198 | " elif param.kind == Parameter.VAR_KEYWORD:\n", 199 | " raise ValueError(\n", 200 | " f\"Cannot create pcell from functions with var keyword [**kwargs] arguments.\"\n", 201 | " )\n", 202 | " elif param.kind == Parameter.POSITIONAL_ONLY:\n", 203 | " raise ValueError(\n", 204 | " f\"Cannot create pcell from functions with positional arguments. Please use keyword arguments.\"\n", 205 | " )\n", 206 | " elif (param.kind == Parameter.POSITIONAL_OR_KEYWORD) and (param.default is Parameter.empty):\n", 207 | " raise ValueError(\n", 208 | " f\"Cannot create pcell from functions with positional arguments. Please use keyword arguments.\"\n", 209 | " )\n", 210 | " annotation = _python_type(_klayout_type(_python_type(param)))\n", 211 | " default = param.default\n", 212 | " try:\n", 213 | " default = annotation(default)\n", 214 | " except Exception:\n", 215 | " pass\n", 216 | " return Parameter(\n", 217 | " name,\n", 218 | " kind=Parameter.KEYWORD_ONLY,\n", 219 | " default=default,\n", 220 | " annotation=annotation,\n", 221 | " )\n", 222 | "\n", 223 | "def _pcell_parameters(func: Callable, on_error=\"raise\"):\n", 224 | " sig = signature(func)\n", 225 | " params = sig.parameters\n", 226 | " on_error = _validate_on_error(on_error)\n", 227 | " new_params = {\n", 228 | " \"name\": Parameter(\n", 229 | " \"name\", kind=Parameter.KEYWORD_ONLY, default=func.__name__, annotation=str\n", 230 | " )\n", 231 | " }\n", 232 | " for name, param in params.items():\n", 233 | " try:\n", 234 | " new_params[name] = _validate_parameter(name, param)\n", 235 | " except ValueError:\n", 236 | " if on_error == \"raise\":\n", 237 | " raise\n", 238 | " return new_params" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "id": "51c6e9e0-f791-4de6-a072-8efb6a1e09db", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "# hide\n", 249 | "def func(x: int = 3, y: float = 5.0, z=6.0, d: pya.Point = pya.Point(3, 4)):\n", 250 | " return (x, y)\n", 251 | "_pcell_parameters(func, on_error=\"ignore\")" 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": null, 257 | "id": "f3d1f3fd-e5a7-4523-aabd-66bc6d51834a", 258 | "metadata": {}, 259 | "outputs": [], 260 | "source": [ 261 | "# export\n", 262 | "\n", 263 | "def pcell(func=None, on_error=\"raise\"):\n", 264 | " \"\"\"create a KLayout PCell from a native python function\n", 265 | "\n", 266 | " Args:\n", 267 | " func: the function creating a KLayout cell\n", 268 | "\n", 269 | " Returns:\n", 270 | " the Klayout PCell\n", 271 | " \"\"\"\n", 272 | " if func is None:\n", 273 | " return partial(pcell, on_error=on_error)\n", 274 | " \n", 275 | " params = _pcell_parameters(func, on_error=on_error)\n", 276 | " \n", 277 | "\n", 278 | " def init(self):\n", 279 | " pya.PCellDeclarationHelper.__init__(self)\n", 280 | " self._params = {}\n", 281 | " for name, param in params.items():\n", 282 | " self.param(\n", 283 | " name=name,\n", 284 | " value_type=_klayout_type(param),\n", 285 | " description=name.replace(\"_\", \" \"),\n", 286 | " default=param.default,\n", 287 | " )\n", 288 | " self._params[name] = param\n", 289 | " self.func = func\n", 290 | "\n", 291 | " def call(self, **kwargs):\n", 292 | " name = kwargs.pop(\"name\", (self.name or func.__name__))\n", 293 | " try:\n", 294 | " obj = self.func(**kwargs, name=name)\n", 295 | " except TypeError:\n", 296 | " obj = self.func(**kwargs)\n", 297 | " obj.name = name\n", 298 | " return obj\n", 299 | "\n", 300 | " def produce_impl(self):\n", 301 | " kwargs = {}\n", 302 | " for name, param in self._params.items():\n", 303 | " v = getattr(self, name)\n", 304 | " if v is None:\n", 305 | " v = param.default\n", 306 | " if v is Parameter.empty:\n", 307 | " continue\n", 308 | " kwargs[name] = v\n", 309 | " print(kwargs)\n", 310 | " cell = self(**kwargs)\n", 311 | " copy_tree(cell, self.cell, on_same_name=\"replace\")\n", 312 | "\n", 313 | " def display_text_impl(self):\n", 314 | " return f\"{self.name}<{self.__class__.__name__}>\"\n", 315 | "\n", 316 | " DynamicPCell = type(\n", 317 | " func.__name__,\n", 318 | " (pya.PCellDeclarationHelper,),\n", 319 | " {\n", 320 | " \"__init__\": init,\n", 321 | " \"__call__\": call,\n", 322 | " \"__doc__\": func.__doc__\n", 323 | " if func.__doc__ is not None\n", 324 | " else f\"a {func.__name__} PCell.\",\n", 325 | " \"produce_impl\": produce_impl,\n", 326 | " \"display_text_impl\": display_text_impl,\n", 327 | " },\n", 328 | " )\n", 329 | " pcell = DynamicPCell()\n", 330 | " pcell.__signature__ = Signature(list(params.values()), return_annotation=pya.Cell)\n", 331 | " pcell.name = func.__name__\n", 332 | " return pcell" 333 | ] 334 | } 335 | ], 336 | "metadata": { 337 | "kernelspec": { 338 | "display_name": "fl", 339 | "language": "python", 340 | "name": "fl" 341 | } 342 | }, 343 | "nbformat": 4, 344 | "nbformat_minor": 5 345 | } 346 | -------------------------------------------------------------------------------- /flayout/bokeh.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: source/91_bokeh.ipynb (unless otherwise specified). 2 | 3 | 4 | from __future__ import annotations 5 | 6 | 7 | __all__ = ['ALPHA', 'RED', 'GREEN', 'BLUE', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'get_lyp_path', 8 | 'LayerProperty', 'LayerProperties', 'read_lyp', 'get_lyp', 'new_plot', 'adjust_plot', 'draw_polys', 9 | 'draw_poly', 'draw_path', 'draw_point', 'draw_vector', 'draw_box', 'draw_cell', 'draw_inst', 'draw_layout'] 10 | 11 | # Internal Cell 12 | #nbdev_comment from __future__ import annotations 13 | 14 | import glob 15 | import os 16 | from functools import lru_cache 17 | from typing import Dict, List, Optional, Tuple, Union 18 | 19 | import bokeh.io as bio 20 | import bokeh.models as bm 21 | import bokeh.plotting as bp 22 | import numpy as np 23 | import pkg_resources 24 | import pya 25 | from lxml import etree 26 | 27 | # Cell 28 | ALPHA = 0.3 29 | RED = "#FF0000" 30 | GREEN = "#00FF00" 31 | BLUE = "#0000FF" 32 | C0 = '#1f77b4' 33 | C1 = '#ff7f0e' 34 | C2 = '#2ca02c' 35 | C3 = '#d62728' 36 | C4 = '#9467bd' 37 | C5 = '#8c564b' 38 | C6 = '#e377c2' 39 | C7 = '#7f7f7f' 40 | C8 = '#bcbd22' 41 | C9 = '#17becf' 42 | 43 | # Cell 44 | LayerProperty = Dict[str, Union[str, bool]] 45 | LayerProperties = Dict[Tuple[int, int], LayerProperty] 46 | 47 | def get_lyp_path(path: Optional[str] = None): 48 | # first, let's try "~/.klayout" 49 | if path is None: 50 | path = "" 51 | path = os.path.abspath(os.path.expanduser(path)) 52 | if os.path.isdir(path): 53 | possible_paths = glob.glob(f"{path}/*.lyp") 54 | if not possible_paths: 55 | path = get_lyp_path(pkg_resources.resource_filename("flayout", "layers.lyp")) 56 | else: 57 | path = possible_paths[0] 58 | return path 59 | 60 | # Cell 61 | @lru_cache 62 | def read_lyp(path: Optional[str] = None) -> LayerProperties: 63 | """Load layer properties from a file 64 | 65 | Args: 66 | path: the path where to load the layer properties from 67 | 68 | Returns: 69 | a dictionary of layer property dictionaries 70 | """ 71 | path = get_lyp_path(path) 72 | xml = etree.parse(path) 73 | root = xml.getroot() 74 | parsed: LayerProperties = { 75 | (0, 0): { 76 | "name": "", 77 | "frame-color": "#000000", 78 | "fill-color": "#000000", 79 | "visible": True, 80 | } 81 | } 82 | for properties in root.iter("properties"): 83 | name = properties.find("name") 84 | if name is not None: 85 | name, *_ = name.text.split("(") 86 | name = name.strip() 87 | else: 88 | name = "" 89 | 90 | layerstr = properties.find("source") 91 | if layerstr is not None: 92 | layerstr, *_ = layerstr.text.split("@") 93 | lr, dt = layerstr.strip().split("/") 94 | lr, dt = int(lr), int(dt) 95 | parsed[lr, dt] = { 96 | "name": name, 97 | "frame-color": properties.find("frame-color").text, 98 | "fill-color": properties.find("fill-color").text, 99 | "visible": bool(properties.find("visible").text), 100 | } 101 | 102 | return parsed 103 | 104 | # Cell 105 | @lru_cache 106 | def get_lyp( 107 | layer_info: Union[pya.LayerInfo, Tuple[int, int]], path: Optional[str] = None 108 | ) -> LayerProperty: 109 | """Load layer properties for a specific layer from a file 110 | 111 | Args: 112 | layer_info: the layer info tuple to load the layer properties for 113 | path: the path where to load the layer properties from 114 | 115 | Returns: 116 | a layer property dictionary 117 | """ 118 | if isinstance(layer_info, pya.LayerInfo): 119 | layer, datatype = layer_info.layer, layer_info.datatype # type: ignore 120 | else: 121 | layer, datatype, *_ = layer_info 122 | lyps = read_lyp(path=path) 123 | lyp = lyps.get((layer, datatype), lyps[0, 0]) 124 | return lyp 125 | 126 | # Internal Cell 127 | def _get_range(box: Union[pya.Box, pya.DBox]) -> Tuple[float, float, float, float, float, float]: 128 | """Get the plotting bbox for a klayout box 129 | 130 | Args: 131 | poly: the polygon to create the bbox for 132 | 133 | returns: 134 | x1, x2, y1, y2, w, h 135 | """ 136 | if isinstance(box, pya.Box): 137 | box = box.to_dtype() 138 | x1, x2 = min(box.p1.x, box.p2.x), max(box.p1.x, box.p2.x) 139 | y1, y2 = min(box.p1.y, box.p2.y), max(box.p1.y, box.p2.y) 140 | w, h = x2 - x1, y2 - y1 141 | if w > 5 * h: 142 | y, h = 0.5 * (y1 + y2), w / 5 143 | y1, y2 = y - h / 2, y + h / 2 144 | if h > 5 * w: 145 | x, w = 0.5 * (x1 + x2), h / 5 146 | x1, x2 = x - w / 2, x + w / 2 147 | if w < 2.0: 148 | x1, x2 = (x2 + x1) / 2 - 1.0, (x2 + x1) / 2 + 1.0 149 | w = 2.0 150 | if h < 2.0: 151 | y1, y2 = (y2 + y1) / 2 - 1.0, (y2 + y1) / 2 + 1.0 152 | h = 2.0 153 | return x1, x2, y1, y2, w, h 154 | 155 | # Cell 156 | def new_plot(box: Union[pya.Box, pya.DBox], max_dim: Optional[float] = None) -> bp.Figure: 157 | """Create a new plot with limits determined by a bbox 158 | 159 | Args: 160 | box: the bbox of the polygon or cell to create the figure for 161 | 162 | Returns: 163 | a bokeh Figure. 164 | """ 165 | if max_dim is None: 166 | max_dim = 500 167 | x0, x1, y0, y1, w, h = _get_range(box) 168 | if w > h: 169 | plot_width = max_dim 170 | plot_height = max_dim * h / w 171 | else: 172 | plot_height = max_dim 173 | plot_width = max_dim * w / h 174 | plot = bp.figure( 175 | plot_width=round(plot_width), 176 | plot_height=round(plot_height), 177 | x_range=(x0, x1), 178 | y_range=(y0, y1), 179 | match_aspect=True, 180 | toolbar_location=None, 181 | tools=[bm.PanTool(), zoom := bm.WheelZoomTool()], 182 | ) 183 | plot.toolbar.active_scroll = zoom # type: ignore 184 | return plot 185 | 186 | # Cell 187 | def adjust_plot(plot: bp.Figure, box: Union[pya.Box, pya.DBox], max_dim: Optional[float] = None) -> bp.Figure: 188 | """Adjust a plot with limits determined by a bbox 189 | 190 | Args: 191 | plot: the plot to adjust the limits for 192 | box: the bbox of the polygon or cell to create the figure for 193 | 194 | Returns: 195 | a bokeh Figure. 196 | """ 197 | if plot is None: 198 | return new_plot(box, max_dim=max_dim) 199 | if max_dim is None: 200 | max_dim = max(plot.height, plot.width) 201 | assert max_dim is not None 202 | x0, x1, y0, y1, w, h = _get_range(box) 203 | if w > h: 204 | plot_width = max_dim 205 | plot_height = max_dim * h / w 206 | else: 207 | plot_height = max_dim 208 | plot_width = max_dim * w / h 209 | plot.plot_width = round(plot_width) 210 | plot.plot_height = round(plot_height) 211 | plot.x_range = bm.Range1d(start=x0, end=x1) 212 | plot.y_range = bm.Range1d(start=y0, end=y1) 213 | return plot 214 | 215 | # Cell 216 | def draw_polys( 217 | plot: bp.Figure, 218 | polys: List[Union[pya.Polygon, pya.SimplePolygon, pya.DPolygon, pya.DSimplePolygon]], 219 | layer=(0, 0), 220 | fill_color=None, 221 | line_color=None, 222 | fill_alpha=ALPHA, 223 | ): 224 | """draw polygons with bokeh 225 | 226 | Args: 227 | plot: the plot to draw the polygon in 228 | polys: the polygons to draw 229 | 230 | Returns: 231 | the (inplace) modified plot containing the polygons 232 | """ 233 | # some typing definitions for our own sanity... 234 | 235 | # Array of single coordinates (x OR y): ndim==1, shape=(N,). 236 | SimplePolygon = np.ndarray 237 | 238 | # List of coordinate arrays (first element: hull, other elements: holes) 239 | Polygon = List[SimplePolygon] 240 | 241 | # List of individual polygons (bokeh will XOR those polygons -> usually 1 Polygon per MultiPolygon) 242 | MultiPolygon = List[Polygon] 243 | 244 | # List of multi polygons 245 | MultiPolygons = List[MultiPolygon] 246 | 247 | xs: MultiPolygons = [] 248 | ys: MultiPolygons = [] 249 | 250 | for poly in polys: 251 | if isinstance(poly, (pya.Polygon, pya.SimplePolygon)): 252 | poly = poly.to_dtype() 253 | 254 | if isinstance(poly, pya.DPolygon): 255 | hull = np.asarray([(p.x, p.y) for p in poly.each_point_hull()]) 256 | holes = [ 257 | np.asarray([(p.x, p.y) for p in poly.each_point_hole(i)]) 258 | for i in range(poly.holes()) 259 | ] 260 | elif isinstance(poly, pya.DSimplePolygon): 261 | hull = np.asarray([(p.x, p.y) for p in poly.each_point()]) 262 | holes = [] 263 | else: 264 | raise ValueError( 265 | f"Invalid polygon type. Got: {type(poly)}. " 266 | f"Expected 'DPolygon' or 'DSimplePolygon'" 267 | ) 268 | 269 | if hull.shape[0] < 3: 270 | continue 271 | 272 | plot = adjust_plot( 273 | plot, pya.Polygon([pya.DPoint(x, y) for x, y in hull]).bbox() 274 | ) 275 | 276 | xs_: MultiPolygon = [[hull[:, 0], *(h[:, 0] for h in holes)]] 277 | ys_: MultiPolygon = [[hull[:, 1], *(h[:, 1] for h in holes)]] 278 | xs.append(xs_) 279 | ys.append(ys_) 280 | 281 | source = bm.ColumnDataSource({"xs": xs, "ys": ys}) 282 | lyp = get_lyp(layer) 283 | patch = bm.MultiPolygons( 284 | xs="xs", 285 | ys="ys", 286 | fill_color=fill_color or lyp["fill-color"], 287 | fill_alpha=fill_alpha, 288 | line_color=line_color or lyp["frame-color"], 289 | ) 290 | plot.add_glyph(source, patch) 291 | return plot 292 | 293 | # Cell 294 | def draw_poly( 295 | plot: bp.Figure, 296 | poly: Union[pya.Polygon, pya.SimplePolygon, pya.DPolygon, pya.DSimplePolygon], 297 | layer=(0, 0), 298 | fill_color=None, 299 | line_color=None, 300 | fill_alpha=ALPHA, 301 | ): 302 | """draw a polygon with bokeh 303 | 304 | Args: 305 | plot: the plot to draw the polygon in 306 | poly: the polygon to draw 307 | 308 | Returns: 309 | the (inplace) modified plot containing the polygon 310 | """ 311 | plot = adjust_plot(plot, poly.bbox()) 312 | return draw_polys( 313 | plot, 314 | [poly], 315 | layer=layer, 316 | fill_color=fill_color, 317 | line_color=line_color, 318 | fill_alpha=fill_alpha, 319 | ) 320 | 321 | # Cell 322 | def draw_path( 323 | plot: bp.Figure, 324 | path: Union[pya.Path, pya.DPath], 325 | layer=(0, 0), 326 | fill_color=None, 327 | line_color=None, 328 | fill_alpha=ALPHA, 329 | ): 330 | """draw a path with bokeh 331 | 332 | Args: 333 | plot: the plot to draw the path in 334 | poly: the path to draw 335 | 336 | Returns: 337 | the (inplace) modified plot containing the path 338 | """ 339 | return draw_polys( 340 | plot, 341 | [path.polygon()], 342 | layer=layer, 343 | fill_color=fill_color, 344 | line_color=line_color, 345 | fill_alpha=fill_alpha, 346 | ) 347 | 348 | # Cell 349 | def draw_point( 350 | plot: bp.Figure, 351 | p: Union[pya.Point, pya.DPoint], 352 | layer=(0, 0), 353 | fill_color=None, 354 | line_color=None, 355 | fill_alpha=ALPHA, 356 | ): 357 | """draw a point with bokeh 358 | 359 | Args: 360 | plot: the plot to draw the point in 361 | p: the point to draw 362 | 363 | Returns: 364 | the (inplace) modified plot containing the point 365 | """ 366 | if isinstance(p, pya.Point): 367 | p = p.to_dtype() 368 | v = pya.DVector(1.0, 1.0) 369 | box = pya.DBox(p - v, p + v) 370 | plot = adjust_plot(plot, box) 371 | *_, w, h = _get_range(box) 372 | radius = max(w, h) / 30 373 | lyp = get_lyp(layer) 374 | plot.circle( 375 | p.x, 376 | p.y, 377 | fill_alpha=fill_alpha, 378 | fill_color=fill_color or lyp["fill-color"], 379 | line_color=line_color or ["frame-color"], 380 | radius=radius, 381 | ) 382 | return plot 383 | 384 | # Cell 385 | def draw_vector( 386 | plot: bp.Figure, 387 | v: Union[pya.Vector, pya.DVector], 388 | layer=(0, 0), 389 | fill_color=None, 390 | line_color=None, 391 | fill_alpha=ALPHA, 392 | ): 393 | """draw a vector as an arrow with bokeh 394 | 395 | Args: 396 | plot: the plot to draw the vector in 397 | v: the vector to draw 398 | 399 | Returns: 400 | the (inplace) modified plot containing the vector 401 | """ 402 | if isinstance(v, pya.Vector): 403 | v = pya.DVector(v.x / 1000.0, v.y / 1000.0) 404 | box = pya.DBox(0, 0, v.x, v.y) 405 | plot = adjust_plot(plot, box, max_dim=250) 406 | lyp = get_lyp(layer) 407 | arrow_head = bm.VeeHead( 408 | fill_alpha=fill_alpha, 409 | fill_color=fill_color or lyp["fill-color"], 410 | line_color=line_color or lyp["frame-color"], 411 | ) 412 | arrow = bm.Arrow( 413 | end=arrow_head, 414 | x_start=0, 415 | y_start=0, 416 | x_end=v.x, 417 | y_end=v.y, 418 | line_color=line_color or lyp["frame-color"], 419 | ) 420 | plot.add_layout(arrow) 421 | return plot 422 | 423 | # Cell 424 | def _box_to_poly(box: Union[pya.Box, pya.DBox]): 425 | """convert a box into a polygon 426 | 427 | Args: 428 | box: the box to convert into a polygon 429 | 430 | Returns: 431 | the polygon 432 | """ 433 | if isinstance(box, pya.Box): 434 | box = box.to_dtype() 435 | x0, y0 = box.p1.x, box.p1.y 436 | x1, y1 = box.p2.x, box.p2.y 437 | return pya.DPolygon( 438 | [pya.DPoint(x0, y0), pya.DPoint(x1, y0), pya.DPoint(x1, y1), pya.DPoint(x0, y1)] 439 | ) 440 | 441 | 442 | def draw_box( 443 | plot: bp.Figure, 444 | box: Union[pya.Box, pya.DBox], 445 | fill_alpha=0.0, 446 | fill_color="#000000", 447 | line_color=GREEN, 448 | ): 449 | """draw a box with bokeh 450 | 451 | Args: 452 | plot: the plot to draw the box in 453 | box: the box to draw 454 | 455 | Returns: 456 | the (inplace) modified plot containing the box 457 | """ 458 | plot = adjust_plot(plot, box) 459 | poly = _box_to_poly(box) 460 | return draw_poly( 461 | plot, poly, fill_alpha=fill_alpha, fill_color=fill_color, line_color=line_color 462 | ) 463 | 464 | # Cell 465 | def _draw_shapes(plot, shapes, layer=(0, 0)): 466 | """draw shapes with bokeh 467 | 468 | Args: 469 | plot: the plot to draw the shape in 470 | shapes: the shapes to draw 471 | 472 | Returns: 473 | the (inplace) modified plot containing the shape 474 | """ 475 | polys = [] 476 | for shape in shapes: 477 | if shape.is_box(): 478 | polys.append(_box_to_poly(shape.dbbox())) 479 | elif shape.is_path(): 480 | polys.append(shape.dpath.polygon()) 481 | elif shape.is_polygon(): 482 | polys.append(shape.dpolygon) 483 | elif shape.is_simple_polygon(): 484 | polys.append(shape.dsimple_polygon) 485 | #for poly in polys: 486 | # plot = draw_poly(plot, poly, layer=layer) 487 | return draw_polys(plot, polys, layer=layer) 488 | 489 | def draw_cell(plot, cell, draw_bbox=True): 490 | """draw a cell with bokeh 491 | 492 | Args: 493 | plot: the plot to draw the cell in 494 | cell: the cell to draw 495 | 496 | Returns: 497 | the (inplace) modified plot containing the cell 498 | """ 499 | layout = pya.Layout() 500 | new_cell = layout.create_cell(cell.name) 501 | new_cell.copy_tree(cell) 502 | 503 | cell = new_cell 504 | cell = cell.flatten(-1, True) 505 | box = cell.dbbox() 506 | plot = adjust_plot(plot, box, max_dim=500) 507 | for lr in layout.layer_infos(): 508 | shapes = [*cell.shapes(layout.layer(lr)).each()] 509 | plot = _draw_shapes(plot, shapes, layer=(lr.layer, lr.datatype)) 510 | if draw_bbox: 511 | draw_box(plot, box) 512 | return plot 513 | 514 | # Cell 515 | def draw_inst(plot, inst, draw_bbox=True, draw_arrow=True): 516 | """draw a instance with bokeh 517 | 518 | Args: 519 | plot: the plot to draw the instance in 520 | inst: the instance to draw 521 | 522 | Returns: 523 | the (inplace) modified plot with containing the instance 524 | """ 525 | _layout = pya.Layout() 526 | _cell = _layout.create_cell(inst.cell.name) 527 | _cell.copy_tree(inst.cell) 528 | _refcell = _layout.create_cell(f"ref_{inst.cell.name}") 529 | _refcell.insert(pya.CellInstArray(_layout.cell_by_name(inst.cell.name), inst.trans)) 530 | 531 | plot = new_plot(_refcell.dbbox() + pya.Point(0, 0)) 532 | plot = draw_vector(plot, inst.trans.disp) 533 | plot = draw_cell(plot, _refcell) 534 | plot = adjust_plot(plot, _refcell.dbbox() + pya.Point(0, 0)) 535 | return plot 536 | 537 | # Cell 538 | def draw_layout(plot, layout): 539 | """draw a layout with bokeh 540 | 541 | Args: 542 | plot: the plot to draw the layout in 543 | layout: the layout to draw 544 | 545 | Returns: 546 | the (inplace) modified plot with containing the layout 547 | """ 548 | plots = bp.Column(*[draw_cell(plot, cell) for cell in layout.top_cells()]) 549 | return plots -------------------------------------------------------------------------------- /source/01_factories.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "893fd908-aa0b-4daa-ac08-16c7782101f6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp factories" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "d816dbc2-a09f-4e7a-b173-680c9fb05d38", 16 | "metadata": {}, 17 | "source": [ 18 | "# FLayout Factories\n", 19 | "> FLayout core factories yielding native klayout datastructures." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "582867a3-6517-48a2-9557-7e9591a9988c", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "from typing import Dict, List, Optional, Tuple, Union, overload\n", 31 | "\n", 32 | "import pya\n", 33 | "from flayout.cell import (\n", 34 | " LibReference,\n", 35 | " PCellLibReference,\n", 36 | " PCellReference,\n", 37 | " Reference,\n", 38 | " _add_cell_to_layout,\n", 39 | " _add_lib_cell_to_layout,\n", 40 | " _add_lib_pcell_to_layout,\n", 41 | " add_cells_to_layout,\n", 42 | " add_pcells_to_layout,\n", 43 | " reference,\n", 44 | ")" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "id": "8216abd4-8a99-4f14-a11e-d2adfc0235c0", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "# hide\n", 55 | "import flayout.notebook" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "5ad0137d-fa1d-4c1d-b63c-62a22fbadce8", 61 | "metadata": {}, 62 | "source": [ 63 | "#### Shape" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "3b5f97a2-db22-42dc-8958-643eb256bf2d", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "# exports" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "id": "5bedd58b-748a-42c7-a38b-57fe4037ffc9", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "# export\n", 84 | "def layer(lr: int, dt: int, name: str = \"\") -> pya.LayerInfo:\n", 85 | " layer = pya.LayerInfo(int(lr), int(dt))\n", 86 | " if name:\n", 87 | " layer.name = name\n", 88 | " return layer" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "id": "95901d00-caee-4521-a206-158b89996575", 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "layer(19, 0)" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "id": "01e5a82c-23d4-47e4-b911-9209560499af", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "# export\n", 109 | "def point(x: Union[float, int], y: Union[float, int]) -> pya.DPoint:\n", 110 | " \"\"\"create a KLayout DPoint\n", 111 | "\n", 112 | " Args:\n", 113 | " x: the x-value of the point\n", 114 | " y: the y-value of the point\n", 115 | "\n", 116 | " Returns:\n", 117 | " the point\n", 118 | " \"\"\"\n", 119 | " return pya.DPoint(float(x), float(y))" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "id": "29c197ac-e305-4771-ba7b-2981d5eebcac", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "my_point = point(3, 5)\n", 130 | "my_point" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "da742702-0d6d-4653-9ec6-af873a17a8e9", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "# export\n", 141 | "def vector(x: Union[float, int], y: Union[float, int]) -> pya.DVector:\n", 142 | " \"\"\"create a KLayout DVector\n", 143 | "\n", 144 | " Args:\n", 145 | " x: the x-value of the vector\n", 146 | " y: the y-value of the vector\n", 147 | "\n", 148 | " Returns:\n", 149 | " the vector\n", 150 | " \"\"\"\n", 151 | " return pya.DVector(float(x), float(y))" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "f6b92404-e19b-4331-88c2-02e27ed17008", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "my_vector = vector(3, 5)\n", 162 | "my_vector" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "c30dea4b-0dfd-4226-b7aa-6669a8c6c43e", 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "# export\n", 173 | "\n", 174 | "def _validate_point(p):\n", 175 | " if isinstance(p, pya.Point):\n", 176 | " p = p.to_dtype()\n", 177 | " if not isinstance(p, pya.DPoint):\n", 178 | " p = pya.DPoint(*(float(x) for x in p))\n", 179 | " return p\n", 180 | "\n", 181 | "def box(p1: Union[pya.Point, pya.DPoint], p2: Union[pya.Point, pya.DPoint]) -> pya.DBox:\n", 182 | " p1 = _validate_point(p1)\n", 183 | " p2 = _validate_point(p2)\n", 184 | " return pya.DBox(p1, p2)" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "id": "321746cc-5c32-48f1-97b5-c250fcd2df7a", 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "my_box = box((0, 0), (5, 10))\n", 195 | "my_box" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "id": "1214fc0f-6d0f-4d60-b8ac-e5f1abf3e0d1", 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "# export\n", 206 | "\n", 207 | "Point = Union[pya.DPoint, Tuple[int, int], Tuple[float, float], 'np.ndarray']\n", 208 | "Points = Union[List[Point], 'np.ndarray']\n", 209 | "\n", 210 | "@overload\n", 211 | "def polygon(\n", 212 | " hull: Points,\n", 213 | " *,\n", 214 | " holes: None = None,\n", 215 | " raw: bool = False,\n", 216 | ") -> pya.DSimplePolygon:\n", 217 | " ...\n", 218 | "\n", 219 | "\n", 220 | "@overload\n", 221 | "def polygon(\n", 222 | " hull: Points,\n", 223 | " *,\n", 224 | " holes: List[Points] = None,\n", 225 | " raw: bool = False,\n", 226 | ") -> pya.DPolygon:\n", 227 | " ...\n", 228 | "\n", 229 | "\n", 230 | "def polygon(\n", 231 | " hull: Points,\n", 232 | " *,\n", 233 | " holes: Optional[List[Points]] = None,\n", 234 | " raw: bool = False,\n", 235 | ") -> Union[pya.DSimplePolygon, pya.DPolygon]:\n", 236 | " \"\"\"create a KLayout polygon\n", 237 | "\n", 238 | " Args:\n", 239 | " hull: the points that make the polygon hull\n", 240 | " holes: the collection of points that make the polygon holes\n", 241 | " raw: if true, the points won't be compressed.\n", 242 | "\n", 243 | " Returns:\n", 244 | " the polygon\n", 245 | " \"\"\"\n", 246 | " hull = [(p if isinstance(p, (pya.Point, pya.DPoint)) else point(*p)) for p in hull]\n", 247 | " if holes is None:\n", 248 | " poly = pya.DSimplePolygon(hull, raw)\n", 249 | " else:\n", 250 | " poly = pya.DPolygon(hull, raw)\n", 251 | " for hole in holes:\n", 252 | " hole = [(p if isinstance(p, pya.DPoint) else point(*p)) for p in hole]\n", 253 | " poly.insert_hole(hole)\n", 254 | " return poly" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "id": "83f841c1-74c1-4562-806a-cf9fdad66d1a", 260 | "metadata": {}, 261 | "source": [ 262 | "A simple polygon is a polygon without holes. You can create this by using `fl.polygon` and only specifying a hull." 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": null, 268 | "id": "28f151ec-2636-4810-9d3c-b648a5fda60f", 269 | "metadata": {}, 270 | "outputs": [], 271 | "source": [ 272 | "my_simple_poly = polygon(\n", 273 | " hull=[(0, -2), (5, -2), (1, 2)],\n", 274 | ")\n", 275 | "my_simple_poly" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "id": "49a271df-65a2-4e1a-be39-17a14549ce20", 281 | "metadata": {}, 282 | "source": [ 283 | "General Polygons (with holes) can be specified by also specifying a `holes` keyword in `fl.polygon`:" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": null, 289 | "id": "788a1bfb-99ad-4a09-afaf-624a761e7010", 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "my_poly = polygon(\n", 294 | " hull=[(0, 0), (6, 0), (6, 4), (0, 3)],\n", 295 | " holes=[[(3, 1), (4, 1), (4, 2), (3, 2)]]\n", 296 | ")\n", 297 | "my_poly" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": null, 303 | "id": "06b9af6d-d592-455c-8ee4-a750038e137d", 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "# export\n", 308 | "def path(\n", 309 | " pts: Points,\n", 310 | " width: float,\n", 311 | " *,\n", 312 | " bgn_ext: float = 0.0,\n", 313 | " end_ext: float = 0.0,\n", 314 | " round: bool = False,\n", 315 | ") -> pya.DPath:\n", 316 | " \"\"\"create a KLayout path\n", 317 | "\n", 318 | " Args:\n", 319 | " pts: the points that make the path\n", 320 | " width: the width of the path\n", 321 | " bgn_ext: the begin extension of the path\n", 322 | " end_ext: the end extension of the path\n", 323 | " round: round the ends of the path\n", 324 | "\n", 325 | " Returns:\n", 326 | " the path\n", 327 | " \"\"\"\n", 328 | " pts = [(p if isinstance(p, (pya.Point, pya.DPoint)) else point(*p)) for p in pts]\n", 329 | " print(pts)\n", 330 | " return pya.DPath(pts, width, bgn_ext, end_ext, round)" 331 | ] 332 | }, 333 | { 334 | "cell_type": "code", 335 | "execution_count": null, 336 | "id": "7db9a373-c41c-4c9c-be7e-b50e8321175b", 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "my_path = path([(3.0, 4.0), (6.0, -1.0)], 1.2)\n", 341 | "my_path" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": null, 347 | "id": "fa0d71f5-bd2b-4fc4-9a12-caeabc0f4fa9", 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "# export\n", 352 | "Shape = Union[pya.DBox, pya.DSimplePolygon, pya.DPolygon, pya.DPath, pya.Box, pya.SimplePolygon, pya.Polygon, pya.Path]\n", 353 | "def cell(\n", 354 | " name: str,\n", 355 | " *,\n", 356 | " shapes: Optional[Dict[pya.LayerInfo, Shape]] = None,\n", 357 | " child_cells: Optional[List[Union[pya.Cell, Tuple[pya.Cell, pya.CplxTrans]]]] = None,\n", 358 | ") -> pya.Cell:\n", 359 | " \"\"\"create a KLayout cell\n", 360 | "\n", 361 | " Args:\n", 362 | " name: the name of the cell\n", 363 | " shapes: the shapes to add to the cell\n", 364 | " child_cells: the child cells to add to the cell\n", 365 | "\n", 366 | " Returns:\n", 367 | " the cell\n", 368 | " \"\"\"\n", 369 | " layout = pya.Layout()\n", 370 | " cell = layout.create_cell(name)\n", 371 | " cell._layout = layout # make sure layout does not get destroyed\n", 372 | " if shapes:\n", 373 | " for lr, lrshapes in shapes.items():\n", 374 | " if not isinstance(lr, pya.LayerInfo):\n", 375 | " lr = layer(*lr)\n", 376 | " lr = layout.layer(lr)\n", 377 | " for i, shape in enumerate(lrshapes):\n", 378 | " \n", 379 | " # if isinstance(shape, np.ndarray):\n", 380 | " if type(shape).__name__ == 'ndarray': # yeah... I don't want to import numpy...\n", 381 | " lrshapes[i] = polygon(shape)\n", 382 | " \n", 383 | " for i, shape in enumerate(lrshapes): # TODO: insert all at once?\n", 384 | " cell.shapes(lr).insert(shape)\n", 385 | " if child_cells:\n", 386 | " cell_idxs = {}\n", 387 | " for ref in child_cells:\n", 388 | " if isinstance(ref, (str, pya.Cell, pya.PCellDeclaration)):\n", 389 | " ref = reference(ref)\n", 390 | " else:\n", 391 | " ref = reference(*ref)\n", 392 | " if isinstance(ref, Reference):\n", 393 | " if ref.cell not in cell_idxs:\n", 394 | " cell_idxs[ref.cell] = _add_cell_to_layout(layout, ref.cell)\n", 395 | " cell.insert(pya.CellInstArray(cell_idxs[ref.cell], ref.trans))\n", 396 | " elif isinstance(ref, LibReference):\n", 397 | " # don't store index in cell_idxs... Cell belongs to library and will not be copied.\n", 398 | " idx = _add_lib_cell_to_layout(layout, ref.lib, ref.cell)\n", 399 | " cell.insert(pya.CellInstArray(idx, ref.trans))\n", 400 | " elif isinstance(ref, PCellReference):\n", 401 | " raise ValueError(\n", 402 | " f\"One can only add pcells belonging to a library to to a new cell. \"\n", 403 | " f\"Add a pcell reference using the following string format to \"\n", 404 | " f\"represent the cell: '.'.\"\n", 405 | " )\n", 406 | " elif isinstance(ref, PCellLibReference):\n", 407 | " # don't store index in cell_idxs... PCell belongs to library and will not be copied.\n", 408 | " idx = _add_lib_pcell_to_layout(layout, ref.lib, ref.cell, ref.params)\n", 409 | " cell.insert(pya.CellInstArray(idx, ref.trans))\n", 410 | "\n", 411 | " return cell" 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": null, 417 | "id": "82a1aa01-bdf6-4441-ba2b-7eb3173c5501", 418 | "metadata": {}, 419 | "outputs": [], 420 | "source": [ 421 | "my_cell = cell(\n", 422 | " name=\"cell0\", \n", 423 | " shapes={\n", 424 | " (10, 0): [my_simple_poly, my_poly],\n", 425 | " (19, 0): [my_path],\n", 426 | " },\n", 427 | ")\n", 428 | "my_cell" 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "id": "6c2c7979-4a0b-4414-b805-7375149a2cf4", 434 | "metadata": {}, 435 | "source": [ 436 | "Child cells can also be added to a cell:" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": null, 442 | "id": "161fb58d-5112-44ff-938c-1a2653c15ca9", 443 | "metadata": {}, 444 | "outputs": [], 445 | "source": [ 446 | "my_other_poly = polygon([(10, 0), (11, 0), (11, 0), (10, 5)])\n", 447 | "my_other_cell = cell(\n", 448 | " name=\"cell1\", \n", 449 | " shapes={(0, 0): [my_other_poly]},\n", 450 | " child_cells=[my_cell],\n", 451 | ")\n", 452 | "my_other_cell" 453 | ] 454 | }, 455 | { 456 | "cell_type": "code", 457 | "execution_count": null, 458 | "id": "690a1844-22cb-4f66-873d-5172063ff224", 459 | "metadata": {}, 460 | "outputs": [], 461 | "source": [ 462 | "# export\n", 463 | "def transform(\n", 464 | " mag: float = 1.0,\n", 465 | " rot: float = 0.0,\n", 466 | " mirrx: bool = False,\n", 467 | " x: float = 0.0,\n", 468 | " y: float = 0.0,\n", 469 | ") -> pya.CplxTrans:\n", 470 | " \"\"\"create a KLayout Transformation\n", 471 | "\n", 472 | " Args:\n", 473 | " mag: the magnitude of the transformation\n", 474 | " rot: the rotation of the transformation\n", 475 | " mirrx: mirror over the x-axis (x=0 line).\n", 476 | " x: translation distance in the x-direction\n", 477 | " y: translation distance in the y-direction\n", 478 | "\n", 479 | " Returns:\n", 480 | " the cell reference\n", 481 | " \"\"\"\n", 482 | " return pya.CplxTrans(mag, rot, mirrx, int(1000 * x), int(1000 * y))" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "id": "a3ac6d8d-b744-424a-a51c-aaea60f888c9", 488 | "metadata": {}, 489 | "source": [ 490 | "One can apply transformations to the child cell as well:" 491 | ] 492 | }, 493 | { 494 | "cell_type": "code", 495 | "execution_count": null, 496 | "id": "d0402f6e-84cc-4927-b7c9-c14de6a39971", 497 | "metadata": {}, 498 | "outputs": [], 499 | "source": [ 500 | "my_third_poly = polygon([(0, 0), (6, 0), (6, 4), (0, 5)])\n", 501 | "my_third_cell = cell(\n", 502 | " name=\"cell2\", \n", 503 | " shapes={(0, 0): [my_third_poly]},\n", 504 | " child_cells=[\n", 505 | " (my_other_cell, transform(mirrx=True, x=12.0, y=3.0)),\n", 506 | " ],\n", 507 | ")\n", 508 | "my_third_cell" 509 | ] 510 | }, 511 | { 512 | "cell_type": "markdown", 513 | "id": "8db53b55-5fda-4265-9e15-7c18ea5e6aed", 514 | "metadata": {}, 515 | "source": [ 516 | "The combination of a child cell and a transformation is called an `instance` of a cell (sometimes also called a reference):" 517 | ] 518 | }, 519 | { 520 | "cell_type": "code", 521 | "execution_count": null, 522 | "id": "58c43740-7686-45b4-ac4d-d15d23d9de11", 523 | "metadata": {}, 524 | "outputs": [], 525 | "source": [ 526 | "inst = next(my_third_cell.each_inst()) # get the only instance\n", 527 | "inst" 528 | ] 529 | }, 530 | { 531 | "cell_type": "code", 532 | "execution_count": null, 533 | "id": "957503e0-ce66-4b7b-86a9-07ae6b44d4c7", 534 | "metadata": {}, 535 | "outputs": [], 536 | "source": [ 537 | "# export\n", 538 | "def layout(*, cells: Optional[List[pya.Cell]] = None) -> pya.Layout:\n", 539 | " \"\"\"create a KLayout Layout\n", 540 | "\n", 541 | " Args:\n", 542 | " cells: the cells to add to the layout\n", 543 | "\n", 544 | " Returns:\n", 545 | " the KLayout layout\n", 546 | " \"\"\"\n", 547 | " layout = pya.Layout()\n", 548 | " layout.dbu = 0.001 # hard coded for now\n", 549 | " if cells:\n", 550 | " add_cells_to_layout(layout, cells)\n", 551 | " return layout" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "id": "818065f9-6f08-42f8-a30f-63ba38a66543", 557 | "metadata": {}, 558 | "source": [ 559 | "Finally, a layout is a collection of multiple cells. If a cell is not a sub cell of another cell, it will be considered a top-level cell. Moreover, one cannot have cyclic references within a layout." 560 | ] 561 | }, 562 | { 563 | "cell_type": "markdown", 564 | "id": "231cf13d-2ff6-4db0-8724-3d82ac305fd9", 565 | "metadata": {}, 566 | "source": [ 567 | "Let's create a layout with two top-level cells:" 568 | ] 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": null, 573 | "id": "bcf1a948-5ab5-4005-9c88-0cd218ad8745", 574 | "metadata": {}, 575 | "outputs": [], 576 | "source": [ 577 | "my_independent_cell = cell(\n", 578 | " name='independent_cell',\n", 579 | " shapes={(19, 0): [polygon([(6, 0), (10, -2), (10, 0)])]}\n", 580 | ")\n", 581 | " \n", 582 | "# even though we specify four cells for our layout, only two of them are independent\n", 583 | "# i.e. some of the cells we are added are actually child cells of other cells\n", 584 | "# top level cells in this case are 'my_independent_cell' and 'my_third_cell'\n", 585 | "my_layout = layout(cells=[my_independent_cell, my_cell, my_other_cell, my_third_cell])\n", 586 | "# my_layout = layout(cells=[my_independent_cell, my_third_cell]) # equivalent\n", 587 | "\n", 588 | "my_layout" 589 | ] 590 | }, 591 | { 592 | "cell_type": "code", 593 | "execution_count": null, 594 | "id": "9ffec107-ac98-4317-aa7f-062289267ab6", 595 | "metadata": {}, 596 | "outputs": [], 597 | "source": [ 598 | "# export\n", 599 | "def library(\n", 600 | " name: str,\n", 601 | " *,\n", 602 | " cells: Optional[List[pya.Cell]] = None,\n", 603 | " pcells: Optional[List[pya.Cell]] = None,\n", 604 | " description: str = \"\",\n", 605 | ") -> pya.Library:\n", 606 | " \"\"\"create a KLayout Library\n", 607 | "\n", 608 | " Args:\n", 609 | " name: the name of the library\n", 610 | " cells: the cells to add to the library\n", 611 | " pcells: the pcells to add to the library\n", 612 | "\n", 613 | " Returns:\n", 614 | " the KLayout library\n", 615 | " \"\"\"\n", 616 | " lib = pya.Library()\n", 617 | " if description:\n", 618 | " lib.description = description\n", 619 | " layout = lib.layout()\n", 620 | " layout.dbu = 0.001 # hard coded for now\n", 621 | " if cells:\n", 622 | " add_cells_to_layout(layout, cells)\n", 623 | " if pcells:\n", 624 | " add_pcells_to_layout(layout, pcells)\n", 625 | " lib.register(name)\n", 626 | " return lib" 627 | ] 628 | } 629 | ], 630 | "metadata": { 631 | "kernelspec": { 632 | "display_name": "fl", 633 | "language": "python", 634 | "name": "fl" 635 | } 636 | }, 637 | "nbformat": 4, 638 | "nbformat_minor": 5 639 | } 640 | -------------------------------------------------------------------------------- /source/02_cell.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "38ee6b54-23c7-4577-a8ba-67abffa6678e", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp cell" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "d816dbc2-a09f-4e7a-b173-680c9fb05d38", 16 | "metadata": {}, 17 | "source": [ 18 | "# FLayout Cell\n", 19 | "> FLayout cell functionality" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "582867a3-6517-48a2-9557-7e9591a9988c", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "import sys\n", 31 | "from typing import Any, Dict, List, NamedTuple, Union\n", 32 | "\n", 33 | "import flayout as fl\n", 34 | "import pya\n", 35 | "\n", 36 | "\n", 37 | "class DoesNotExist:\n", 38 | " pass\n", 39 | "\n", 40 | "try:\n", 41 | " from gdspy import Cell as GdsPyCell\n", 42 | "except ImportError:\n", 43 | " GdsPyCell = DoesNotExist" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "a4b7930a-be8d-4814-875b-ead652c8b9f8", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# hide\n", 54 | "import flayout as fl\n", 55 | "import bokeh.io as bio\n", 56 | "import flayout.bokeh as flbk\n", 57 | "import flayout.notebook" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "14e1e034-d255-4b09-bf47-01271ad0fe8b", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "# exporti\n", 68 | "COPY_IMPLEMENTATIONS = {}\n", 69 | "MAX_DEPTH = 2 * sys.getrecursionlimit()\n", 70 | "ON_SAME_NAME_DOC = \"\"\"what to do when the layout containing the destination cell\n", 71 | " already contains a cell with the same name. 'skip' (default): don't\n", 72 | " add the new child cell, use the one already present in the layout\n", 73 | " in stead. This will make the instances in the new cell point to the\n", 74 | " cell already in the layout. 'replace': replace the cell\n", 75 | " already present in the layout. This will make the instances in all\n", 76 | " cells alread present in the target cell's layout point to the newly\n", 77 | " added cell. 'add_suffix': add the new cell, but change its name by\n", 78 | " adding a suffix (this is the default behavior of KLayout's native\n", 79 | " cell.copy_tree method.)\"\"\"" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "id": "416e429c-0f4f-4344-b2ef-2a4794a03ada", 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "# exporti\n", 90 | "class Reference(NamedTuple):\n", 91 | " cell: pya.Cell\n", 92 | " trans: pya.CplxTrans\n", 93 | "\n", 94 | "\n", 95 | "class LibReference(NamedTuple):\n", 96 | " lib: pya.Library\n", 97 | " cell: pya.Cell\n", 98 | " trans: pya.CplxTrans\n", 99 | "\n", 100 | "\n", 101 | "class PCellReference(NamedTuple):\n", 102 | " cell: pya.PCellDeclaration\n", 103 | " trans: pya.CplxTrans\n", 104 | " params: Dict\n", 105 | "\n", 106 | "\n", 107 | "class PCellLibReference(NamedTuple):\n", 108 | " lib: pya.Library\n", 109 | " cell: pya.PCellDeclaration\n", 110 | " trans: pya.CplxTrans\n", 111 | " params: Dict" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "addd12cc-ed01-4bbe-9209-c4c60300c12f", 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "# export\n", 122 | "def reference(*args) -> Union[Reference, LibReference, PCellReference, PCellLibReference]:\n", 123 | " \"\"\"create a cell reference\n", 124 | "\n", 125 | " Note:\n", 126 | " this is not a native KLayout datastructure,\n", 127 | " but rather a convenience wrapper to ease the cell constructor API.\n", 128 | "\n", 129 | " Args:\n", 130 | " cell: the cell to create a reference for\n", 131 | " trans: the transformation to apply to the cell upon adding\n", 132 | "\n", 133 | " Returns:\n", 134 | " the cell reference\n", 135 | " \"\"\"\n", 136 | " cell_str = _get_object_from_type(args, str)\n", 137 | " if cell_str is not None:\n", 138 | " num_dots = cell_str.count(\".\")\n", 139 | " if num_dots != 1:\n", 140 | " raise ValueError(\n", 141 | " \"Reference to cell by string name should have format '.'.\"\n", 142 | " )\n", 143 | " lib_name, cell_name = cell_str.split(\".\")\n", 144 | " lib = pya.Library.library_by_name(lib_name)\n", 145 | " if lib is None:\n", 146 | " raise ValueError(\n", 147 | " f\"Cannot use cell {cell_str!r}. Library {lib_name!r} does not exist (or is not registered).\"\n", 148 | " )\n", 149 | "\n", 150 | " # search for pcell in library\n", 151 | " cell = lib.layout().pcell_declaration(cell_name)\n", 152 | "\n", 153 | " # search for normal cell in library\n", 154 | " if cell is None:\n", 155 | " try:\n", 156 | " cell_idx = lib.layout().cell_by_name(cell_name)\n", 157 | " cell = lib.layout().cell(cell_idx)\n", 158 | " except RuntimeError:\n", 159 | " raise ValueError(\n", 160 | " f\"Cannot use cell {cell_str!r}. Cell {cell_name!r} does not exist \"\n", 161 | " f\"in library {lib_name!r}.\"\n", 162 | " )\n", 163 | "\n", 164 | " args = (\n", 165 | " arg\n", 166 | " for arg in args\n", 167 | " if not (str(arg) == cell_str or arg is cell or arg is lib)\n", 168 | " )\n", 169 | " return reference(cell, lib, *args)\n", 170 | "\n", 171 | " lib = _get_object_from_type(args, pya.Library)\n", 172 | " cell = _get_object_from_type(args, (pya.Cell, pya.PCellDeclaration))\n", 173 | " trans = _get_object_from_type(\n", 174 | " args,\n", 175 | " (pya.CplxTrans, pya.ICplxTrans, pya.DCplxTrans),\n", 176 | " default=pya.CplxTrans(1.0, 0.0, False, 0, 0),\n", 177 | " )\n", 178 | "\n", 179 | " if isinstance(cell, pya.Cell):\n", 180 | " if lib is None:\n", 181 | " return Reference(cell, trans)\n", 182 | " else:\n", 183 | " return LibReference(lib, cell, trans)\n", 184 | " elif isinstance(cell, pya.PCellDeclaration):\n", 185 | " params = _get_object_from_type(args, dict, default={})\n", 186 | " if lib is None:\n", 187 | " return PCellReference(cell, trans, params)\n", 188 | " else:\n", 189 | " return PCellLibReference(lib, cell, trans, params)\n", 190 | " else:\n", 191 | " raise ValueError(f\"No cell found in reference tuple: {tuple(args)}.\")\n", 192 | " \n", 193 | "def _get_object_from_type(objs, cls, default=None):\n", 194 | " selected = [obj for obj in objs if isinstance(obj, cls)]\n", 195 | " if len(selected) > 1:\n", 196 | " raise ValueError(\n", 197 | " f\"Only one argument of type {cls.__name__!r} expected. \"\n", 198 | " f\"Got: {', '.join(repr(s) for s in selected)}.\"\n", 199 | " )\n", 200 | " if not selected:\n", 201 | " return default\n", 202 | " return selected[0]" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "id": "4ec35518-ce94-40f0-9010-db9910207e9a", 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "from numpy import linspace, pi, stack, cos, sin\n", 213 | "triangle = fl.cell(\n", 214 | " name='triangle',\n", 215 | " shapes={(19, 0): [fl.polygon([(6, 0), (10, -2), (10, 1)])]}\n", 216 | ")\n", 217 | "\n", 218 | "@fl.pcell\n", 219 | "def circle(\n", 220 | " name=\"circle\",\n", 221 | " radius: float = 0.1,\n", 222 | " layer: pya.LayerInfo = pya.LayerInfo(1, 0),\n", 223 | " num_points=64,\n", 224 | "):\n", 225 | " t = linspace(0, 2 * pi, num_points, endpoint=False)\n", 226 | " xy = stack([radius * cos(t), radius * sin(t)], -1)\n", 227 | " cell = fl.cell(\n", 228 | " name,\n", 229 | " shapes={layer: [xy]},\n", 230 | " )\n", 231 | " return cell\n", 232 | "\n", 233 | "lib = fl.library(\n", 234 | " \"lib\",\n", 235 | " pcells=[circle],\n", 236 | " cells=[triangle],\n", 237 | " description=\"my library\",\n", 238 | ")" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "id": "5dc060db-12cb-4806-8de8-345e6005164a", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "ref = reference(triangle)\n", 249 | "print(f\"{type(ref).__name__}(cell={ref.cell.name}, trans={ref.trans})\")" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "id": "53de01d2-af92-44bb-a211-c68e0fd7e110", 256 | "metadata": {}, 257 | "outputs": [], 258 | "source": [ 259 | "ref = reference(triangle, fl.transform(mag=2.0, rot=90.0, x=3, y=3))\n", 260 | "print(f\"{type(ref).__name__}(cell={ref.cell.name}, trans={ref.trans})\")" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "id": "5381fe91-3a44-4dc3-b0ab-4e3111e78ed6", 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "ref = reference(lib, triangle, fl.transform(mag=2.0, rot=90.0, x=3, y=3))\n", 271 | "print(f\"{type(ref).__name__}(lib={ref.lib.name()}, cell={ref.cell.name}, trans={ref.trans})\")" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "id": "38841188-da75-462b-9578-91e7dfc35d00", 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [ 281 | "ref = reference(lib, triangle, fl.transform(mag=2.0, rot=90.0, x=3, y=3))\n", 282 | "print(f\"{type(ref).__name__}(lib={ref.lib.name()}, cell={ref.cell.name}, trans={ref.trans})\")" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "id": "69b78f35-3c7e-4d8b-aae1-9fd9f083e5eb", 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [ 292 | "ref = reference(circle, fl.transform(mag=2.0, rot=90.0, x=3, y=3))\n", 293 | "print(f\"{type(ref).__name__}(cell={ref.cell.name}, trans={ref.trans})\")" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "id": "598d3122-9aff-445c-b5e0-bbe37bb64aed", 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "ref = reference(lib, circle, fl.transform(mag=2.0, rot=90.0, x=3, y=3))\n", 304 | "print(f\"{type(ref).__name__}(lib={ref.lib.name()}, cell={ref.cell.name}, trans={ref.trans})\")" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "id": "98cc610a-6649-4b7b-adb4-848548c228e1", 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "# exporti\n", 315 | "def _copy_klayout(source, dest, on_same_name, depth=0):\n", 316 | " if depth > MAX_DEPTH:\n", 317 | " return # basically just to make type checker happy\n", 318 | "\n", 319 | " # if on_same_name == \"add_suffix\":\n", 320 | " # return dest.copy_tree(source)\n", 321 | " # # default klayout behavior. Not used because our implementation is\n", 322 | " # # slightly different in the order cells are added\n", 323 | "\n", 324 | " dest_layout = dest.layout()\n", 325 | " source_layout = source.layout()\n", 326 | " for layer in source_layout.layer_infos():\n", 327 | " source_idx = source_layout.layer(layer)\n", 328 | " dest_idx = dest_layout.layer(layer)\n", 329 | " for shape in source.each_shape(source_idx):\n", 330 | " dest.shapes(dest_idx).insert(shape)\n", 331 | "\n", 332 | " source_cells_map = {}\n", 333 | " for idx in source.each_child_cell():\n", 334 | " current_cell = source_layout.cell(idx)\n", 335 | " source_cells_map[idx] = _add_cell_to_layout(\n", 336 | " dest_layout, current_cell, on_same_name, depth\n", 337 | " )\n", 338 | "\n", 339 | " for inst in source.each_inst():\n", 340 | " dest_ref = pya.CellInstArray(source_cells_map[inst.cell_index], inst.cplx_trans)\n", 341 | " dest.insert(dest_ref)\n", 342 | " \n", 343 | "COPY_IMPLEMENTATIONS[pya.Cell] = _copy_klayout" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": null, 349 | "id": "9e85461a-a930-4313-b0e4-d16e8b446da3", 350 | "metadata": {}, 351 | "outputs": [], 352 | "source": [ 353 | "# exporti\n", 354 | "def _copy_gdspy(source, dest, on_same_name, depth=0):\n", 355 | " if depth > MAX_DEPTH:\n", 356 | " return # basically just to make type checker happy\n", 357 | " dest_layout = dest.layout()\n", 358 | " for lr, dt in source.get_layers():\n", 359 | " dest_idx = dest_layout.layer(pya.LayerInfo(lr, dt))\n", 360 | " for arr in source.get_polygons(by_spec=(lr, dt), depth=1):\n", 361 | " dest.shapes(dest_idx).insert(\n", 362 | " pya.DPolygon([pya.DPoint(x, y) for x, y in arr])\n", 363 | " )\n", 364 | " for arr in source.get_paths(depth=1):\n", 365 | " raise NotImplementedError(\"Cannot convert native gdspy paths (yet).\")\n", 366 | "\n", 367 | " source_cells_map = {}\n", 368 | " child_cells = sorted(\n", 369 | " set(ref.ref_cell for ref in source.references), key=lambda c: c.name\n", 370 | " )\n", 371 | " for current_cell in child_cells:\n", 372 | " if on_same_name == \"skip\" and dest_layout.has_cell(current_cell.name):\n", 373 | " source_cells_map[current_cell.name] = dest_layout.cell_by_name(\n", 374 | " current_cell.name\n", 375 | " )\n", 376 | " continue\n", 377 | " elif on_same_name == \"replace\" and dest_layout.has_cell(current_cell.name):\n", 378 | " dest_layout.delete_cell(dest_layout.cell_by_name(current_cell.name))\n", 379 | " new_cell = dest_layout.create_cell(current_cell.name)\n", 380 | " _copy_gdspy(current_cell, new_cell, on_same_name, depth=depth + 1)\n", 381 | " new_cell._layout = dest_layout\n", 382 | " source_cells_map[current_cell.name] = dest_layout.cell_by_name(\n", 383 | " current_cell.name\n", 384 | " )\n", 385 | "\n", 386 | " for ref in source.references:\n", 387 | " dest_cell = source_cells_map[ref.ref_cell.name]\n", 388 | " mag = ref.magnification if ref.magnification is not None else 1.0\n", 389 | " rot = ref.rotation if ref.rotation is not None else 0.0\n", 390 | " x, y = ref.origin\n", 391 | " dest_trans = pya.CplxTrans(\n", 392 | " mag, rot, ref.x_reflection, int(1000.0 * x), int(1000.0 * y)\n", 393 | " )\n", 394 | " dest_ref = pya.CellInstArray(dest_cell, dest_trans)\n", 395 | " dest.insert(dest_ref)\n", 396 | " \n", 397 | "if GdsPyCell is not DoesNotExist:\n", 398 | " COPY_IMPLEMENTATIONS[GdsPyCell] = _copy_gdspy" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": null, 404 | "id": "ec99e453-8e9d-4bc2-9fbd-ad2d61f273f0", 405 | "metadata": {}, 406 | "outputs": [], 407 | "source": [ 408 | "# export\n", 409 | "\n", 410 | "def copy_tree(source: pya.Cell, dest: pya.Cell, on_same_name: str = \"skip\"):\n", 411 | " f\"\"\"Copy the contents of a cell into another cell\n", 412 | "\n", 413 | " Args:\n", 414 | " source: the source cell to copy the contents from\n", 415 | " dest: the destination cell to copy the contents into\n", 416 | " on_same_name: {ON_SAME_NAME_DOC}\n", 417 | " \"\"\"\n", 418 | " on_same_name = _validate_on_same_name(on_same_name)\n", 419 | " for cls, _copy_impl in COPY_IMPLEMENTATIONS.items():\n", 420 | " if isinstance(source, cls):\n", 421 | " break\n", 422 | " else:\n", 423 | " raise TypeError(\n", 424 | " f\"Error in copy_tree: source is not a supported cell type. \"\n", 425 | " f\"Got: {type(source)}. Expected: {', '.join(t.__name__ for t in COPY_IMPLEMENTATIONS)}.\"\n", 426 | " )\n", 427 | " \n", 428 | " _copy_impl(source, dest, on_same_name)\n", 429 | " return dest\n", 430 | "\n", 431 | "def _validate_on_same_name(on_same_name):\n", 432 | " on_same_name = on_same_name.lower()\n", 433 | " allowed_on_same_name = [\"skip\", \"replace\", \"add_suffix\"]\n", 434 | " if not on_same_name in allowed_on_same_name:\n", 435 | " raise ValueError(\n", 436 | " \"on_same_name should be one of the following: \"\n", 437 | " f\"{', '.join(repr(key) for key in allowed_on_same_name)}.\"\n", 438 | " )\n", 439 | " return on_same_name" 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": null, 445 | "id": "078ac576-523a-493c-abfa-31f9cd998b48", 446 | "metadata": {}, 447 | "outputs": [], 448 | "source": [ 449 | "my_cell = fl.cell(\n", 450 | " name='my_cell',\n", 451 | " shapes={(19, 0): [fl.polygon([(0, 0), (5, 0), (5, 5), (0, 5)])]}\n", 452 | ")\n", 453 | "my_cell" 454 | ] 455 | }, 456 | { 457 | "cell_type": "code", 458 | "execution_count": null, 459 | "id": "b1a6c584-c7bb-4d75-b0a4-339973ccce25", 460 | "metadata": {}, 461 | "outputs": [], 462 | "source": [ 463 | "copy_tree(triangle, my_cell)\n", 464 | "my_cell" 465 | ] 466 | }, 467 | { 468 | "cell_type": "code", 469 | "execution_count": null, 470 | "id": "fe299bab-2055-4b7c-b577-3e41913dd7a3", 471 | "metadata": {}, 472 | "outputs": [], 473 | "source": [ 474 | "# export\n", 475 | "def add_cells_to_layout(\n", 476 | " layout: pya.Layout,\n", 477 | " cells: List[pya.Cell],\n", 478 | " on_same_name: str = \"skip\",\n", 479 | " depth: int = 0,\n", 480 | "):\n", 481 | " f\"\"\"Add multiple cells to a layout\n", 482 | "\n", 483 | " Args:\n", 484 | " layout: The layout to add the cell into\n", 485 | " cells: the cells to add into the layout\n", 486 | " on_same_name: {ON_SAME_NAME_DOC}\n", 487 | " \"\"\"\n", 488 | " cells = sorted(cells, key=lambda c: c.hierarchy_levels())\n", 489 | " for cell in cells:\n", 490 | " _add_cell_to_layout(layout, cell, on_same_name, depth)\n", 491 | " return layout\n", 492 | " \n", 493 | "def _add_lib_cell_to_layout(layout: pya.Layout, lib: pya.Library, cell: pya.Cell):\n", 494 | " \"\"\"Add a library Cell to a layout\n", 495 | "\n", 496 | " Args:\n", 497 | " layout: The layout to add the cell into\n", 498 | " lib: The library to which the cell belongs\n", 499 | " cell: the cell to add into the layout\n", 500 | " \"\"\"\n", 501 | " pcell = cell.pcell_declaration()\n", 502 | " if pcell is not None:\n", 503 | " return _add_lib_pcell_to_layout(layout, lib, pcell, cell.pcell_parameters())\n", 504 | " else:\n", 505 | " cell_idx = lib.layout().cell_by_name(cell.name)\n", 506 | " return layout.add_lib_cell(lib, cell_idx)\n", 507 | " \n", 508 | "\n", 509 | "def _add_cell_to_layout(\n", 510 | " layout: pya.Layout, cell: pya.Cell, on_same_name: str = \"skip\", depth: int = 0\n", 511 | "):\n", 512 | " f\"\"\"Add a cell to a layout\n", 513 | "\n", 514 | " Args:\n", 515 | " layout: The layout to add the cell into\n", 516 | " cell: the cell to add into the layout\n", 517 | " on_same_name: {ON_SAME_NAME_DOC}\n", 518 | " \"\"\"\n", 519 | " if cell.is_proxy():\n", 520 | " _add_lib_cell_to_layout(layout, cell.library(), cell)\n", 521 | " if on_same_name == \"skip\" and layout.has_cell(cell.name):\n", 522 | " return layout.cell_by_name(cell.name)\n", 523 | " elif on_same_name == \"replace\" and layout.has_cell(cell.name):\n", 524 | " layout.delete_cell(layout.cell_by_name(cell.name))\n", 525 | " new_cell = layout.create_cell(cell.name)\n", 526 | " _copy_klayout(cell, new_cell, on_same_name, depth=depth + 1)\n", 527 | " new_cell._layout = layout\n", 528 | " return layout.cell_by_name(new_cell.name)" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": null, 534 | "id": "7b7bf0aa-b75a-4aed-ae6b-42e7f6b03987", 535 | "metadata": {}, 536 | "outputs": [], 537 | "source": [ 538 | "layout = fl.layout()\n", 539 | "add_cells_to_layout(layout, cells=[my_cell, triangle, circle()])\n", 540 | "fig =flbk.draw_layout(None, layout)\n", 541 | "bio.show(fig)" 542 | ] 543 | }, 544 | { 545 | "cell_type": "code", 546 | "execution_count": null, 547 | "id": "2c799eaf-534c-4284-96a0-82f835e0818f", 548 | "metadata": {}, 549 | "outputs": [], 550 | "source": [ 551 | "# export\n", 552 | "\n", 553 | "def add_pcells_to_layout(layout, pcells):\n", 554 | " for pcell in pcells:\n", 555 | " func = pcell\n", 556 | " while hasattr(func, \"func\"):\n", 557 | " func = func.func\n", 558 | " layout.register_pcell(func.__name__, pcell)\n", 559 | "\n", 560 | "def _get_pcell_param_value(params, param):\n", 561 | " value = params.get(param.name, param.default)\n", 562 | " if param.type == pya.PCellDeclarationHelper.TypeLayer:\n", 563 | " if not isinstance(value, pya.LayerInfo):\n", 564 | " value = pya.LayerInfo(*value)\n", 565 | " return value\n", 566 | "\n", 567 | "def _add_lib_pcell_to_layout(\n", 568 | " layout: pya.Layout,\n", 569 | " lib: pya.Library,\n", 570 | " pcell: pya.PCellDeclaration,\n", 571 | " params: Dict[str, Any],\n", 572 | "):\n", 573 | " \"\"\"Add a library PCell to a layout\n", 574 | "\n", 575 | " Args:\n", 576 | " layout: The layout to add the cell into\n", 577 | " lib: The library to which the cell belongs\n", 578 | " cell: the cell to add into the layout\n", 579 | " params: the parameters to instantiate the pcell with\n", 580 | " \"\"\"\n", 581 | " if isinstance(params, dict):\n", 582 | " params_list = [\n", 583 | " _get_pcell_param_value(params, p) for p in pcell.get_parameters()\n", 584 | " ]\n", 585 | " else:\n", 586 | " params_list = params\n", 587 | " return layout.add_pcell_variant(lib, pcell.id(), params_list)" 588 | ] 589 | }, 590 | { 591 | "cell_type": "code", 592 | "execution_count": null, 593 | "id": "a8ec2686-f720-4f5e-867d-13ca856c9497", 594 | "metadata": {}, 595 | "outputs": [], 596 | "source": [ 597 | "add_pcells_to_layout(layout, pcells=[circle])\n", 598 | "layout" 599 | ] 600 | } 601 | ], 602 | "metadata": { 603 | "kernelspec": { 604 | "display_name": "fl", 605 | "language": "python", 606 | "name": "fl" 607 | } 608 | }, 609 | "nbformat": 4, 610 | "nbformat_minor": 5 611 | } 612 | -------------------------------------------------------------------------------- /source/91_bokeh.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e575b56f-0caf-49cf-a935-fe5b931395b2", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# default_exp bokeh" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "7031f6c1-8422-4092-85a3-ebd76b410b10", 16 | "metadata": {}, 17 | "source": [ 18 | "# Extension - Bokeh\n", 19 | "> Bokeh visualization extension" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "a7f7e0ea-b6b2-44ce-a2d9-20b0af0bef8f", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# exporti\n", 30 | "from __future__ import annotations\n", 31 | "\n", 32 | "import glob\n", 33 | "import os\n", 34 | "from functools import lru_cache\n", 35 | "from typing import Dict, List, Optional, Tuple, Union\n", 36 | "\n", 37 | "import bokeh.io as bio\n", 38 | "import bokeh.models as bm\n", 39 | "import bokeh.plotting as bp\n", 40 | "import numpy as np\n", 41 | "import pkg_resources\n", 42 | "import pya\n", 43 | "from lxml import etree" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "ccaeabe7-3260-45ca-a2d6-5c27873e0f15", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# hide\n", 54 | "bio.output_notebook(hide_banner=True)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "8829c0cd-68a1-430e-ac98-e3417b748364", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "# export\n", 65 | "ALPHA = 0.3\n", 66 | "RED = \"#FF0000\"\n", 67 | "GREEN = \"#00FF00\"\n", 68 | "BLUE = \"#0000FF\"\n", 69 | "C0 = '#1f77b4'\n", 70 | "C1 = '#ff7f0e'\n", 71 | "C2 = '#2ca02c'\n", 72 | "C3 = '#d62728'\n", 73 | "C4 = '#9467bd'\n", 74 | "C5 = '#8c564b'\n", 75 | "C6 = '#e377c2'\n", 76 | "C7 = '#7f7f7f'\n", 77 | "C8 = '#bcbd22'\n", 78 | "C9 = '#17becf'" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "cd18eb7b-4b16-421a-8a2b-150b94401585", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# export\n", 89 | "LayerProperty = Dict[str, Union[str, bool]]\n", 90 | "LayerProperties = Dict[Tuple[int, int], LayerProperty]\n", 91 | "\n", 92 | "def get_lyp_path(path: Optional[str] = None):\n", 93 | " # first, let's try \"~/.klayout\"\n", 94 | " if path is None:\n", 95 | " path = \"\"\n", 96 | " path = os.path.abspath(os.path.expanduser(path))\n", 97 | " if os.path.isdir(path):\n", 98 | " possible_paths = glob.glob(f\"{path}/*.lyp\")\n", 99 | " if not possible_paths:\n", 100 | " path = get_lyp_path(pkg_resources.resource_filename(\"flayout\", \"layers.lyp\"))\n", 101 | " else:\n", 102 | " path = possible_paths[0]\n", 103 | " return path" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "id": "2d3a2c3c-4625-4304-8a4c-6bce298028a6", 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "get_lyp_path()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "id": "d04f6281-3c09-4350-8a98-a7a52429f9fe", 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "# export\n", 124 | "@lru_cache\n", 125 | "def read_lyp(path: Optional[str] = None) -> LayerProperties:\n", 126 | " \"\"\"Load layer properties from a file\n", 127 | "\n", 128 | " Args:\n", 129 | " path: the path where to load the layer properties from\n", 130 | "\n", 131 | " Returns:\n", 132 | " a dictionary of layer property dictionaries\n", 133 | " \"\"\"\n", 134 | " path = get_lyp_path(path)\n", 135 | " xml = etree.parse(path)\n", 136 | " root = xml.getroot()\n", 137 | " parsed: LayerProperties = {\n", 138 | " (0, 0): {\n", 139 | " \"name\": \"\",\n", 140 | " \"frame-color\": \"#000000\",\n", 141 | " \"fill-color\": \"#000000\",\n", 142 | " \"visible\": True,\n", 143 | " }\n", 144 | " }\n", 145 | " for properties in root.iter(\"properties\"):\n", 146 | " name = properties.find(\"name\")\n", 147 | " if name is not None:\n", 148 | " name, *_ = name.text.split(\"(\")\n", 149 | " name = name.strip()\n", 150 | " else:\n", 151 | " name = \"\"\n", 152 | " \n", 153 | " layerstr = properties.find(\"source\")\n", 154 | " if layerstr is not None:\n", 155 | " layerstr, *_ = layerstr.text.split(\"@\")\n", 156 | " lr, dt = layerstr.strip().split(\"/\")\n", 157 | " lr, dt = int(lr), int(dt)\n", 158 | " parsed[lr, dt] = {\n", 159 | " \"name\": name,\n", 160 | " \"frame-color\": properties.find(\"frame-color\").text,\n", 161 | " \"fill-color\": properties.find(\"fill-color\").text,\n", 162 | " \"visible\": bool(properties.find(\"visible\").text),\n", 163 | " }\n", 164 | " \n", 165 | " return parsed" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "id": "6f47525e-0a04-431b-ab77-4068eda87a7d", 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "lyp = read_lyp()\n", 176 | "{k: v for k, v in lyp.items() if k in [(19, 0), (10001, 0)]}" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "id": "579d7d05-e00b-4b4b-b102-bfb2229a5c4f", 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "# export\n", 187 | "@lru_cache\n", 188 | "def get_lyp(\n", 189 | " layer_info: Union[pya.LayerInfo, Tuple[int, int]], path: Optional[str] = None\n", 190 | ") -> LayerProperty:\n", 191 | " \"\"\"Load layer properties for a specific layer from a file\n", 192 | "\n", 193 | " Args:\n", 194 | " layer_info: the layer info tuple to load the layer properties for\n", 195 | " path: the path where to load the layer properties from\n", 196 | "\n", 197 | " Returns:\n", 198 | " a layer property dictionary\n", 199 | " \"\"\"\n", 200 | " if isinstance(layer_info, pya.LayerInfo):\n", 201 | " layer, datatype = layer_info.layer, layer_info.datatype # type: ignore\n", 202 | " else:\n", 203 | " layer, datatype, *_ = layer_info\n", 204 | " lyps = read_lyp(path=path)\n", 205 | " lyp = lyps.get((layer, datatype), lyps[0, 0])\n", 206 | " return lyp" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "id": "a739a021-45b8-4860-9d8b-6aff145374d2", 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "get_lyp((19, 0))" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "id": "19e329c2-9900-4745-8c5e-e9f247043b49", 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "# exporti\n", 227 | "def _get_range(box: Union[pya.Box, pya.DBox]) -> Tuple[float, float, float, float, float, float]:\n", 228 | " \"\"\"Get the plotting bbox for a klayout box\n", 229 | "\n", 230 | " Args:\n", 231 | " poly: the polygon to create the bbox for\n", 232 | "\n", 233 | " returns:\n", 234 | " x1, x2, y1, y2, w, h\n", 235 | " \"\"\"\n", 236 | " if isinstance(box, pya.Box):\n", 237 | " box = box.to_dtype()\n", 238 | " x1, x2 = min(box.p1.x, box.p2.x), max(box.p1.x, box.p2.x)\n", 239 | " y1, y2 = min(box.p1.y, box.p2.y), max(box.p1.y, box.p2.y)\n", 240 | " w, h = x2 - x1, y2 - y1\n", 241 | " if w > 5 * h:\n", 242 | " y, h = 0.5 * (y1 + y2), w / 5\n", 243 | " y1, y2 = y - h / 2, y + h / 2\n", 244 | " if h > 5 * w:\n", 245 | " x, w = 0.5 * (x1 + x2), h / 5\n", 246 | " x1, x2 = x - w / 2, x + w / 2\n", 247 | " if w < 2.0:\n", 248 | " x1, x2 = (x2 + x1) / 2 - 1.0, (x2 + x1) / 2 + 1.0\n", 249 | " w = 2.0\n", 250 | " if h < 2.0:\n", 251 | " y1, y2 = (y2 + y1) / 2 - 1.0, (y2 + y1) / 2 + 1.0\n", 252 | " h = 2.0\n", 253 | " return x1, x2, y1, y2, w, h" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "id": "064d8dcf-f3dc-4e77-a6ef-83de2a585638", 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "box = pya.Box(0.0, 0.0, 5.0, 2.5)\n", 264 | "_get_range(box)" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "id": "2e786a55-fca5-4cf3-8932-41cb7f7044cf", 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "box = pya.DBox(0.0, 0.0, 5.0, 2.5)\n", 275 | "#box = box.enlarge(pya.DVector(0.5, 0.5))\n", 276 | "_get_range(box)" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": null, 282 | "id": "9eac0379-4bc4-4ec4-a5da-55a6b7cc4974", 283 | "metadata": {}, 284 | "outputs": [], 285 | "source": [ 286 | "# export\n", 287 | "def new_plot(box: Union[pya.Box, pya.DBox], max_dim: Optional[float] = None) -> bp.Figure:\n", 288 | " \"\"\"Create a new plot with limits determined by a bbox\n", 289 | "\n", 290 | " Args:\n", 291 | " box: the bbox of the polygon or cell to create the figure for\n", 292 | "\n", 293 | " Returns:\n", 294 | " a bokeh Figure.\n", 295 | " \"\"\"\n", 296 | " if max_dim is None:\n", 297 | " max_dim = 500\n", 298 | " x0, x1, y0, y1, w, h = _get_range(box)\n", 299 | " if w > h:\n", 300 | " plot_width = max_dim\n", 301 | " plot_height = max_dim * h / w\n", 302 | " else:\n", 303 | " plot_height = max_dim\n", 304 | " plot_width = max_dim * w / h\n", 305 | " plot = bp.figure(\n", 306 | " plot_width=round(plot_width),\n", 307 | " plot_height=round(plot_height),\n", 308 | " x_range=(x0, x1),\n", 309 | " y_range=(y0, y1),\n", 310 | " match_aspect=True,\n", 311 | " toolbar_location=None,\n", 312 | " tools=[bm.PanTool(), zoom := bm.WheelZoomTool()],\n", 313 | " )\n", 314 | " plot.toolbar.active_scroll = zoom # type: ignore\n", 315 | " return plot" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "id": "a0735379-fc00-4f7b-a03a-644e06fc080d", 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "p = new_plot(box)\n", 326 | "p.line([0, 1], [0, 1])\n", 327 | "bio.show(p)" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": null, 333 | "id": "5cc20edd-4ea8-488a-bdf3-444f2ca92301", 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "# export\n", 338 | "def adjust_plot(plot: bp.Figure, box: Union[pya.Box, pya.DBox], max_dim: Optional[float] = None) -> bp.Figure:\n", 339 | " \"\"\"Adjust a plot with limits determined by a bbox\n", 340 | "\n", 341 | " Args:\n", 342 | " plot: the plot to adjust the limits for\n", 343 | " box: the bbox of the polygon or cell to create the figure for\n", 344 | "\n", 345 | " Returns:\n", 346 | " a bokeh Figure.\n", 347 | " \"\"\"\n", 348 | " if plot is None:\n", 349 | " return new_plot(box, max_dim=max_dim)\n", 350 | " if max_dim is None:\n", 351 | " max_dim = max(plot.height, plot.width)\n", 352 | " assert max_dim is not None\n", 353 | " x0, x1, y0, y1, w, h = _get_range(box)\n", 354 | " if w > h:\n", 355 | " plot_width = max_dim\n", 356 | " plot_height = max_dim * h / w\n", 357 | " else:\n", 358 | " plot_height = max_dim\n", 359 | " plot_width = max_dim * w / h\n", 360 | " plot.plot_width = round(plot_width)\n", 361 | " plot.plot_height = round(plot_height)\n", 362 | " plot.x_range = bm.Range1d(start=x0, end=x1)\n", 363 | " plot.y_range = bm.Range1d(start=y0, end=y1)\n", 364 | " return plot" 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "id": "7955840b-27d4-4dd1-bec3-05fdfd75abfc", 371 | "metadata": {}, 372 | "outputs": [], 373 | "source": [ 374 | "fig = adjust_plot(p, pya.DBox(-1.0, -1.0, 5.0, 3.0))\n", 375 | "bio.show(fig)" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": null, 381 | "id": "557b15f1-983c-4a98-9a18-8b2f3c27536a", 382 | "metadata": {}, 383 | "outputs": [], 384 | "source": [ 385 | "# export\n", 386 | "def draw_polys(\n", 387 | " plot: bp.Figure,\n", 388 | " polys: List[Union[pya.Polygon, pya.SimplePolygon, pya.DPolygon, pya.DSimplePolygon]],\n", 389 | " layer=(0, 0),\n", 390 | " fill_color=None,\n", 391 | " line_color=None,\n", 392 | " fill_alpha=ALPHA,\n", 393 | "):\n", 394 | " \"\"\"draw polygons with bokeh\n", 395 | "\n", 396 | " Args:\n", 397 | " plot: the plot to draw the polygon in\n", 398 | " polys: the polygons to draw\n", 399 | "\n", 400 | " Returns:\n", 401 | " the (inplace) modified plot containing the polygons\n", 402 | " \"\"\"\n", 403 | " # some typing definitions for our own sanity...\n", 404 | "\n", 405 | " # Array of single coordinates (x OR y): ndim==1, shape=(N,).\n", 406 | " SimplePolygon = np.ndarray\n", 407 | "\n", 408 | " # List of coordinate arrays (first element: hull, other elements: holes)\n", 409 | " Polygon = List[SimplePolygon]\n", 410 | "\n", 411 | " # List of individual polygons (bokeh will XOR those polygons -> usually 1 Polygon per MultiPolygon)\n", 412 | " MultiPolygon = List[Polygon]\n", 413 | "\n", 414 | " # List of multi polygons\n", 415 | " MultiPolygons = List[MultiPolygon]\n", 416 | "\n", 417 | " xs: MultiPolygons = []\n", 418 | " ys: MultiPolygons = []\n", 419 | "\n", 420 | " for poly in polys:\n", 421 | " if isinstance(poly, (pya.Polygon, pya.SimplePolygon)):\n", 422 | " poly = poly.to_dtype()\n", 423 | "\n", 424 | " if isinstance(poly, pya.DPolygon):\n", 425 | " hull = np.asarray([(p.x, p.y) for p in poly.each_point_hull()])\n", 426 | " holes = [\n", 427 | " np.asarray([(p.x, p.y) for p in poly.each_point_hole(i)])\n", 428 | " for i in range(poly.holes())\n", 429 | " ]\n", 430 | " elif isinstance(poly, pya.DSimplePolygon):\n", 431 | " hull = np.asarray([(p.x, p.y) for p in poly.each_point()])\n", 432 | " holes = []\n", 433 | " else:\n", 434 | " raise ValueError(\n", 435 | " f\"Invalid polygon type. Got: {type(poly)}. \"\n", 436 | " f\"Expected 'DPolygon' or 'DSimplePolygon'\"\n", 437 | " )\n", 438 | " \n", 439 | " if hull.shape[0] < 3:\n", 440 | " continue\n", 441 | "\n", 442 | " plot = adjust_plot(\n", 443 | " plot, pya.Polygon([pya.DPoint(x, y) for x, y in hull]).bbox()\n", 444 | " )\n", 445 | "\n", 446 | " xs_: MultiPolygon = [[hull[:, 0], *(h[:, 0] for h in holes)]]\n", 447 | " ys_: MultiPolygon = [[hull[:, 1], *(h[:, 1] for h in holes)]]\n", 448 | " xs.append(xs_)\n", 449 | " ys.append(ys_)\n", 450 | "\n", 451 | " source = bm.ColumnDataSource({\"xs\": xs, \"ys\": ys})\n", 452 | " lyp = get_lyp(layer)\n", 453 | " patch = bm.MultiPolygons(\n", 454 | " xs=\"xs\",\n", 455 | " ys=\"ys\",\n", 456 | " fill_color=fill_color or lyp[\"fill-color\"],\n", 457 | " fill_alpha=fill_alpha,\n", 458 | " line_color=line_color or lyp[\"frame-color\"],\n", 459 | " )\n", 460 | " plot.add_glyph(source, patch)\n", 461 | " return plot" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "id": "9c949c3c-ed3f-44b1-a0e1-c6f44a075b3e", 468 | "metadata": {}, 469 | "outputs": [], 470 | "source": [ 471 | "fig = new_plot(box)\n", 472 | "draw_polys(fig, [pya.DPolygon([pya.DPoint(0, 0), pya.DPoint(2, 0), pya.DPoint(3, 1)])])\n", 473 | "bio.show(fig)" 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": null, 479 | "id": "a54d3152-b0eb-46f7-8e32-276af7f848cb", 480 | "metadata": {}, 481 | "outputs": [], 482 | "source": [ 483 | "# export\n", 484 | "def draw_poly(\n", 485 | " plot: bp.Figure,\n", 486 | " poly: Union[pya.Polygon, pya.SimplePolygon, pya.DPolygon, pya.DSimplePolygon],\n", 487 | " layer=(0, 0),\n", 488 | " fill_color=None,\n", 489 | " line_color=None,\n", 490 | " fill_alpha=ALPHA,\n", 491 | "):\n", 492 | " \"\"\"draw a polygon with bokeh\n", 493 | "\n", 494 | " Args:\n", 495 | " plot: the plot to draw the polygon in\n", 496 | " poly: the polygon to draw\n", 497 | "\n", 498 | " Returns:\n", 499 | " the (inplace) modified plot containing the polygon\n", 500 | " \"\"\"\n", 501 | " plot = adjust_plot(plot, poly.bbox())\n", 502 | " return draw_polys(\n", 503 | " plot,\n", 504 | " [poly],\n", 505 | " layer=layer,\n", 506 | " fill_color=fill_color,\n", 507 | " line_color=line_color,\n", 508 | " fill_alpha=fill_alpha,\n", 509 | " )" 510 | ] 511 | }, 512 | { 513 | "cell_type": "code", 514 | "execution_count": null, 515 | "id": "8a51ba82-24f3-4be9-a3a4-93eb2f30625c", 516 | "metadata": {}, 517 | "outputs": [], 518 | "source": [ 519 | "fig = new_plot(box)\n", 520 | "draw_poly(fig, pya.DPolygon([pya.DPoint(0, 0), pya.DPoint(2, 0), pya.DPoint(3, 1)]), layer=(19, 0))\n", 521 | "bio.show(fig)" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": null, 527 | "id": "d1adf85b-b520-4638-810d-466cd9d2e504", 528 | "metadata": {}, 529 | "outputs": [], 530 | "source": [ 531 | "# export\n", 532 | "def draw_path(\n", 533 | " plot: bp.Figure,\n", 534 | " path: Union[pya.Path, pya.DPath],\n", 535 | " layer=(0, 0),\n", 536 | " fill_color=None,\n", 537 | " line_color=None,\n", 538 | " fill_alpha=ALPHA,\n", 539 | "):\n", 540 | " \"\"\"draw a path with bokeh\n", 541 | "\n", 542 | " Args:\n", 543 | " plot: the plot to draw the path in\n", 544 | " poly: the path to draw\n", 545 | "\n", 546 | " Returns:\n", 547 | " the (inplace) modified plot containing the path\n", 548 | " \"\"\"\n", 549 | " return draw_polys(\n", 550 | " plot,\n", 551 | " [path.polygon()],\n", 552 | " layer=layer,\n", 553 | " fill_color=fill_color,\n", 554 | " line_color=line_color,\n", 555 | " fill_alpha=fill_alpha,\n", 556 | " )" 557 | ] 558 | }, 559 | { 560 | "cell_type": "code", 561 | "execution_count": null, 562 | "id": "829d9970-a1ca-44e6-8345-b53542f09188", 563 | "metadata": {}, 564 | "outputs": [], 565 | "source": [ 566 | "fig = new_plot(box)\n", 567 | "path = pya.DPath([pya.Point(0, 0), pya.Point(10, 0)], 3.0)\n", 568 | "draw_path(fig, path)\n", 569 | "bio.show(fig)" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": null, 575 | "id": "f0878503-a639-4402-ae92-49e242f2bd73", 576 | "metadata": {}, 577 | "outputs": [], 578 | "source": [ 579 | "# export\n", 580 | "def draw_point(\n", 581 | " plot: bp.Figure,\n", 582 | " p: Union[pya.Point, pya.DPoint],\n", 583 | " layer=(0, 0),\n", 584 | " fill_color=None,\n", 585 | " line_color=None,\n", 586 | " fill_alpha=ALPHA,\n", 587 | "):\n", 588 | " \"\"\"draw a point with bokeh\n", 589 | "\n", 590 | " Args:\n", 591 | " plot: the plot to draw the point in\n", 592 | " p: the point to draw\n", 593 | "\n", 594 | " Returns:\n", 595 | " the (inplace) modified plot containing the point\n", 596 | " \"\"\"\n", 597 | " if isinstance(p, pya.Point):\n", 598 | " p = p.to_dtype()\n", 599 | " v = pya.DVector(1.0, 1.0)\n", 600 | " box = pya.DBox(p - v, p + v)\n", 601 | " plot = adjust_plot(plot, box)\n", 602 | " *_, w, h = _get_range(box)\n", 603 | " radius = max(w, h) / 30\n", 604 | " lyp = get_lyp(layer)\n", 605 | " plot.circle(\n", 606 | " p.x,\n", 607 | " p.y,\n", 608 | " fill_alpha=fill_alpha,\n", 609 | " fill_color=fill_color or lyp[\"fill-color\"],\n", 610 | " line_color=line_color or [\"frame-color\"],\n", 611 | " radius=radius,\n", 612 | " )\n", 613 | " return plot" 614 | ] 615 | }, 616 | { 617 | "cell_type": "code", 618 | "execution_count": null, 619 | "id": "e2592aa9-990f-4d5e-9d70-7e3f8f677290", 620 | "metadata": {}, 621 | "outputs": [], 622 | "source": [ 623 | "fig = new_plot(box)\n", 624 | "draw_point(fig, pya.Point(0, 0))\n", 625 | "bio.show(fig)" 626 | ] 627 | }, 628 | { 629 | "cell_type": "code", 630 | "execution_count": null, 631 | "id": "4d710d58-f8e3-44a8-b0d1-eea2c794a3ac", 632 | "metadata": {}, 633 | "outputs": [], 634 | "source": [ 635 | "# export\n", 636 | "def draw_vector(\n", 637 | " plot: bp.Figure,\n", 638 | " v: Union[pya.Vector, pya.DVector],\n", 639 | " layer=(0, 0),\n", 640 | " fill_color=None,\n", 641 | " line_color=None,\n", 642 | " fill_alpha=ALPHA,\n", 643 | "):\n", 644 | " \"\"\"draw a vector as an arrow with bokeh\n", 645 | "\n", 646 | " Args:\n", 647 | " plot: the plot to draw the vector in\n", 648 | " v: the vector to draw\n", 649 | "\n", 650 | " Returns:\n", 651 | " the (inplace) modified plot containing the vector\n", 652 | " \"\"\"\n", 653 | " if isinstance(v, pya.Vector):\n", 654 | " v = pya.DVector(v.x / 1000.0, v.y / 1000.0)\n", 655 | " box = pya.DBox(0, 0, v.x, v.y)\n", 656 | " plot = adjust_plot(plot, box, max_dim=250)\n", 657 | " lyp = get_lyp(layer)\n", 658 | " arrow_head = bm.VeeHead(\n", 659 | " fill_alpha=fill_alpha,\n", 660 | " fill_color=fill_color or lyp[\"fill-color\"],\n", 661 | " line_color=line_color or lyp[\"frame-color\"],\n", 662 | " )\n", 663 | " arrow = bm.Arrow(\n", 664 | " end=arrow_head,\n", 665 | " x_start=0,\n", 666 | " y_start=0,\n", 667 | " x_end=v.x,\n", 668 | " y_end=v.y,\n", 669 | " line_color=line_color or lyp[\"frame-color\"],\n", 670 | " )\n", 671 | " plot.add_layout(arrow)\n", 672 | " return plot" 673 | ] 674 | }, 675 | { 676 | "cell_type": "code", 677 | "execution_count": null, 678 | "id": "665ca0da-05f9-4d8a-b1a8-dbbdd17ff64e", 679 | "metadata": {}, 680 | "outputs": [], 681 | "source": [ 682 | "fig = new_plot(box)\n", 683 | "draw_vector(fig, pya.DVector(3, 4))\n", 684 | "bio.show(fig)" 685 | ] 686 | }, 687 | { 688 | "cell_type": "code", 689 | "execution_count": null, 690 | "id": "95f5d1c6-b4c3-4e0e-940d-4d0569d27340", 691 | "metadata": {}, 692 | "outputs": [], 693 | "source": [ 694 | "# export\n", 695 | "def _box_to_poly(box: Union[pya.Box, pya.DBox]):\n", 696 | " \"\"\"convert a box into a polygon\n", 697 | "\n", 698 | " Args:\n", 699 | " box: the box to convert into a polygon\n", 700 | "\n", 701 | " Returns:\n", 702 | " the polygon\n", 703 | " \"\"\"\n", 704 | " if isinstance(box, pya.Box):\n", 705 | " box = box.to_dtype()\n", 706 | " x0, y0 = box.p1.x, box.p1.y\n", 707 | " x1, y1 = box.p2.x, box.p2.y\n", 708 | " return pya.DPolygon(\n", 709 | " [pya.DPoint(x0, y0), pya.DPoint(x1, y0), pya.DPoint(x1, y1), pya.DPoint(x0, y1)]\n", 710 | " )\n", 711 | "\n", 712 | "\n", 713 | "def draw_box(\n", 714 | " plot: bp.Figure,\n", 715 | " box: Union[pya.Box, pya.DBox],\n", 716 | " fill_alpha=0.0,\n", 717 | " fill_color=\"#000000\",\n", 718 | " line_color=GREEN,\n", 719 | "):\n", 720 | " \"\"\"draw a box with bokeh\n", 721 | "\n", 722 | " Args:\n", 723 | " plot: the plot to draw the box in\n", 724 | " box: the box to draw\n", 725 | "\n", 726 | " Returns:\n", 727 | " the (inplace) modified plot containing the box\n", 728 | " \"\"\"\n", 729 | " plot = adjust_plot(plot, box)\n", 730 | " poly = _box_to_poly(box)\n", 731 | " return draw_poly(\n", 732 | " plot, poly, fill_alpha=fill_alpha, fill_color=fill_color, line_color=line_color\n", 733 | " )" 734 | ] 735 | }, 736 | { 737 | "cell_type": "code", 738 | "execution_count": null, 739 | "id": "7ddc16b7-d188-4d4c-9667-dd08c37069dd", 740 | "metadata": {}, 741 | "outputs": [], 742 | "source": [ 743 | "fig = new_plot(box)\n", 744 | "draw_box(fig, pya.DBox(1.0, 2.0, 4.0, 3.0))\n", 745 | "bio.show(fig)" 746 | ] 747 | }, 748 | { 749 | "cell_type": "code", 750 | "execution_count": null, 751 | "id": "b141005a-0e36-491a-b387-e3df71069d2e", 752 | "metadata": {}, 753 | "outputs": [], 754 | "source": [ 755 | "# export\n", 756 | "def _draw_shapes(plot, shapes, layer=(0, 0)):\n", 757 | " \"\"\"draw shapes with bokeh\n", 758 | "\n", 759 | " Args:\n", 760 | " plot: the plot to draw the shape in\n", 761 | " shapes: the shapes to draw\n", 762 | "\n", 763 | " Returns:\n", 764 | " the (inplace) modified plot containing the shape\n", 765 | " \"\"\"\n", 766 | " polys = []\n", 767 | " for shape in shapes:\n", 768 | " if shape.is_box():\n", 769 | " polys.append(_box_to_poly(shape.dbbox()))\n", 770 | " elif shape.is_path():\n", 771 | " polys.append(shape.dpath.polygon())\n", 772 | " elif shape.is_polygon():\n", 773 | " polys.append(shape.dpolygon)\n", 774 | " elif shape.is_simple_polygon():\n", 775 | " polys.append(shape.dsimple_polygon)\n", 776 | " #for poly in polys:\n", 777 | " # plot = draw_poly(plot, poly, layer=layer)\n", 778 | " return draw_polys(plot, polys, layer=layer)\n", 779 | "\n", 780 | "def draw_cell(plot, cell, draw_bbox=True):\n", 781 | " \"\"\"draw a cell with bokeh\n", 782 | "\n", 783 | " Args:\n", 784 | " plot: the plot to draw the cell in\n", 785 | " cell: the cell to draw\n", 786 | "\n", 787 | " Returns:\n", 788 | " the (inplace) modified plot containing the cell\n", 789 | " \"\"\"\n", 790 | " layout = pya.Layout()\n", 791 | " new_cell = layout.create_cell(cell.name)\n", 792 | " new_cell.copy_tree(cell)\n", 793 | " \n", 794 | " cell = new_cell\n", 795 | " cell = cell.flatten(-1, True)\n", 796 | " box = cell.dbbox()\n", 797 | " plot = adjust_plot(plot, box, max_dim=500)\n", 798 | " for lr in layout.layer_infos():\n", 799 | " shapes = [*cell.shapes(layout.layer(lr)).each()]\n", 800 | " plot = _draw_shapes(plot, shapes, layer=(lr.layer, lr.datatype))\n", 801 | " if draw_bbox:\n", 802 | " draw_box(plot, box)\n", 803 | " return plot" 804 | ] 805 | }, 806 | { 807 | "cell_type": "code", 808 | "execution_count": null, 809 | "id": "d6b5788c-17e6-4946-bbe6-86c53022edca", 810 | "metadata": {}, 811 | "outputs": [], 812 | "source": [ 813 | "# export\n", 814 | "def draw_inst(plot, inst, draw_bbox=True, draw_arrow=True):\n", 815 | " \"\"\"draw a instance with bokeh\n", 816 | "\n", 817 | " Args:\n", 818 | " plot: the plot to draw the instance in\n", 819 | " inst: the instance to draw\n", 820 | "\n", 821 | " Returns:\n", 822 | " the (inplace) modified plot with containing the instance\n", 823 | " \"\"\"\n", 824 | " _layout = pya.Layout()\n", 825 | " _cell = _layout.create_cell(inst.cell.name)\n", 826 | " _cell.copy_tree(inst.cell)\n", 827 | " _refcell = _layout.create_cell(f\"ref_{inst.cell.name}\")\n", 828 | " _refcell.insert(pya.CellInstArray(_layout.cell_by_name(inst.cell.name), inst.trans))\n", 829 | "\n", 830 | " plot = new_plot(_refcell.dbbox() + pya.Point(0, 0))\n", 831 | " plot = draw_vector(plot, inst.trans.disp)\n", 832 | " plot = draw_cell(plot, _refcell)\n", 833 | " plot = adjust_plot(plot, _refcell.dbbox() + pya.Point(0, 0))\n", 834 | " return plot" 835 | ] 836 | }, 837 | { 838 | "cell_type": "code", 839 | "execution_count": null, 840 | "id": "af153c3d-d908-48a9-9f20-ab2ba7c697bb", 841 | "metadata": {}, 842 | "outputs": [], 843 | "source": [ 844 | "# export\n", 845 | "def draw_layout(plot, layout):\n", 846 | " \"\"\"draw a layout with bokeh\n", 847 | "\n", 848 | " Args:\n", 849 | " plot: the plot to draw the layout in\n", 850 | " layout: the layout to draw\n", 851 | "\n", 852 | " Returns:\n", 853 | " the (inplace) modified plot with containing the layout\n", 854 | " \"\"\"\n", 855 | " plots = bp.Column(*[draw_cell(plot, cell) for cell in layout.top_cells()])\n", 856 | " return plots" 857 | ] 858 | } 859 | ], 860 | "metadata": { 861 | "kernelspec": { 862 | "display_name": "fl", 863 | "language": "python", 864 | "name": "fl" 865 | } 866 | }, 867 | "nbformat": 4, 868 | "nbformat_minor": 5 869 | } 870 | --------------------------------------------------------------------------------