Repository: SiEPIC/SiEPIC-Tools (language: python)

Issue:
SiEPIC Version check, layout_diff

siepic.extend.find_components function, subcells
Limitation of this function: 
- it requires that the DevRec and PinRec shapes be in the highest level of the component's hierarchy.  In the example below, the component was embedded inside another top cell with a different name. find_components does not find it, and connecting waveguides / components via script doesn't work.  
- considering adding "raise Exception", as this should only occur in coded layouts.

PDK developers should be aware of this limitation.  Update documentation.

<img width="470" alt="image" src="https://github.com/SiEPIC/SiEPIC-Tools/assets/15843200/722b3d3e-f12c-42ce-8a10-183d9ff8749d">

sliver in waveguides
Issue found by Susan Xu at CMC, where Bezier waveguides had 1 nm slivers at the ends:

![PHOTO-2024-07-08-15-09-38](https://github.com/SiEPIC/SiEPIC-Tools/assets/15843200/5a78f29c-e1fc-45e1-8fbd-20b1d61fd3fe)

Feature request: identify Pins facing the wrong way
Sometimes cells have the pins facing the wrong way in a component.  The Pin should be pointing outwards.  It would be nice to generate an error.

Solution:
- add a verification rule that checks if the first point in the PinRec path is inside the DevRec region



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/check-path-length.yml b/.github/workflows/check-path-length.yml
new file mode 100644
index 000000000..fcf27498c
--- /dev/null
+++ b/.github/workflows/check-path-length.yml
@@ -0,0 +1,39 @@
+# .github/workflows/check-path-length.yml
+name: Check Path Length (200 limit)
+
+on: 
+  push:
+  pull_request:
+
+jobs:
+  check-path-length:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Check file path lengths
+        run: |
+          # Set the maximum allowed length
+          MAX_LENGTH=200
+          # Find all files in the repository and check their path lengths
+          too_long_paths=0
+
+          # Loop through each file path found by find
+          IFS=$'\n'  # Set Internal Field Separator to newline to handle spaces in filenames
+          for file in $(find . -type f); do
+              length=${#file}
+              if (( length > MAX_LENGTH )); then
+                  echo "Path too long: $file ($length characters)"
+                  too_long_paths=$((too_long_paths + 1))
+              fi
+          done
+
+          if (( too_long_paths > 0 )); then
+            echo "Error: Found $too_long_paths file paths longer than $MAX_LENGTH characters."
+            exit 1
+          else
+            echo "All file paths are within the $MAX_LENGTH character limit."
+          fi
+      
\ No newline at end of file
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 6debfcb76..ce85b1f47 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -21,7 +21,8 @@ env:
 
 jobs:
   build-and-push-image:
-  
+    if: github.repository_owner == 'SiEPIC'
+
     runs-on: ubuntu-latest
     permissions:
       contents: read
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 000000000..a8d812f29
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,55 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload Python Package
+
+on:
+  release:
+    types: [published]
+
+permissions:
+  contents: read
+
+jobs:
+  deploy:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python
+      uses: actions/setup-python@v3
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install build
+    - name: Build package
+      run: |
+        # python -m build
+        cd klayout_dot_config/python
+        python3 -m build
+        cp -a dist ../..
+    - name: Publish package
+      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+      with:
+        user: __token__
+        password: ${{ secrets.PYPI_API_TOKEN }}
+
+
+    - name: Publish on KLayout Salt server
+      run: |
+          # Extract the tag from GITHUB_REF (e.g., 'refs/tags/v1.0.0' -> 'v1.0.0')
+          RELEASE_TAG=${GITHUB_REF#refs/tags/}
+          echo "Release tag: $RELEASE_TAG"
+          
+          # Use the tag in your script
+          # ./your-script.sh "$RELEASE_TAG"
+      
+          curl "https://sami.klayout.org/register_request?agree_terms=1&mail=lukasc%40ece.ubc.ca&provider=5&url_path=siepic%2FSiEPIC-Tools.git%2Ftags%2F$RELEASE_TAG%2Fklayout_dot_config&user=Lukas+Chrostowski"
diff --git a/.gitignore b/.gitignore
index a881e46a9..9eaf4f6bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 klayoutrc
+*.lyrdb
 
 # Byte-compiled / optimized / DLL files
 __pycache__/
diff --git a/Dockerfile b/Dockerfile
index f6041b7b8..2e6096142 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,10 @@
 FROM quay.io/centos/centos:stream8
 
+# CentOS 8 reached EOL Dec 31, 2021. Therefore, mirrors need to be change to the following
+RUN cd /etc/yum.repos.d/
+RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
+RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
+
 # Update the system and install necessary tools.
 RUN dnf -y update && \
     dnf -y install wget bzip2 unzip git mesa-dri-drivers python3 python3-pip
diff --git a/Documentation/KLayout-SiEPIC-Tools Python Programming.ipynb b/Documentation/KLayout-SiEPIC-Tools Python Programming.ipynb
new file mode 100644
index 000000000..ab1fee0c6
--- /dev/null
+++ b/Documentation/KLayout-SiEPIC-Tools Python Programming.ipynb
@@ -0,0 +1,370 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "224a47cb",
+   "metadata": {},
+   "source": [
+    "# Creating layouts by programming KLayout in Python\n",
+    "This tutorial is based on an open source application for creating layouts (KLayout), and open source set of helper scripts in Python (SiEPIC-Tools), and an open source Process Design Kit (PDK) with technology files (SiEPIC-EBeam-PDK).\n",
+    "\n",
+    "Note this notebook is for educational purposes. Copy and paste the necessary code segments into your own Python project, using your favourite development environment, e.g., VSCode, Spyder, Jupyter notebook, KLayout's embedded Python, etc.\n",
+    "\n",
+    "### Software installation:\n",
+    "\n",
+    "***KLayout***\n",
+    "\n",
+    "> pip install klayout\n",
+    "\n",
+    "***SiEPIC-Tools***\n",
+    "\n",
+    "> pip install SiEPIC\n",
+    "\n",
+    "***SiEPIC-EBeam PDK***\n",
+    "\n",
+    "> pip install siepic_ebeam_pdk\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7a1042dd",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "!pip install klayout --upgrade\n",
+    "!pip install SiEPIC  --upgrade\n",
+    "!pip install siepic_ebeam_pdk  --upgrade"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f5ca9186",
+   "metadata": {},
+   "source": [
+    "# Tutorial\n",
+    "\n",
+    "### Create a new layout\n",
+    " - technology: EBeam\n",
+    " - top cell name: Top\n",
+    " - floor plan dimensions: 605,000 x 410,000 nm = 605 x 410 µm\n",
+    " - database units (1 unit = 0.001 micron = 1 nm, configured in the EBeam technology)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "3ce45d0b",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "KLayout SiEPIC-Tools version 0.5.11\n",
+      "SiEPIC-EBeam-PDK Python module: siepic_ebeam_pdk, KLayout technology: EBeam\n",
+      "No libraries associated to EBeam technology\n",
+      "Loaded technology libraries: ['Basic', 'EBeam', 'EBeam-ANT', 'EBeam-Dream', 'EBeam-SiN', 'EBeam_Beta']\n"
+     ]
+    }
+   ],
+   "source": [
+    "import pya # KLayout Python API\n",
+    "import SiEPIC  # import module for SiEPIC-Tools, helper functions for KLayout\n",
+    "import siepic_ebeam_pdk  # import module for the SiEPIC-EBeam-PDK technology\n",
+    "from SiEPIC.utils.layout import new_layout, floorplan\n",
+    "\n",
+    "tech_name, top_cell_name = 'EBeam', 'Top'\n",
+    "topcell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)\n",
+    "floorplan(topcell, 605e3, 410e3)\n",
+    "dbu = ly.dbu"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1c264a9f",
+   "metadata": {},
+   "source": [
+    "### Create a blank cell in the layout\n",
+    "- name: cell\n",
+    "- Instantiate it as a subcell of the top cell at position (0,0) database units\n",
+    "- record the instance in a variable (inst_cell) so we can manipulate or query it later if necessary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "fb55cd8f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pya  # import module for KLayout\n",
+    "\n",
+    "subcell = ly.create_cell('cell')\n",
+    "t = pya.Trans(pya.Trans.R0, 0,0)\n",
+    "inst_cell = topcell.insert(pya.CellInstArray(subcell.cell_index(), t))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1cf4ae2d",
+   "metadata": {},
+   "source": [
+    "### Place cells from the PDK\n",
+    "- use SiEPIC create_cell2 function, which is an enhanced version (error checking) of pya.Layout.create_cell\n",
+    "- Instantiate it in the subcell at position (40 µm, 15 µm) or (40e3, 15e3) database units\n",
+    "- record the instance in a variable (inst_gc1) so we can manipulate or query it later if necessary"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "1fa6c5cc",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<IPython.core.display.Image object>"
+      ]
+     },
+     "metadata": {
+      "image/png": {
+       "height": 271,
+       "width": 400
+      }
+     },
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<IPython.core.display.Image object>"
+      ]
+     },
+     "metadata": {
+      "image/png": {
+       "height": 271,
+       "width": 400
+      }
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from SiEPIC.utils import create_cell2\n",
+    "cell = create_cell2(ly, 'GC_TE_1310_8degOxide_BB', 'EBeam')\n",
+    "t = pya.Trans(pya.Trans.R0, 40e3,15e3)\n",
+    "inst_gc1 = topcell.insert(pya.CellInstArray(cell.cell_index(), t))\n",
+    "\n",
+    "t = pya.Trans(pya.Trans.R0, 40e3,15e3+127e3)\n",
+    "inst_gc2 = topcell.insert(pya.CellInstArray(cell.cell_index(), t))\n",
+    "\n",
+    "cell.plot(width = 400)\n",
+    "topcell.plot(width = 400)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "502aa6ce",
+   "metadata": {},
+   "source": [
+    "## Components and their pins\n",
+    "- Query a cell to get the SiEPIC Component description\n",
+    "- Get the pins associated with this Component\n",
+    "- Alternative: Query a cell to get a list of Pins"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "1725d0b8",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Display component details:\n",
+      "<class 'SiEPIC.core.Component'>\n",
+      "- basic_name: GC_TE_1310_8degOxide_BB, component: GC_TE_1310_8degOxide_BB-0 / GC_TE_1310_8degOxide_BB; transformation: r0 *1 0,0; center position: -19.985,0.084; number of pins: 2; optical pins: [['opt1', '0,0', None]]; electrical pins: []; optical IO pins: [['GC_TE_1310_8degOxide_BB', '-20400,0', None]]; has compact model: False; params: .\n",
+      "\n",
+      "Display pin details:\n",
+      "- pin_name opt1: component_idx 0, pin_type 1, rotation: 0.0, net: None, (0,0), path: (-10,0;10,0) w=350 bx=0 ex=0 r=false\n",
+      "- pin_name GC_TE_1310_8degOxide_BB: component_idx 0, pin_type 0, rotation: 0, net: None, (-20400,0), path: None\n",
+      "\n",
+      "Display pin names:\n",
+      " -  opt1\n"
+     ]
+    }
+   ],
+   "source": [
+    "\n",
+    "print('Display component details:')\n",
+    "component = cell.find_components()[0]  # find component within a cell, return the first one\n",
+    "print(type(component))\n",
+    "component.display()\n",
+    "\n",
+    "print('\\nDisplay pin details:')\n",
+    "pins = cell.find_pins_component(component)\n",
+    "for p in pins:\n",
+    "    p.display()\n",
+    "    \n",
+    "# or only pins\n",
+    "print('\\nDisplay pin names:')\n",
+    "pins, _ = cell.find_pins()\n",
+    "for p in pins:\n",
+    "    if p.type == 1:  # optical pin type, defined in SiEPIC._globals\n",
+    "        print(' - ', p.pin_name)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "166e3def",
+   "metadata": {},
+   "source": [
+    "## Waveguides\n",
+    "- Find out what types of waveguides are defined in the PDK\n",
+    "- Connect two components using a waveguide\n",
+    "- Specify the waveguide type\n",
+    "- Specify the path the waveguide should take\n",
+    "- Return the wavelength details (length)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "d01e8e3e",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Waveguide types:\n",
+      "  Strip TE 1550 nm, w=500 nm\n",
+      "  Strip TE 1310 nm, w=410 nm\n",
+      "  Strip TE 1310 nm, w=350 nm\n",
+      "  Strip TM 1550 nm, w=500 nm\n",
+      "  Strip TE-TM 1550, w=450 nm\n",
+      "  Multimode Strip TE 1550 nm, w=2000 nm\n",
+      "  Multimode Strip TE 1550 nm, w=3000 nm\n",
+      "  Slot TE 1550 nm, w=500 nm, gap=100nm\n",
+      "  eskid TE 1550\n",
+      "  Rib (90 nm slab) TE 1550 nm, w=500 nm\n",
+      "  Rib (90 nm slab) TE 1310 nm, w=350 nm\n",
+      "  Si routing TE 1550 nm (compound waveguide)\n",
+      "  Si routing TE 1310 nm (compound waveguide)\n",
+      "  SiN Strip TE 895 nm, w=450 nm\n",
+      "  SiN Strip TE 1550 nm, w=750 nm\n",
+      "  SiN Strip TE 1550 nm, w=800 nm\n",
+      "  SiN Strip TE 1550 nm, w=1000 nm\n",
+      "  SiN Strip TM 1550 nm, w=1000 nm\n",
+      "  SiN Strip TE 1310 nm, w=750 nm\n",
+      "  SiN Strip TM 1310 nm, w=750 nm\n",
+      "  Multimode SiN Strip TE 1550 nm, w=3000 nm\n",
+      "  SiN routing TE 1550 nm (compound waveguide)\n",
+      "190.793\n"
+     ]
+    },
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<IPython.core.display.Image object>"
+      ]
+     },
+     "metadata": {
+      "image/png": {
+       "height": 549,
+       "width": 800
+      }
+     },
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Message from klive: {\"version\": \"0.2.2\", \"type\": \"open\", \"file\": \"/var/folders/2_/hfctv7kn7h965wdl4jgrm8t80000gn/T/tmp9z98hcki/Top.gds\"}\n"
+     ]
+    }
+   ],
+   "source": [
+    "from SiEPIC.utils import load_Waveguides_by_Tech\n",
+    "waveguide_types = load_Waveguides_by_Tech(tech_name)\n",
+    "\n",
+    "print('Waveguide types:')\n",
+    "for w in waveguide_types:\n",
+    "    print('  %s' % w['name'])\n",
+    "wg_type = waveguide_types[0]['name']\n",
+    "\n",
+    "from SiEPIC.scripts import connect_pins_with_waveguide\n",
+    "wg = connect_pins_with_waveguide(inst_gc1, 'opt1', inst_gc2, 'opt1', \n",
+    "                                    turtle_A=[5, -90, \n",
+    "                                              20, 90, 10, 90, 1, 90],\n",
+    "                                    waveguide_type=wg_type,\n",
+    "                                )\n",
+    "\n",
+    "from SiEPIC.utils import waveguide_length\n",
+    "print(waveguide_length(wg))\n",
+    "\n",
+    "topcell.plot() # in the browser\n",
+    "topcell.show() # in KLayout GUI\n",
+    "        "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9648c84c",
+   "metadata": {},
+   "source": [
+    "## Developers\n",
+    "- Install the tools and PDKs by creating a copy using GitHub Desktop, then installing as a symbolic link in pip."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ff650ee7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "!python -m pip install -e '/Users/lukasc/Documents/GitHub/SiEPIC-Tools/klayout_dot_config/python'\n",
+    "!python -m pip install -e '/Users/lukasc/Documents/GitHub/SiEPIC_EBeam_PDK/klayout'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "36268e1c",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.13"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/README.md b/README.md
index 84a974554..1531258db 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
   - Lukas Chrostowski, Hossam Shoman, Mustafa Hammood, Han Yun,  Jaspreet Jhoja, Enxiao Luan,  Stephen Lin, Ajay Mistry, Donald Witt, Nicolas A. F. Jaeger, Sudip Shekhar,  Hasitha Jayatilleka, Philippe Jean, Simon B.-de Villers, Jonathan Cauchon, Wei Shi,  Cameron Horvath, Jocelyn N. Westwood-Bachman, Kevin Setzer, Mirwais Aktary, N. Shane Patrick, Richard Bojko, Amin Khavasi, Xu Wang, Thomas Ferreira de Lima,  Alexander N. Tait, Paul R. Prucnal, David E. Hagan, Doris Stevanovic, Andy P. Knights, "<a href="https://doi.org/10.1109/JSTQE.2019.2917501">Silicon Photonic Circuit Design Using Rapid Prototyping Foundry Process Design Kits</a>" IEEE Journal of Selected Topics in Quantum Electronics, Volume: 25, Issue: 5, Sept.-Oct. 2019. (<a href="https://www.dropbox.com/s/i1z4ackr3q7fz1l/2019_JSTQE_foundry.pdf?dl=1">PDF</a>)
 
 ## Download and Installation instructions:
- - in KLayout v0.27, use Tools | Package Manager, and find SiEPIC-Tools there (more details in the [Wiki Instructions](https://github.com/SiEPIC/SiEPIC-Tools/wiki/Installation))
+ - in KLayout (version 0.27 or greater, preferably 0.29), use Tools | Package Manager, and find SiEPIC-Tools there (more details in the [Wiki Instructions](https://github.com/SiEPIC/SiEPIC-Tools/wiki/Installation))
  - install PDK, e.g., <a href="https://github.com/siepic/SiEPIC_EBeam_PDK/wiki/Installation-instructions">SiEPIC_EBeam_PDK download and installation instructions</a> on the wiki page.  
 
  
diff --git a/klayout_dot_config/grain.xml b/klayout_dot_config/grain.xml
index 69e3a64bf..e19adfc87 100644
--- a/klayout_dot_config/grain.xml
+++ b/klayout_dot_config/grain.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <salt-grain>
  <name>siepic_tools</name>
- <version>0.5.5</version>
+ <version>0.5.16</version>
  <api-version>0.27</api-version>
  <title>SiEPIC Tools</title>
  <doc>Tools for designing Silicon Photonic Integrated Circuits, including waveguides, component simulations, functional verification, DRC verification, Functional verification, netlist extraction, circuit simulations. Layout can be implemented graphically or by programming in Python using the SiEPIC functions and KLayout Python API. Framework and examples for creating layouts using scripts. Includes a generic PDK (GSiP). Other PDKs are installed separately, and depend on SiEPIC-Tools.</doc>
diff --git a/klayout_dot_config/pymacros/Keybindings/Layout/Show component information.lym b/klayout_dot_config/pymacros/Keybindings/Layout/Show component information.lym
index f3f74f3d3..d9273f894 100644
--- a/klayout_dot_config/pymacros/Keybindings/Layout/Show component information.lym
+++ b/klayout_dot_config/pymacros/Keybindings/Layout/Show component information.lym
@@ -77,7 +77,7 @@ def show_component_info():
     text += ('&lt;br&gt;')
     text += "  selected cell name: %s" % obj.inst().cell.name
     text += ('&lt;br&gt;')
-    c = cell.find_components(cell_selected=[obj.inst().cell],verbose=True)
+    c = cell.find_components(cell_selected=tuple([obj.inst().cell]),verbose=True, raiseException = False)
     if c and c[0].cell:
       text += c[0].display().replace(';','&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;')
       if c[0].cell.is_pcell_variant():
diff --git a/klayout_dot_config/pymacros/Keybindings/Verification/Functional Layout Check.lym b/klayout_dot_config/pymacros/Keybindings/Verification/Functional Layout Check.lym
index 552cc1740..a461c1312 100644
--- a/klayout_dot_config/pymacros/Keybindings/Verification/Functional Layout Check.lym
+++ b/klayout_dot_config/pymacros/Keybindings/Verification/Functional Layout Check.lym
@@ -21,15 +21,16 @@
 import pya
 from SiEPIC import verification
 
-#verification.layout_check(verbose=True, GUI=True)
-
-import cProfile
-cProfile.run('verification.layout_check(verbose=False, GUI=True)','/tmp/restats')
-import pstats
-from pstats import SortKey
-p = pstats.Stats('/tmp/restats')
-p.sort_stats(SortKey.CUMULATIVE).print_stats(20)
-p.sort_stats(SortKey.TIME).print_stats(20)
+if 1:
+    verification.layout_check(verbose=True, GUI=True)
+else:
+    import cProfile
+    cProfile.run('verification.layout_check(verbose=False, GUI=True)','/tmp/restats')
+    import pstats
+    from pstats import SortKey
+    p = pstats.Stats('/tmp/restats')
+    p.sort_stats(SortKey.CUMULATIVE).print_stats(20)
+    p.sort_stats(SortKey.TIME).print_stats(20)
 
 </text>
 </klayout-macro>
diff --git a/klayout_dot_config/python/SiEPIC/__init__.py b/klayout_dot_config/python/SiEPIC/__init__.py
index 36c202e39..ac7f953c9 100644
--- a/klayout_dot_config/python/SiEPIC/__init__.py
+++ b/klayout_dot_config/python/SiEPIC/__init__.py
@@ -2,7 +2,7 @@
 SiEPIC-Tools package for KLayout
 '''
 
-__version__ = '0.5.5'
+__version__ = "0.5.16"
 
 print("KLayout SiEPIC-Tools version %s" %__version__)
 
@@ -24,4 +24,19 @@
     if _globals.Python_Env == "KLayout_GUI":
         from . import extend, _globals, core, examples, github, scripts, utils, setup, install, verification
     else:
-        from . import extend, _globals, verification
+        from . import _globals, core, utils, extend, verification, scripts
+        
+
+
+try:
+    # Start timer
+    import time
+    start_time = time.time()
+    from .scripts import version_check
+
+    version_check()
+
+    execution_time = time.time() - start_time
+    print(f"Version check, time: {execution_time} seconds")
+except:
+    pass
diff --git a/klayout_dot_config/python/SiEPIC/core.py b/klayout_dot_config/python/SiEPIC/core.py
index 720090ae7..c2033eff6 100644
--- a/klayout_dot_config/python/SiEPIC/core.py
+++ b/klayout_dot_config/python/SiEPIC/core.py
@@ -131,8 +131,12 @@ def transform(self, trans):
 
     def display(self):
         p = self
-        print("- pin_name %s: component_idx %s, pin_type %s, rotation: %s, net: %s, (%s), path: %s" %
-              (p.pin_name, p.component.idx, p.type, p.rotation, p.net.idx, p.center, p.path))
+        if p.component:
+            print("- pin_name %s: component_idx %s, pin_type %s, rotation: %s, net: %s, (%s), path: %s" %
+                  (p.pin_name, p.component.idx, p.type, p.rotation, p.net.idx, p.center, p.path))
+        else:
+            print("- pin_name %s: pin_type %s, rotation: %s, net: %s, (%s), path: %s" %
+                  (p.pin_name, p.type, p.rotation, p.net.idx, p.center, p.path))
         o = self
 #        print("- pin #%s: component_idx %s, pin_name %s, pin_type %s, net: %s, (%s), path: %s" %
 #              (o.idx, o.component_idx, o.pin_name, o.type, o.net.idx, o.center, o.path))
@@ -303,7 +307,7 @@ def set_SPICE_params(self, arg, verbose = False):
         self.params = spice_str
         return True
         
-    def get_SPICE_params(self): #Retturns a SPICE parameter string (without the 'Spice_param:' label)
+    def get_SPICE_params(self): #Returns a SPICE parameter string (without the 'Spice_param:' label)
         return (self.params)
       
     def find_pins(self):
diff --git a/klayout_dot_config/python/SiEPIC/extend.py b/klayout_dot_config/python/SiEPIC/extend.py
index 58438e8d0..7c2cfa93b 100644
--- a/klayout_dot_config/python/SiEPIC/extend.py
+++ b/klayout_dot_config/python/SiEPIC/extend.py
@@ -49,6 +49,7 @@
   - spice_netlist_export
   - check_component_models
   - pinPoint
+  - plot: display an image for Jupyter notebook
 
 pya.Instance Extensions:
   - find_pins: find Pin objects for all pins in a cell instance
@@ -685,6 +686,11 @@ def print_parameter_values(self):
 def find_pins(self, verbose=False, polygon_devrec=None, GUI=False):
     '''
     Find Pins in a Cell.
+    Inputs:
+        self: pya.Cell
+        verbose: True prints details for debugging
+        polygon_devrec:
+
     Optical Pins have:
      1) path on layer PinRec, indicating direction (out of component)
      2) text on layer PinRec, inside the path
@@ -696,6 +702,11 @@ def find_pins(self, verbose=False, polygon_devrec=None, GUI=False):
     Electrical Pins have:
      1) box on layer PinRec
      2) text on layer PinRec, inside the box
+     
+    Returns:
+        pins: SiEPIC.core.Pin
+        pin_errors: text
+
     '''
 
     if verbose:
@@ -749,10 +760,14 @@ def find_pins(self, verbose=False, polygon_devrec=None, GUI=False):
             while not(iter2.at_end()):
                 if iter2.shape().is_text():
                     pin_name = iter2.shape().text.string
-                iter2.next()
                 if pin_name and pin_path.num_points()==2:
-                  # Store the pin information in the pins array
-                  pins.append(Pin(path=pin_path, _type=_globals.PIN_TYPES.OPTICAL, pin_name=pin_name))
+                    # make sure that the Pin's path and text are in the same cell:
+                    if it.shape().cell.name == iter2.shape().cell.name:
+                        # Store the pin information in the pins array
+                        pins.append(Pin(path=pin_path, _type=_globals.PIN_TYPES.OPTICAL, pin_name=pin_name))
+                        if verbose:
+                            print(' - found pin: %s in cell %s, in %s, text %s' % (pin_name, subcell.name, it.shape().cell.name, iter2.shape().cell.name ))
+                iter2.next()
             if pin_name == None or pin_path.num_points()!=2:
                 print("Invalid pin Path detected: %s. Cell: %s" % (pin_path, subcell.name))
                 error_text += ("Invalid pin Path detected: %s, in Cell: %s, Optical Pins must have a pin name.\n" %
@@ -878,7 +893,22 @@ def find_pins(self, verbose=False, polygon_devrec=None, GUI=False):
     return pins, pin_errors
 
 
-def find_pin(self, name):
+def find_pin(self, pin_name, verbose=False):
+
+    pins, _ = self.find_pins()
+
+    if pins:
+        p = [ p for p in pins if (p.pin_name==pin_name)]
+        if p:
+            if len(p)>1:
+                raise Exception ('Multiple Pins with name "%s" found in cell "%s"' % (pin_name, self.basic_name()) )
+            return p[0]
+        else:
+            raise Exception ('Pin with name "%s" not found in cell "%s". Available pins: %s' % (pin_name, self.basic_name(), [p.pin_name for p in pins]) )
+    else:
+        raise Exception ('No Pins found in cell "%s"' % (self.basic_name()) )
+    '''
+    (self, name):
     from . import _globals
     from .core import Pin
     pins = []
@@ -902,6 +932,8 @@ def find_pin(self, name):
             return Pin(pin, _globals.PIN_TYPES.OPTICAL)
 
     return None
+    '''
+
 
 # find the pins inside a component
 
@@ -917,7 +949,9 @@ def find_pins_component(self, component):
 '''
 Components:
 '''
-def find_components(self, cell_selected=None, inst=None, verbose=False):
+from functools import lru_cache
+@lru_cache(maxsize=None)
+def find_components(self, cell_selected=None, inst=None, verbose=False, raiseException = True):
     '''
     Function to traverse the cell's hierarchy and find all the components
     returns list of components (class Component)
@@ -933,13 +967,25 @@ def find_components(self, cell_selected=None, inst=None, verbose=False):
     cell_selected: only find components that match this specific cell.
     
     inst: return only the component that matches the instance inst
+    
+    raiseException: False turns of exception handling, and returns None instead
+    
+    limitation:
+     - flat components only. doesn't find the component if it is buried in a hierarchy
+     - no function for instance.find_components.  Instead we find based on cell, then try to match it to the requested instance.
 
     '''
+    
+    if cell_selected != None and type(cell_selected) != type([]) and type(cell_selected) != tuple:
+          cell_selected=[cell_selected]
+
     if verbose:
         print('*** Cell.find_components:')
+        if cell_selected[0]:
+          print('  - cell_selected=%s' % (cell_selected[0].name if cell_selected[0] else None))
+        if inst:
+          print('  - inst=%s' % (inst.cell.name))
 
-    if cell_selected != None and type(cell_selected) != type([]):
-          cell_selected=[cell_selected]
 
     components = []
 
@@ -968,8 +1014,12 @@ def find_components(self, cell_selected=None, inst=None, verbose=False):
         idx = len(components)  # component index value to be assigned to Component.idx
         component_ID = idx
         subcell = iter1.cell()  # cell (component) to which this shape belongs
+        if verbose:
+          print(' - looking at shape in cell %s. ' % subcell.name)
         if cell_selected and not subcell in cell_selected:
             # check if subcell is one of the arguments to this function: cell_selected
+            if verbose:
+              print(' - cell_selected and not subcell (%s) in cell_selected (%s). ' % (subcell.name, cell_selected[0].name))
             iter1.next()
             continue
         component = subcell.basic_name().replace(' ', '_')   # name library component
@@ -985,6 +1035,10 @@ def find_components(self, cell_selected=None, inst=None, verbose=False):
             polygon = pya.Polygon(box)  # Save the component outline polygon
             DevRec_polygon = pya.Polygon(iter1.shape().box)
             found_component = True
+        if iter1.shape().is_path():
+            polygon = iter1.shape().path.polygon().transformed(iter1.itrans())  # Save the component outline polygon
+            DevRec_polygon = iter1.shape().path.polygon
+            found_component = True
         if iter1.shape().is_polygon():
             polygon = iter1.shape().polygon.transformed(iter1.itrans())  # Save the component outline polygon
             DevRec_polygon = iter1.shape().polygon
@@ -1095,6 +1149,11 @@ def find_components(self, cell_selected=None, inst=None, verbose=False):
     if component_matched:
         return component_matched
     
+    if components == []:
+        if raiseException:
+            raise Exception ('SiEPIC.extend.find_components: No component found for cell_selected=%s' % (cell_selected[0].name if cell_selected else None))
+        else:
+            return None
     return components
 # end def find_components
 
@@ -1184,9 +1243,9 @@ def get_LumericalINTERCONNECT_analyzers(self, components, verbose=None):
     topcell = self
 
     from . import _globals
-    from .utils import select_paths, get_technology
+    from .utils import select_paths, get_technology_by_name
     from .core import Net
-    TECHNOLOGY = get_technology()
+    TECHNOLOGY = get_technology_by_name(self.layout().technology().name)
 
     layout = topcell.layout()
     LayerLumericalN = self.layout().layer(TECHNOLOGY['Lumerical'])
@@ -1283,14 +1342,19 @@ def get_LumericalINTERCONNECT_analyzers_from_opt_in(self, components, verbose=No
     from .core import Net
 
     from SiEPIC.utils import load_DFT
-    DFT = load_DFT()
+    from .utils import get_technology_by_name
+    if 'TECHNOLOGY' in dir(self.layout()):
+        TECHNOLOGY = self.layout().TECHNOLOGY
+    else:
+        TECHNOLOGY = get_technology_by_name(self.layout().technology().name)
+    DFT = load_DFT(TECHNOLOGY)
     if not DFT:
         if verbose:
             print(' no DFT rules available.')
         return False, False, False, False, False, False, False, False
 
     from .scripts import user_select_opt_in
-    opt_in_selection_text, opt_in_dict = user_select_opt_in(
+    opt_in_selection_text, opt_in_dict = user_select_opt_in(cell=self,
         verbose=verbose, option_all=False, opt_in_selection_text=opt_in_selection_text)
     if not opt_in_dict:
         if verbose:
@@ -1404,8 +1468,8 @@ def spice_netlist_export(self, verbose=False, opt_in_selection_text=[]):
     from time import strftime
     from .utils import eng_str
 
-    from .utils import get_technology
-    TECHNOLOGY = get_technology()
+    from .utils import get_technology_by_name
+    TECHNOLOGY = get_technology_by_name(self.layout().technology().name)
     if not TECHNOLOGY['technology_name']:
         v = pya.MessageBox.warning("Errors", "SiEPIC-Tools requires a technology to be chosen.  \n\nThe active technology is displayed on the bottom-left of the KLayout window, next to the T. \n\nChange the technology using KLayout File | Layout Properties, then choose Technology and find the correct one (e.g., EBeam, GSiP).", pya.MessageBox.Ok)
         return '', '', 0, []
@@ -1687,10 +1751,72 @@ def pinPoint(self, pin_name, verbose=False):
             raise Exception("Did not find matching pin (%s), in the components list of pins (%s)." %(pin_name, [p.pin_name for p in pins]) )
             return
         return matched_pins[0].center
-    else:
-        pass
 
 
+def show(self):
+    '''Show the cell in KLayout using klive'''
+
+    # Save the cell in a temporary file
+    from ._globals import TEMP_FOLDER
+    import os
+    file_out = os.path.join(TEMP_FOLDER, self.name+'.gds')
+    self.write(file_out)
+
+    # Display in KLayout
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, technology=self.layout().technology().name, keep_position=True)
+
+def plot(self, width = 800, show_labels = True, show_ruler = True, retina = True):
+        '''
+        Generate an image of the layout cell, and display. Useful for Jupyter notebooks
+        
+        Args:
+            self: pya.Cell
+            width: number of pixels
+            show_labels: KLayout display config to show text = True, https://www.klayout.de/doc-qt5/code/class_LayoutView.html#method101
+            show_ruler: KLayout display config to show ruler = True
+            retina: IPython.display.Image configuration for retina display, True
+        '''
+        
+        from io import BytesIO
+        from IPython.display import Image, display
+        
+        # Create a LayoutView, and populate it with the current cell & layout
+        cell = self
+        layout_view = pya.LayoutView()
+        cell_view_index = layout_view.create_layout(True)
+        layout_view.active_cellview_index = cell_view_index
+        cell_view = layout_view.cellview(cell_view_index)
+        layout = cell_view.layout()
+        layout.assign(cell.layout())
+        cell_view.cell = layout.cell(cell.name)
+
+        # Load layer properties from the technology
+        lyp_path=layout.technology().eff_layer_properties_file()
+        if not lyp_path:
+            raise Exception ('SiEPIC.extend.plot: technology not specified.')
+        layout_view.load_layer_props(lyp_path)
+        
+        # Configure the layout view settings
+        # print(layout_view.get_config_names())
+        layout_view.set_config("text-font",3)        
+        layout_view.set_config("background-color", "#ffffff")
+        layout_view.set_config("text-visible", "true" if show_labels else "false")
+        layout_view.set_config("grid-show-ruler", "true" if show_ruler else "false")
+
+        # Zoom out and show all layout details
+        layout_view.max_hier()
+        layout_view.zoom_fit()
+
+        # Display as a PNG
+        width = width * (2 if retina else 1)
+        pixel_buffer = layout_view.get_pixels(width, cell.bbox().height()/cell.bbox().width()*width)
+        png_data = pixel_buffer.to_png_data()
+        im = Image(png_data, retina=retina)
+        display(im)
+
 #################################################################################
 
 pya.Cell.print_parameter_values = print_parameter_values
@@ -1703,6 +1829,8 @@ def pinPoint(self, pin_name, verbose=False):
 pya.Cell.get_LumericalINTERCONNECT_analyzers_from_opt_in = get_LumericalINTERCONNECT_analyzers_from_opt_in
 pya.Cell.spice_netlist_export = spice_netlist_export
 pya.Cell.pinPoint = pinPoint
+pya.Cell.show = show
+pya.Cell.plot = plot
 
 
 #################################################################################
@@ -1750,7 +1878,7 @@ def find_pin(self, pin_name, verbose=False):
                 raise Exception ('Multiple Pins with name "%s" found in cell "%s"' % (pin_name, self.cell.basic_name()) )
             return p[0]
         else:
-            raise Exception ('Pin with name "%s" not found in cell "%s"' % (pin_name, self.cell.basic_name()) )
+            raise Exception ('Pin with name "%s" not found in cell "%s". Available pins: %s' % (pin_name, self.cell.basic_name(), [p.pin_name for p in pins]) )
     else:
         raise Exception ('No Pins found in cell "%s"' % (self.cell.basic_name()) )
 
diff --git a/klayout_dot_config/python/SiEPIC/lumerical/ApodizedContraDC.py b/klayout_dot_config/python/SiEPIC/lumerical/ApodizedContraDC.py
index 38d2ecca3..64bd42ec6 100644
--- a/klayout_dot_config/python/SiEPIC/lumerical/ApodizedContraDC.py
+++ b/klayout_dot_config/python/SiEPIC/lumerical/ApodizedContraDC.py
@@ -57,7 +57,7 @@ def TOP():
 
   print(params)
 
-            if instance.cell.basic_name() == "Waveguide":
+  #          if instance.cell.basic_name() == "Waveguide":
   
   # check if selected PCell is a contra DC
   if "component: ebeam_contra_dc" not in text:
diff --git a/klayout_dot_config/python/SiEPIC/opics/components.py b/klayout_dot_config/python/SiEPIC/opics/components.py
index b99408dc4..79b4de796 100644
--- a/klayout_dot_config/python/SiEPIC/opics/components.py
+++ b/klayout_dot_config/python/SiEPIC/opics/components.py
@@ -62,9 +62,10 @@ def __init__(
         self.sparam_file = filename
 
         for key, value in kwargs.items():
-            self.componentParameters.append([key, str(value)])
+            self.componentParameters.append([key, value])
 
-    def load_sparameters(self, data_folder: PosixPath, filename: str) -> ndarray:
+    def load_sparameters(self, data_folder: PosixPath, filename: str, 
+            verbose: bool = True) -> ndarray:
         """
         Loads sparameters either from an npz file or from a raw sparam\
              file using a look-up table.
@@ -90,6 +91,7 @@ def load_sparameters(self, data_folder: PosixPath, filename: str) -> ndarray:
                 self.componentParameters,
                 self.nports,
                 self.sparam_attr,
+                verbose = verbose
             )
             return self.interpolate_sparameters(
                 self.f, componentData[0], componentData[1]
diff --git a/klayout_dot_config/python/SiEPIC/opics/utils.py b/klayout_dot_config/python/SiEPIC/opics/utils.py
index 0f72a4254..07d6bad38 100644
--- a/klayout_dot_config/python/SiEPIC/opics/utils.py
+++ b/klayout_dot_config/python/SiEPIC/opics/utils.py
@@ -188,8 +188,28 @@ def LUT_reader(filedir: PosixPath, lutfilename: str, lutdata: List[List[str]]):
     xml = parse(filedir / lutfilename)
     root = xml.getroot()
 
+    if "contraDC" in lutfilename:
+        print(1)
+
     for node in root.iter("association"):
         sample = [[each.attrib["name"], each.text] for each in node.iter("value")]
+        '''
+        Look-up is not working. returns the last one each time.
+        Attempt to fix it.. gave up...
+        
+        sample = [[each.attrib["name"], float(each.text) if each.attrib["type"]=='double' else int(each.text) if each.attrib["type"]=='int' else bool(each.text) if each.attrib["type"]=='bool' else  each.text, each.attrib["type"]] for each in node.iter("value")]    
+
+        # Make sure the two data sets have the same datatypes
+        for q in lutdata:
+            w = [r for r in sample if r[0]==q[0]] [0]
+            if w[2] == 'double':
+                q[1] = float(q[1])
+            if w[2] == 'bool':
+                q[1] = bool(q[1])
+            if w[2] == 'int':
+                q[1] = int(q[1])
+        '''
+        
         if sorted(sample[0:-1]) == sorted(lutdata):
             break
     sparam_file = sample[-1][1].split(";")
@@ -208,6 +228,8 @@ def LUT_processor(
     start = time.time()
     sparam_file, xml, node = LUT_reader(filedir, lutfilename, lutdata)
 
+    print(' - LUT_processor: file: %s' % sparam_file)
+
     # read data
     if ".npz" in sparam_file[0] or ".npz" in sparam_file[-1]:
         npzfile = [each for each in sparam_file if ".npz" in each][0]
@@ -217,11 +239,11 @@ def LUT_processor(
 
     else:
         if verbose:
-            print("numpy datafile not found. reading sparam file instead..")
+            print("numpy datafile not found. reading sparam file instead.. %s " % sparam_file)
 
         sdata = universal_sparam_filereader(nports, sparam_file[-1], filedir, "auto")
         # create npz file name
-        npz_file = sparam_file[-1].split(".")[0]
+        npz_file = sparam_file[-1].split(".dat")[0]
 
         # save as npz file
         np.savez(filedir / npz_file, f=sdata[0], s=sdata[1])
diff --git a/klayout_dot_config/python/SiEPIC/opics_netlist_sim.py b/klayout_dot_config/python/SiEPIC/opics_netlist_sim.py
index 24a5fb980..2996a3605 100644
--- a/klayout_dot_config/python/SiEPIC/opics_netlist_sim.py
+++ b/klayout_dot_config/python/SiEPIC/opics_netlist_sim.py
@@ -1,72 +1,101 @@
 import pya
 
-def circuit_simulation_opics(verbose=False,opt_in_selection_text=[], require_save=True):
-    ''' Simulate the circuit using OPICS
-    Using a netlist extracte from the layout'''
-    
+
+def circuit_simulation_opics(
+    verbose=False, opt_in_selection_text=[], require_save=True
+):
+    """Simulate the circuit using OPICS
+    Using a netlist extracte from the layout"""
+
     # Required packages
     from SiEPIC.install import install
-    if not install('plotly'):
+
+    if not install("plotly"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'plotly'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'plotly'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('pandas'):
+    if not install("pandas"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'pandas'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'pandas'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('packaging'):
+    if not install("packaging"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'packaging'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'packaging'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('defusedxml'):
+    if not install("defusedxml"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'defusedxml'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'defusedxml'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('numpy'):
+    if not install("numpy"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'numpy'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'numpy'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('yaml'):
+    if not install("yaml"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'yaml'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'yaml'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    if not install('scipy'):
+    if not install("scipy"):
         pya.MessageBox.warning(
-        "Missing package", "The OPICS circuit simulator does not function without the package 'scipy'.",  pya.MessageBox.Ok)    
+            "Missing package",
+            "The OPICS circuit simulator does not function without the package 'scipy'.",
+            pya.MessageBox.Ok,
+        )
         return None
-    
-    
+
     # obtain the spice file from the layout
     from SiEPIC.netlist import export_spice_layoutview
-    spice_filepath, _ = export_spice_layoutview(verbose=False,opt_in_selection_text=[], require_save=require_save)
-    
+
+    spice_filepath, _ = export_spice_layoutview(
+        verbose=False, opt_in_selection_text=[], require_save=require_save
+    )
+
     from SiEPIC.opics import libraries
     from SiEPIC.opics.network import Network
     from SiEPIC.opics.utils import netlistParser, NetlistProcessor
     from SiEPIC.opics.globals import C as c_
-        
+
     print(spice_filepath)
-    
+
     # get netlist data
     circuitData = netlistParser(spice_filepath).readfile()
-    
+
     print(circuitData)
-    
-    '''
+
+    """
     import numpy as np
     from SiEPIC.opics.globals import C
     freq = np.linspace(C * 1e6 / 1.5, C * 1e6 / 1.6, 2000)
     circuit = Network(network_id="circuit_name", f=freq)
-    '''
-    
+    """
+
     # process netlist data
-    subckt = NetlistProcessor(spice_filepath, Network, libraries, c_, circuitData, verbose=False)
-    
+    subckt = NetlistProcessor(
+        spice_filepath, Network, libraries, c_, circuitData, verbose=False
+    )
+
     print(subckt)
-    
+
     # simulate network
     subckt.simulate_network()
-    
+
     # get input and output net labels
     inp_idx = subckt.global_netlist[list(subckt.global_netlist.keys())[-1]].index(
         circuitData["inp_net"]
@@ -75,37 +104,37 @@ def circuit_simulation_opics(verbose=False,opt_in_selection_text=[], require_sav
         subckt.global_netlist[list(subckt.global_netlist.keys())[-1]].index(each)
         for each in circuitData["out_net"]
     ]
-    
+
     ports = [[each_output, inp_idx] for each_output in out_idx]
-    
-    
+
     # plot results
     # subckt.sim_result.plot_sparameters(ports=ports, interactive=False)
-    
-    
+
     # Plot using Plotly:
     import plotly.express as px
-    import pandas as pd # https://pandas.pydata.org/docs/user_guide/10min.html
+    import pandas as pd  # https://pandas.pydata.org/docs/user_guide/10min.html
+
     result = subckt.sim_result.get_data()
-    wavelengths = c_/subckt.f
+    wavelengths = c_ / subckt.f
 
     # collect all the results for each port:
     import numpy as np
+
     nports = subckt.sim_result.nports
-    out = result['S_1_0']
-    print(out.shape)       
-    columns = ['Output 1']
-    for i in range(1,nports-1):
-        print(out.shape)       
-        out = np.vstack((out, result['S_%s_0' %(i+1)]))
-        columns = columns + ['Output %s' % (i+1) ]
-    
+    out = result["S_1_0"]
+    print(out.shape)
+    columns = ["Output 1"]
+    for i in range(1, nports - 1):
+        print(out.shape)
+        out = np.vstack((out, result["S_%s_0" % (i + 1)]))
+        columns = columns + ["Output %s" % (i + 1)]
+
     # Single line:
-    #df = pd.DataFrame(transmission, index=wavelengths, columns=['Transmission'])
-    
+    # df = pd.DataFrame(transmission, index=wavelengths, columns=['Transmission'])
+
     # Two lines:
     df = pd.DataFrame(out.transpose(), index=wavelengths, columns=columns)
-    fig = px.line(df, labels={'index':'Wavelength', 'value':'Transmission (dB)'}, markers=True)
+    fig = px.line(
+        df, labels={"index": "Wavelength", "value": "Transmission (dB)"}, markers=True
+    )
     fig.show()
-    
-    
diff --git a/klayout_dot_config/python/SiEPIC/scripts.py b/klayout_dot_config/python/SiEPIC/scripts.py
index 107fe8dac..636092555 100644
--- a/klayout_dot_config/python/SiEPIC/scripts.py
+++ b/klayout_dot_config/python/SiEPIC/scripts.py
@@ -4,6 +4,8 @@
 #################################################################################
 '''
 
+Functions in this file:
+
 connect_pins_with_waveguide
 path_to_waveguide
 path_to_waveguide2
@@ -29,10 +31,15 @@
 fetch_measurement_data_from_github
 measurement_vs_simulation
 resize waveguide
+layout_diff
 replace_cell
 svg_from_cell
 zoom_out: When running in the GUI, Zoom out and show full hierarchy
 export_layout
+instantiate_all_library_cells
+load_klayout_library
+technology_libraries
+version_check
 
 '''
 
@@ -113,7 +120,7 @@ def connect_pins_with_waveguide(instanceA, pinA, instanceB, pinB, waveguide = No
      - debug_path=False
      - r=None
      - error_min_bend_radius=True
-     - relaxed_pinnames=True: finds 'opt' if the pin is called 'opt1'
+     - relaxed_pinnames=True: finds the pin 'opt1' if the input is '1'
      - parent_cell = None: move the waveguide into a specific cell
         default is the common parent of instanceA and instanceB
      
@@ -205,54 +212,77 @@ def connect_pins_with_waveguide(instanceA, pinA, instanceB, pinB, waveguide = No
                 
     else:
         cell=instanceA.parent_cell
-            
+
     # Find the two components:
     from time import time
     t = time()
     # benchmarking: find_components here takes 0.015 s
-    componentA = instanceA.parent_cell.find_components(inst=instanceA)
-    componentB = instanceB.parent_cell.find_components(inst=instanceB)
 #    print('Time elapsed: %s' % (time() - t))    
-    if componentA==[]:
-        print('InstA: %s, %s' % (instanceA.cell.name, instanceA) )
-        print('componentA: %s' % (componentA) )
-        print('parent_cell A: %s, cell A: %s' % (instanceA.parent_cell, instanceA.cell) )
-        print('all found components A: instance variable: %s' %    ([n.instance for n in instanceA.parent_cell.find_components()]) )
-        print('all found components A: component variable: %s' %    ([n.component for n in instanceA.parent_cell.find_components()]) )
-        raise Exception("Component '%s' not found. \nCheck that the component is correctly built (DevRec and PinRec layers). \nTry SiEPIC > Layout > Show Selected Component Information for debugging." %instanceA.cell.name)
-    if componentB==[]:
-        print('InstB: %s, %s' % (instanceB.cell.name, instanceB) )
-        print('componentB: %s' % (componentB) )
-        print('parent_cell B: %s, cell B: %s' % (instanceB.parent_cell, instanceB.cell) )
-        print('all found components B: instance variable: %s' %    ([n.instance for n in instanceB.parent_cell.find_components()]) )
-        print('all found components B: component variable: %s' %    ([n.component for n in instanceB.parent_cell.find_components()]) )
-        raise Exception("Component '%s' not found. \nCheck that the component is correctly built (DevRec and PinRec layers). \nTry SiEPIC > Layout > Show Selected Component Information for debugging." %instanceB.cell.name)
-
-    # if the instance had sub-cells, then there will be many components. Pick the first one.
-    if type(componentA) == type([]):
-        componentA = componentA[0]
-    if type(componentB) == type([]):
-        componentB = componentB[0]
+
+    # Find the two instances' pins, and see if they uniquely match
+    ipinA = [p for p in instanceA.find_pins()[0] if p.pin_name == pinA]
+    ipinB = [p for p in instanceB.find_pins()[0] if p.pin_name == pinB]
+    if len(ipinA) == 1:
+        cpinA = ipinA
+        if verbose:
+            print(' - connect_pins_with_waveguide: found unique pin %s' % pinA)
+    else:
+        if verbose:
+            print(' - connect_pins_with_waveguide: searching for pin %s' % pinA)
+        componentA = instanceA.parent_cell.find_components(inst=instanceA)
+        if componentA==[]:
+            print('InstA: %s, %s' % (instanceA.cell.name, instanceA) )
+            print('componentA: %s' % (componentA) )
+            print('parent_cell A: %s, cell A: %s' % (instanceA.parent_cell, instanceA.cell) )
+            print('all found components A: instance variable: %s' %    ([n.instance for n in instanceA.parent_cell.find_components()]) )
+            print('all found components A: component variable: %s' %    ([n.component for n in instanceA.parent_cell.find_components()]) )
+            raise Exception("Component '%s' not found. \nCheck that the component is correctly built (DevRec and PinRec layers). \nTry SiEPIC > Layout > Show Selected Component Information for debugging." %instanceA.cell.name)
+        # if the instance had sub-cells, then there will be many components. Pick the first one.
+        if type(componentA) == type([]):
+            componentA = componentA[0]
+        # Find component pinA
+        cpinA = [p for p in componentA.pins if p.pin_name == pinA]     
+    if len(ipinB) == 1:
+        cpinB = ipinB
+        if verbose:
+            print(' - connect_pins_with_waveguide: found unique pin %s' % pinB)
+    else:
+        if verbose:
+            print(' - connect_pins_with_waveguide: searching for pin %s' % pinB)
+        componentB = instanceB.parent_cell.find_components(inst=instanceB)
+        if componentB==[]:
+            print('InstB: %s, %s' % (instanceB.cell.name, instanceB) )
+            print('componentB: %s' % (componentB) )
+            print('parent_cell B: %s, cell B: %s' % (instanceB.parent_cell, instanceB.cell) )
+            print('all found components B: instance variable: %s' %    ([n.instance for n in instanceB.parent_cell.find_components()]) )
+            print('all found components B: component variable: %s' %    ([n.component for n in instanceB.parent_cell.find_components()]) )
+            raise Exception("Component '%s' not found. \nCheck that the component is correctly built (DevRec and PinRec layers). \nTry SiEPIC > Layout > Show Selected Component Information for debugging." %instanceB.cell.name)
+        # if the instance had sub-cells, then there will be many components. Pick the first one.
+        if type(componentB) == type([]):
+            componentB = componentB[0]
+        # Find component pinB
+        cpinB = [p for p in componentB.pins if p.pin_name == pinB]
 
     if verbose:
         print('InstA: %s, InstB: %s' % (instanceA, instanceB) )
-        print('componentA: %s, componentB: %s' % (componentA, componentB) )
-
-        componentA.display()
-        componentB.display()
+        print('cpinA: %s, cpinB: %s' % (cpinA, cpinB) )
+        if 'componentA' in locals():
+            print('componentA: %s' % (componentA) )
+            componentA.display()
+        if 'componentB' in locals():
+            print('componentB: %s' % (componentB) )
+            componentB.display()
         
-    # Find pinA and pinB
-    cpinA = [p for p in componentA.pins if p.pin_name == pinA]
-    cpinB = [p for p in componentB.pins if p.pin_name == pinB]        
-
+    # If we haven't found the pins,
     # relaxed_pinnames:  scan for only the number
     if relaxed_pinnames==True:
-        import re
         try:
             if cpinA==[]:
+                import re
                 if re.findall(r'\d+', pinA):
                     cpinA = [p for p in componentA.pins if re.findall(r'\d+', pinA)[0] in p.pin_name]
             if cpinB==[]:
+                import re
                 if re.findall(r'\d+', pinB):
                     cpinB = [p for p in componentB.pins if re.findall(r'\d+', pinB)[0] in p.pin_name]
         except:
@@ -265,37 +295,20 @@ def connect_pins_with_waveguide(instanceA, pinA, instanceB, pinB, waveguide = No
             # contains only one pin matching the name, e.g. unique opt_input in a sub-circuit
             cpinA = [instanceA.find_pin(pinA)]
         except:
-              error_message = "SiEPIC-Tools, in function connect_pins_with_waveguide: Pin (%s) not found in componentA (%s). Available pins: %s" % (pinA,componentA.component, [p.pin_name for p in componentA.pins])
-              '''
-              if _globals.Python_Env == "KLayout_GUI":
-                question = pya.QMessageBox().setStandardButtons(pya.QMessageBox.Ok)
-                question.setText("SiEPIC-Tools scripted layout, requested pin not found")
-                question.setInformativeText(error_message)
-                pya.QMessageBox_StandardButton(question.exec_())
-                return
-              else:
-              '''          
-              raise Exception(error_message)
+            error_message = "SiEPIC-Tools, in function connect_pins_with_waveguide: Pin (%s) not found in componentA (%s). Available pins: %s" % (pinA,componentA.component, [p.pin_name for p in componentA.pins])
+            raise Exception(error_message)
     if cpinB==[]:
         try:  
             # this checks if the cell (which could contain multiple components) 
             # contains only one pin matching the name, e.g. unique opt_input in a sub-circuit
             cpinB = [instanceB.find_pin(pinB)]
         except:
-              error_message = "SiEPIC-Tools, in function connect_pins_with_waveguide: Pin (%s) not found in componentB (%s). Available pins: %s" % (pinB,componentB.component, [p.pin_name for p in componentB.pins])
-              '''
-              if _globals.Python_Env == "KLayout_GUI":
-                question = pya.QMessageBox().setStandardButtons(pya.QMessageBox.Ok)
-                question.setText("SiEPIC-Tools scripted layout, requested pin not found")
-                question.setInformativeText(error_message)
-                pya.QMessageBox_StandardButton(question.exec_())
-                return
-              else:          
-              '''
-              raise Exception(error_message)
+            error_message = "SiEPIC-Tools, in function connect_pins_with_waveguide: Pin (%s) not found in componentB (%s). Available pins: %s" % (pinB,componentB.component, [p.pin_name for p in componentB.pins])
+            raise Exception(error_message)
 
     cpinA=cpinA[0]
     cpinB=cpinB[0]
+    
     if verbose:
         cpinA.display()
         cpinB.display()
@@ -342,7 +355,7 @@ def connect_pins_with_waveguide(instanceA, pinA, instanceB, pinB, waveguide = No
             else:
                 waveguide = waveguides[0]
                 print('error: waveguide type not found in PDK waveguides')
-                raise Exception('error: waveguide type (%s) not found in PDK waveguides' % waveguide_type)
+                raise Exception('error: waveguide type (%s) not found in PDK. Waveguides available: %s' % (waveguide_type, [w['name'] for w in waveguides]))
         # check if the waveguide type is compound waveguide
         if 'compound_waveguide' in waveguide:
             waveguide = [w for w in waveguides if w['name']==waveguide['compound_waveguide']['singlemode']]
@@ -1731,14 +1744,20 @@ def connect_cell(instanceA, pinA, cellB, pinB, mirror = False, verbose=False, tr
 
   # check cells
   if type(cellB) != pya.Cell:
-      raise Exception("cellB needs to be a cell, not a cell index")
+      raise Exception("cellB needs to be a pya.Cell, not a cell index, nor string")
   if type(instanceA) != pya.Instance:
       raise Exception("instanceA needs to be an Instance, not an index")
 
   # Find the two components:
-  componentA = instanceA.parent_cell.find_components(cell_selected=instanceA.cell, inst=instanceA)
+  componentA = instanceA.parent_cell.find_components(cell_selected=instanceA.cell, inst=instanceA, verbose=verbose)
   componentB = cellB.find_components()
   if componentA==[]:
+    if verbose:
+      print('*** WARNING: componentA not found, looking lower in the hierarchy')
+    componentA = instanceA.cell.find_components(inst=instanceA, verbose=verbose)
+  if componentA==[]:
+    if verbose:
+      print('*** WARNING: componentA not found, looking higher in the hierarchy which may not work correctly: instanceA.parent_cell.find_components(inst=instanceA)')
     componentA = instanceA.parent_cell.find_components(inst=instanceA)
     if componentA==[]:
       if _globals.Python_Env == "KLayout_GUI":
@@ -1767,7 +1786,8 @@ def connect_cell(instanceA, pinA, cellB, pinB, mirror = False, verbose=False, tr
 
   if type(componentA) == type([]):
     componentA = componentA[0]
-  componentB = componentB[0]
+  if type(componentB) == type([]):
+    componentB = componentB[0]
   if verbose:
     componentA.display()
     componentB.display()
@@ -1781,9 +1801,11 @@ def connect_cell(instanceA, pinA, cellB, pinB, mirror = False, verbose=False, tr
       import re
       try:
           if cpinA==[]:
-              cpinA = [p for p in componentA.pins if re.findall(r'\d+', pinA)[0] in p.pin_name]
+              if re.findall(r'\d+', pinA):
+                  cpinA = [p for p in componentA.pins if re.findall(r'\d+', pinA)[0] in p.pin_name]
           if cpinB==[]:
-              cpinB = [p for p in componentB.pins if re.findall(r'\d+', pinB)[0] in p.pin_name]
+              if re.findall(r'\d+', pinB):
+                  cpinB = [p for p in componentB.pins if re.findall(r'\d+', pinB)[0] in p.pin_name]
       except:
           print('error in siepic.scripts.connect_cell')      
 
@@ -2263,6 +2285,8 @@ def download_text(checked):
     pya.Application.instance().main_window().redraw()    
 
 def find_SEM_labels_gui(topcell=None, LayerSEMN=None, MarkersInTopCell=False):
+    '''Find all polygons on the SEM layer'''
+    
     from .utils import get_technology
     TECHNOLOGY = get_technology()
 
@@ -2356,7 +2380,7 @@ def calculate_area(ly=None, cell=None):
     elif cell == None and ly:
         cell = ly.top_cell()
     elif cell:
-        ly = cell.layout
+        ly = cell.layout()
     TECHNOLOGY = get_technology_by_name(ly.technology().name)
     dbu = TECHNOLOGY['dbu']
 
@@ -2379,8 +2403,6 @@ def calculate_area(ly=None, cell=None):
             count += cell2.shapes(l).size()
         p = pya.RelativeProgress("Calculating the area", count)
         text += 'Number of shapes: %s\n' % count
-
-    print (text)
     
     try:
         total = cell.each_shape(ly.layer(TECHNOLOGY['FloorPlan'])).__next__().polygon.area()
@@ -2412,21 +2434,7 @@ def calculate_area(ly=None, cell=None):
         v = pya.MessageBox.warning(
             "Waveguide area.", text, pya.MessageBox.Ok)
     
-    '''
-    area = 0
-    itr = cell.begin_shapes_rec(ly.layer(TECHNOLOGY['Waveguide']))
-    while not itr.at_end():
-        area += itr.shape().area()
-        itr.next()
-    print("Waveguide area: %s mm^2, chip area: %s mm^2, percentage: %s %%" % (area/1e6*dbu*dbu,total/1e6*dbu*dbu, area/total*100))
-
-    if total == 1e99:
-      v = pya.MessageBox.warning(
-        "Waveguide area.", "Waveguide area: %.5g mm^2 \n   (%.5g micron^2)" % (area/1e6*dbu*dbu, area/1e6), pya.MessageBox.Ok)
-    else:
-      v = pya.MessageBox.warning(
-        "Waveguide area.", "Waveguide area: %.5g mm^2 \n   (%.5g micron^2),\nChip Floorplan: %.5g mm^2, \nPercentage: %.3g %%" % (area/1e6*dbu*dbu, area/1e6, total/1e6*dbu*dbu, area/total*100), pya.MessageBox.Ok)
-    '''
+    return text
 
 def trim_netlist(nets, components, selected_component, verbose=None):
     """Trim Netlist
@@ -2477,478 +2485,10 @@ def trim_netlist(nets, components, selected_component, verbose=None):
     return trimmed_nets, trimmed_components
 
 
-
-
 def layout_check(cell=None, verbose=False):
-    '''Functional Verification:
-    
-    Verification of things that are specific to photonic integrated circuits, including
-    - Waveguides: paths, radius, bend points, Manhattan
-    - Component checking: overlapping, avoiding crosstalk
-    - Connectivity check: disconnected pins, mismatched pins
-    - Simulation model check
-    - Design for Test: Specific for each technology, check of optical IO position, direction, pitch, etc.
-
-    Description: https://github.com/SiEPIC/SiEPIC-Tools/wiki/SiEPIC-Tools-Menu-descriptions#functional-layout-check
-
-    Tools that can create layouts that are compatible with this Verification:
-        - KLayout SiEPIC-Tools, and various PDKs such as
-            https://github.com/SiEPIC/SiEPIC_EBeam_PDK
-        - GDSfactory 
-            "UBCPDK" https://github.com/gdsfactory/ubc
-            based on https://github.com/SiEPIC/SiEPIC_EBeam_PDK
-        - Luceda
-            https://academy.lucedaphotonics.com/pdks/siepic/siepic.html
-            https://academy.lucedaphotonics.com/pdks/siepic_shuksan/siepic_shuksan.html
-    
-    Limitations:
-    - we assume that the layout was created based on the standard defined in SiEPIC-Tools in KLayout
-      https://github.com/SiEPIC/SiEPIC-Tools/wiki/Component-and-PCell-Layout
-    - The layout can contain PCells, or with $$$CONTEXT_INFO$$$ removed, i.e., fixed cells
-    - The layout cannot have been flattened. This allows us to isolate individual components
-      by their instances and cells.
-    - Parameters from cells can be extracted from the PCell, or from the text labels in the cell
-    - Working with a flattened layout would be harder, and require:
-       - reading parameters from the text labels (OK)
-       - find_components would need to look within the DevRec layer, rather than in the selected cell
-       - when pins are connected, we have two overlapping ones, so detecting them would be problematic;
-         This could be solved by putting the pins inside the cells, rather than sticking out.    
-    '''
-
-    if verbose:
-        print("*** layout_check()")
-
-    from . import _globals
-    from .utils import get_technology, find_paths, find_automated_measurement_labels, angle_vector
-    from .utils import advance_iterator
-    TECHNOLOGY = get_technology()
-    dbu = TECHNOLOGY['dbu']
-
-    lv = pya.Application.instance().main_window().current_view()
-    if lv == None:
-        raise Exception("No view selected")
-    if cell is None:
-        ly = lv.active_cellview().layout()
-        if ly == None:
-            raise Exception("No active layout")
-        cell = lv.active_cellview().cell
-        if cell == None:
-            raise Exception("No active cell")
-        cv = lv.active_cellview()
-    else:
-        ly = cell.layout()
+    '''Deprecated, moved to SiEPIC.verification.layout_check()'''
+    SiEPIC.verification.layout_check(cell=None, verbose=False)
 
-    if not TECHNOLOGY['technology_name']:
-        v = pya.MessageBox.warning("Errors", "SiEPIC-Tools verification requires a technology to be chosen.  \n\nThe active technology is displayed on the bottom-left of the KLayout window, next to the T. \n\nChange the technology using KLayout File | Layout Properties, then choose Technology and find the correct one (e.g., EBeam, GSiP).", pya.MessageBox.Ok)
-        return
-
-    # Get the components and nets for the layout
-    nets, components = cell.identify_nets(verbose=False)
-    if verbose:
-        print("* Display list of components:")
-        [c.display() for c in components]
-
-    if not components:
-        v = pya.MessageBox.warning(
-            "Errors", "No components found (using SiEPIC-Tools DevRec and PinRec definitions).", pya.MessageBox.Ok)
-        return
-
-    # Create a Results Database
-    rdb_i = lv.create_rdb("SiEPIC-Tools Verification: %s technology" %
-                          TECHNOLOGY['technology_name'])
-    rdb = lv.rdb(rdb_i)
-    rdb.top_cell_name = cell.name
-    rdb_cell = rdb.create_cell(cell.name)
-
-    # Waveguide checking
-    rdb_cell = next(rdb.each_cell())
-    rdb_cat_id_wg = rdb.create_category("Waveguide")
-    rdb_cat_id_wg_path = rdb.create_category(rdb_cat_id_wg, "Path")
-    rdb_cat_id_wg_path.description = "Waveguide path: Only 2 points allowed in a path. Convert to a Waveguide if necessary."
-    rdb_cat_id_wg_radius = rdb.create_category(rdb_cat_id_wg, "Radius")
-    rdb_cat_id_wg_radius.description = "Not enough space to accommodate the desired bend radius for the waveguide."
-    rdb_cat_id_wg_bendpts = rdb.create_category(rdb_cat_id_wg, "Bend points")
-    rdb_cat_id_wg_bendpts.description = "Waveguide bend should have more points per circle."
-    rdb_cat_id_wg_manhattan = rdb.create_category(rdb_cat_id_wg, "Manhattan")
-    rdb_cat_id_wg_manhattan.description = "The first and last waveguide segment need to be Manhattan (vertical or horizontal) so that they can connect to device pins."
-
-    # Component checking
-    rdb_cell = next(rdb.each_cell())
-    rdb_cat_id_comp = rdb.create_category("Component")
-    rdb_cat_id_comp_flat = rdb.create_category(rdb_cat_id_comp, "Flattened component")
-    rdb_cat_id_comp_flat.description = "SiEPIC-Tools Verification, Netlist extraction, and Simulation only functions on hierarchical layouts, and not on flattened layouts.  Add to the discussion here: https://github.com/lukasc-ubc/SiEPIC-Tools/issues/37"
-    rdb_cat_id_comp_overlap = rdb.create_category(rdb_cat_id_comp, "Overlapping component")
-    rdb_cat_id_comp_overlap.description = "Overlapping components (defined as overlapping DevRec layers; touching is ok)"
-    rdb_cat_id_comp_shapesoutside = rdb.create_category(rdb_cat_id_comp, "Shapes outside component")
-    rdb_cat_id_comp_shapesoutside.description = "Shapes need to be inside a component. Read more about requirements for components: https://github.com/SiEPIC/SiEPIC-Tools/wiki/Component-and-PCell-Layout"
-
-    # Connectivity checking
-    rdb_cell = next(rdb.each_cell())
-    rdb_cat_id = rdb.create_category("Connectivity")
-    rdb_cat_id_discpin = rdb.create_category(rdb_cat_id, "Disconnected pin")
-    rdb_cat_id_discpin.description = "Disconnected pin"
-    rdb_cat_id_mismatchedpin = rdb.create_category(rdb_cat_id, "Mismatched pin")
-    rdb_cat_id_mismatchedpin.description = "Mismatched pin widths"
-
-    # Simulation checking
-    # disabled by lukasc, 2021/05
-    if 0:
-        rdb_cell = next(rdb.each_cell())
-        rdb_cat_id = rdb.create_category("Simulation")
-        rdb_cat_id_sim_nomodel = rdb.create_category(rdb_cat_id, "Missing compact model")
-        rdb_cat_id_sim_nomodel.description = "A compact model for this component was not found. Possible reasons: 1) Please run SiEPIC | Simulation | Setup Lumerical INTERCONNECT and CML, to make sure that the Compact Model Library is installed in INTERCONNECT, and that KLayout has a list of all component models. 2) the library does not have a compact model for this component. "
-
-    # Design for Test checking
-    from SiEPIC.utils import load_DFT
-    DFT = load_DFT()
-    if DFT:
-        if verbose:
-            print(DFT)
-        rdb_cell = next(rdb.each_cell())
-        rdb_cat_id = rdb.create_category("Design for test")
-        rdb_cat_id_optin_unique = rdb.create_category(rdb_cat_id, "opt_in label: same")
-        rdb_cat_id_optin_unique.description = "Automated test opt_in labels should be unique."
-        rdb_cat_id_optin_missing = rdb.create_category(rdb_cat_id, "opt_in label: missing")
-        rdb_cat_id_optin_missing.description = "Automated test opt_in labels are required for measurements on the Text layer. \n\nDetails on the format for the opt_in labels can be found at https://github.com/lukasc-ubc/SiEPIC-Tools/wiki/SiEPIC-Tools-Menu-descriptions#connectivity-layout-check"
-        rdb_cat_id_optin_toofar = rdb.create_category(rdb_cat_id, "opt_in label: too far away")
-        rdb_cat_id_optin_toofar.description = "Automated test opt_in labels must be placed at the tip of the grating coupler, namely near the (0,0) point of the cell."
-        rdb_cat_id_optin_wavelength = rdb.create_category(rdb_cat_id, "opt_in label: wavelength")
-        if type(DFT['design-for-test']['tunable-laser']) == list:
-            DFT_wavelengths = [w['wavelength'] for w in DFT['design-for-test']['tunable-laser']]
-        else:
-            DFT_wavelengths = DFT['design-for-test']['tunable-laser']['wavelength']
-        rdb_cat_id_optin_wavelength.description = "Automated test opt_in labels must have a wavelength for a laser specified in the DFT.xml file: %s.  \n\nDetails on the format for the opt_in labels can be found at https://github.com/lukasc-ubc/SiEPIC-Tools/wiki/SiEPIC-Tools-Menu-descriptions#connectivity-layout-check" % DFT_wavelengths
-        if type(DFT['design-for-test']['tunable-laser']) == list:
-            DFT_polarizations = [p['polarization']
-                                 for p in DFT['design-for-test']['tunable-laser']]
-        else:
-            if 'polarization' in DFT['design-for-test']['tunable-laser']:
-                DFT_polarizations = DFT['design-for-test']['tunable-laser']['polarization']
-            else:
-                DFT_polarizations = "TE or TM"
-        rdb_cat_id_optin_polarization = rdb.create_category(
-            rdb_cat_id, "opt_in label: polarization")
-        rdb_cat_id_optin_polarization.description = "Automated test opt_in labels must have a polarization as specified in the DFT.xml file: %s. \n\nDetails on the format for the opt_in labels can be found at https://github.com/lukasc-ubc/SiEPIC-Tools/wiki/SiEPIC-Tools-Menu-descriptions#connectivity-layout-check" % DFT_polarizations
-#    rdb_cat_id_GCpitch = rdb.create_category(rdb_cat_id, "Grating Coupler pitch")
-#    rdb_cat_id_GCpitch.description = "Grating couplers must be on a %s micron pitch, vertically arranged, as specified in the DFT.xml." % (float(DFT['design-for-test']['grating-couplers']['gc-pitch']))
-        rdb_cat_id_GCorient = rdb.create_category(rdb_cat_id, "Grating coupler orientation")
-        rdb_cat_id_GCorient.description = "The grating coupler is not oriented (rotated) the correct way for automated testing."
-        rdb_cat_id_GCarrayconfig = rdb.create_category(rdb_cat_id, "Fibre array configuration")
-        array_angle = (float(DFT['design-for-test']['grating-couplers']['gc-array-orientation']))%360.0
-        if array_angle==0:
-          dir1 = ' right of '
-          dir2 = ' left of '
-          dir3 = 'horizontally'
-        elif array_angle==90:
-          dir1 = ' above '
-          dir2 = ' below '
-          dir3 = 'vertically'
-        elif array_angle == 180:
-          dir1 = ' left of '
-          dir2 = ' right of '
-          dir3 = 'horizontally'
-        else:
-          dir1 = ' below '
-          dir2 = ' above '
-          dir3 = 'vertically'
-        
-        rdb_cat_id_GCarrayconfig.description = "Circuit must be connected such that there is at most %s Grating Coupler(s) %s the opt_in label (laser injection port) and at most %s Grating Coupler(s) %s the opt_in label. \nGrating couplers must be on a %s micron pitch, %s arranged." % (
-            int(DFT['design-for-test']['grating-couplers']['detectors-above-laser']), dir1,int(DFT['design-for-test']['grating-couplers']['detectors-below-laser']), dir2,float(DFT['design-for-test']['grating-couplers']['gc-pitch']),dir3)
-
-    else:
-        if verbose:
-            print('  No DFT rules found.')
-
-    paths = find_paths(TECHNOLOGY['Waveguide'], cell=cell)
-    for p in paths:
-        if verbose:
-            print("%s, %s" % (type(p), p))
-        # Check for paths with > 2 vertices
-        Dpath = p.to_dtype(dbu)
-        if Dpath.num_points() > 2:
-            rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_wg_path.rdb_id())
-            rdb_item.add_value(pya.RdbItemValue(Dpath.polygon()))
-
-    '''
-    Shapes need to be inside a component. 
-    Read more about requirements for components: https://github.com/SiEPIC/SiEPIC-Tools/wiki/Component-and-PCell-Layout
-    Method:
-        - find all shapes
-        - find all components, and their shapes
-        - substract the two, and produce errors        
-    rdb_cat_id_comp_shapesoutside
-    '''
-    for i in range(0, len(components)):
-        c = components[i]
-
-
-    for i in range(0, len(components)):
-        c = components[i]
-        # the following only works for layouts where the Waveguide is still a PCells (not flattened)
-        # basic_name is assigned in Cell.find_components, by reading the PCell parameter
-        # if the layout is flattened, we don't have an easy way to get the path
-        # it could be done perhaps as a parameter (points)
-        if c.basic_name == "Waveguide" and c.cell.is_pcell_variant():
-            pcell_params = c.cell.pcell_parameters_by_name()
-            Dpath = pcell_params['path']
-            if 'radius' in pcell_params:
-                radius = pcell_params['radius']
-            else:
-                radius = 5
-            if verbose:
-                print(" - Waveguide: cell: %s, %s" % (c.cell.name, radius))
-
-            # Radius check:
-            if not Dpath.radius_check(radius):
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_wg_radius.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue( "The minimum radius is set at %s microns for this waveguide." % (radius) ))
-                rdb_item.add_value(pya.RdbItemValue(Dpath))
-
-            # Check for waveguides with too few bend points
-
-            # Check if waveguide end segments are Manhattan; this ensures they can connect to a pin
-            if not Dpath.is_manhattan_endsegments():
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_wg_manhattan.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue(Dpath))
-
-        if c.basic_name == "Flattened":
-            if verbose:
-                print(" - Component: Flattened: %s" % (c.polygon))
-            rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_comp_flat.rdb_id())
-            rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
-
-        # check all the component's pins to check if they are assigned a net:
-        for pin in c.pins:
-            if pin.type == _globals.PIN_TYPES.OPTICAL and pin.net.idx == None:
-                # disconnected optical pin
-                if verbose:
-                    print(" - Found disconnected pin, type %s, at (%s)" % (pin.type, pin.center))
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_discpin.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue(pin.path.to_dtype(dbu)))
-
-        # Verification: overlapping components (DevRec)
-            # automatically takes care of waveguides crossing other waveguides & components
-        # Region: put in two DevRec polygons (in raw), measure area, merge, check if are is the same
-        #  checks for touching but not overlapping DevRecs
-        for i2 in range(i + 1, len(components)):
-            c2 = components[i2]
-            r1 = pya.Region(c.polygon)
-            r2 = pya.Region(c2.polygon)
-            polygon_and = [p for p in r1 & r2]
-            if polygon_and:
-                print(" - Found overlapping components: %s, %s" % (c.component, c2.component))
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_comp_overlap.rdb_id())
-                if c.component == c2.component:
-                    rdb_item.add_value(pya.RdbItemValue(
-                        "There are two identical components overlapping: " + c.component))
-                for p in polygon_and:
-                    rdb_item.add_value(pya.RdbItemValue(p.to_dtype(dbu)))
-                # check if these components have the same name; possibly a copy and paste error
-        if DFT:
-            # DFT verification
-            # GC facing the right way
-            if c.basic_name:
-                ci = c.basic_name  # .replace(' ','_').replace('$','_')
-                gc_orientation_error = False
-                for gc in DFT['design-for-test']['grating-couplers']['gc-orientation'].keys():
-                    DFT_GC_angle = int(DFT['design-for-test']['grating-couplers']['gc-orientation'][gc])
-                    if ci.startswith(gc) and c.trans.angle != DFT_GC_angle:
-                        if verbose:
-                            print(" - Found DFT error, GC facing the wrong way: %s, %s" %
-                                  (c.component, c.trans.angle))
-                        rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_GCorient.rdb_id())
-                        rdb_item.add_value(pya.RdbItemValue( "Cell %s should be %s degrees" % (ci,DFT_GC_angle) ))
-                        rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
-
-
-        # Pre-simulation check: do components have models?
-        # disabled by lukasc, 2021/05
-        if not c.has_model() and 0:
-            if verbose:
-                print(" - Missing compact model, for component: %s" % (c.component))
-            rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_sim_nomodel.rdb_id())
-            rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
-
-    if DFT:
-        # DFT verification
-
-        text_out, opt_in = find_automated_measurement_labels(cell)
-
-        '''
-    # opt_in labels missing: 0 labels found. draw box around the entire circuit.
-    # replaced with code below that finds each circuit separately
-    if len(opt_in) == 0:
-      rdb_item = rdb.create_item(rdb_cell.rdb_id(),rdb_cat_id_optin_missing.rdb_id())
-      rdb_item.add_value(pya.RdbItemValue( pya.Polygon(cell.bbox()).to_dtype(dbu) ) )
-    '''
-
-        # dataset for all components found connected to opt_in labels; later
-        # subtract from all components to find circuits with missing opt_in
-        components_connected_opt_in = []
-
-        # opt_in labels
-        for ti1 in range(0, len(opt_in)):
-            if 'opt_in' in opt_in[ti1]:
-                t = opt_in[ti1]['Text']
-                box_s = 1000
-                box = pya.Box(t.x - box_s, t.y - box_s, t.x + box_s, t.y + box_s)
-                # opt_in labels check for unique
-                for ti2 in range(ti1 + 1, len(opt_in)):
-                    if 'opt_in' in opt_in[ti2]:
-                        if opt_in[ti1]['opt_in'] == opt_in[ti2]['opt_in']:
-                            if verbose:
-                                print(" - Found DFT error, non unique text labels: %s, %s, %s" %
-                                      (t.string, t.x, t.y))
-                            rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_optin_unique.rdb_id())
-                            rdb_item.add_value(pya.RdbItemValue(t.string))
-                            rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-    
-                # opt_in format check:
-                if not opt_in[ti1]['wavelength'] in DFT_wavelengths:
-                    if verbose:
-                        print(" - DFT error: wavelength")
-                    rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_optin_wavelength.rdb_id())
-                    rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-    
-                if not (opt_in[ti1]['pol'] in DFT_polarizations):
-                    if verbose:
-                        print(" - DFT error: polarization")
-                    rdb_item = rdb.create_item(
-                        rdb_cell.rdb_id(), rdb_cat_id_optin_polarization.rdb_id())
-                    rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-    
-                # find the GC closest to the opt_in label.
-                from ._globals import KLAYOUT_VERSION
-                components_sorted = sorted([c for c in components if [p for p in c.pins if p.type == _globals.PIN_TYPES.OPTICALIO]],
-                                           key=lambda x: x.trans.disp.to_p().distance(pya.Point(t.x, t.y).to_dtype(1)))
-                # GC too far check:
-                if components_sorted:
-                    dist_optin_c = components_sorted[0].trans.disp.to_p(
-                    ).distance(pya.Point(t.x, t.y).to_dtype(1))
-                    if verbose:
-                        print(" - Found opt_in: %s, nearest GC: %s.  Locations: %s, %s. distance: %s" % (opt_in[ti1][
-                              'Text'], components_sorted[0].instance,  components_sorted[0].center, pya.Point(t.x, t.y), dist_optin_c * dbu))
-                    if dist_optin_c > float(DFT['design-for-test']['opt_in']['max-distance-to-grating-coupler']) * 1000:
-                        if verbose:
-                            print(" - opt_in label too far from the nearest grating coupler: %s, %s" %
-                                  (components_sorted[0].instance, opt_in[ti1]['opt_in']))
-                        rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_optin_toofar.rdb_id())
-                        rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-    
-                    # starting with each opt_in label, identify the sub-circuit, then GCs, and
-                    # check for GC spacing
-                    trimmed_nets, trimmed_components = trim_netlist(
-                        nets, components, components_sorted[0])
-                    components_connected_opt_in = components_connected_opt_in + trimmed_components
-                    detector_GCs = [c for c in trimmed_components if [p for p in c.pins if p.type == _globals.PIN_TYPES.OPTICALIO] if (
-                        c.trans.disp - components_sorted[0].trans.disp).to_p() != pya.DPoint(0, 0)]
-                    if verbose:
-                        print("   N=%s, detector GCs: %s" %
-                              (len(detector_GCs), [c.display() for c in detector_GCs]))
-                    vect_optin_GCs = [(c.trans.disp - components_sorted[0].trans.disp).to_p()
-                                      for c in detector_GCs]
-                    # for vi in range(0,len(detector_GCs)):
-                    #  if round(angle_vector(vect_optin_GCs[vi])%180)!=int(DFT['design-for-test']['grating-couplers']['gc-array-orientation']):
-                    #    if verbose:
-                    #      print( " - DFT GC pitch or angle error: angle %s, %s"  % (round(angle_vector(vect_optin_GCs[vi])%180), opt_in[ti1]['opt_in']) )
-                    #    rdb_item = rdb.create_item(rdb_cell.rdb_id(),rdb_cat_id_GCpitch.rdb_id())
-                    #    rdb_item.add_value(pya.RdbItemValue( detector_GCs[vi].polygon.to_dtype(dbu) ) )
-    
-                    # find the GCs in the circuit that don't match the testing configuration
-                    import numpy as np
-                    array_angle = float(DFT['design-for-test']['grating-couplers']['gc-array-orientation'])
-                    pitch = float(DFT['design-for-test']['grating-couplers']['gc-pitch'])
-                    sx = np.round(np.cos(array_angle/180*np.pi))
-                    sy = np.round(np.sin(array_angle/180*np.pi))
-                    
-                    for d in list(range(int(DFT['design-for-test']['grating-couplers']['detectors-above-laser']) + 0, 0, -1)) + list(range(-1, -int(DFT['design-for-test']['grating-couplers']['detectors-below-laser']) - 1, -1)):
-                        if pya.DPoint(d * sx* pitch * 1000, d *sy* pitch * 1000) in vect_optin_GCs:
-                            del_index = vect_optin_GCs.index(pya.DPoint(
-                                d * sx* pitch * 1000, d *sy* pitch * 1000))
-                            del vect_optin_GCs[del_index]
-                            del detector_GCs[del_index]
-                    for vi in range(0, len(vect_optin_GCs)):
-                        if verbose:
-                            print(" - DFT GC array config error: %s, %s" %
-                                  (components_sorted[0].instance, opt_in[ti1]['opt_in']))
-                        rdb_item = rdb.create_item(
-                            rdb_cell.rdb_id(), rdb_cat_id_GCarrayconfig.rdb_id())
-                        rdb_item.add_value(pya.RdbItemValue(
-                            "The label having the error is: " + opt_in[ti1]['opt_in']))
-                        rdb_item.add_value(pya.RdbItemValue(detector_GCs[vi].polygon.to_dtype(dbu)))
-                        rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-
-        # subtract components connected to opt_in labels from all components to
-        # find circuits with missing opt_in
-        components_without_opt_in = [
-            c for c in components if not (c in components_connected_opt_in)]
-        i = 0  # to avoid getting stuck in the loop in case of an error
-        while components_without_opt_in and i < 50:
-            # find the first GC
-            components_GCs = [c for c in components_without_opt_in if [
-                p for p in c.pins if p.type == _globals.PIN_TYPES.OPTICALIO]]
-            if components_GCs:
-                trimmed_nets, trimmed_components = trim_netlist(
-                    nets, components, components_GCs[0])
-                # circuit without opt_in label, generate error
-                r = pya.Region()
-                for c in trimmed_components:
-                    r.insert(c.polygon)
-                for p in r.each_merged():
-                    rdb_item = rdb.create_item(
-                        rdb_cell.rdb_id(), rdb_cat_id_optin_missing.rdb_id())
-                    rdb_item.add_value(pya.RdbItemValue(p.to_dtype(dbu)))
-                # remove from the list of components without opt_in:
-                components_without_opt_in = [
-                    c for c in components_without_opt_in if not (c in trimmed_components)]
-                i += 1
-            else:
-                break
-
-        # GC spacing between separate GC circuits (to avoid measuring the wrong one)
-
-    for n in nets:
-        # Verification: optical pin width mismatches
-        if n.type == _globals.PIN_TYPES.OPTICAL and not n.idx == None:
-            pin_paths = [p.path for p in n.pins]
-            if pin_paths[0].width != pin_paths[-1].width:
-                if verbose:
-                    print(" - Found mismatched pin widths: %s" % (pin_paths[0]))
-                r = pya.Region([pin_paths[0].to_itype(1).polygon(),
-                                pin_paths[-1].to_itype(1).polygon()])
-                polygon_merged = advance_iterator(r.each_merged())
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_mismatchedpin.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue( "Pin widths: %s, %s" % (pin_paths[0].width, pin_paths[-1].width)  ))
-                rdb_item.add_value(pya.RdbItemValue(polygon_merged.to_dtype(dbu)))
-
-    # displays results in Marker Database Browser, using Results Database (rdb)
-    if rdb.num_items() > 0:
-        v = pya.MessageBox.warning(
-            "Errors", "%s layout errors detected.  \nPlease review errors using the 'Marker Database Browser'." % rdb.num_items(), pya.MessageBox.Ok)
-        lv.show_rdb(rdb_i, cv.cell_index)
-    else:
-        v = pya.MessageBox.warning("Errors", "No layout errors detected.", pya.MessageBox.Ok)
-
-    # Save results of verification as a Text label on the cell. Include OS,
-    # SiEPIC-Tools and PDF version info.
-    LayerTextN = cell.layout().layer(TECHNOLOGY['Text'])
-    iter1 = cell.begin_shapes_rec(LayerTextN)
-    while not(iter1.at_end()):
-        if iter1.shape().is_text():
-            text = iter1.shape().text
-            if text.string.find("SiEPIC-Tools verification") > -1:
-                text_SiEPIC = text
-                print(" * Previous label: %s" % text_SiEPIC)
-                iter1.shape().delete()
-        iter1.next()
-
-    import SiEPIC.__init__
-    import sys
-    from time import strftime
-    text = pya.DText("SiEPIC-Tools verification: %s errors\n%s\nSiEPIC-Tools v%s\ntechnology: %s\n%s\nPython: %s, %s\n%s" % (rdb.num_items(), strftime("%Y-%m-%d %H:%M:%S"),
-                                                                                                                             SiEPIC.__init__.__version__, TECHNOLOGY['technology_name'], sys.platform, sys.version.split('\n')[0], sys.path[0], pya.Application.instance().version()), pya.DTrans(cell.dbbox().p1))
-    shape = cell.shapes(LayerTextN).insert(text)
-    shape.text_size = 0.1 / dbu
 
 '''
 Open all PDF files using an appropriate viewer
@@ -2996,17 +2536,17 @@ def open_folder(folder):
         print("running in windows explorer, %s" % folder)
         print(subprocess.Popen(r'explorer /select,"%s"' % folder))
 
-'''
-User to select opt_in labels, either:
- - Text object selection in the layout
- - GUI with drop-down menu from all labels in the layout
- - argument to the function, opt_in_selection_text, array of opt_in labels (strings)
-'''
 
 
-def user_select_opt_in(verbose=None, option_all=True, opt_in_selection_text=[]):
+def user_select_opt_in(cell=None, verbose=None, option_all=True, opt_in_selection_text=[]):
+    '''
+    User to select opt_in labels, either:
+     - Text object selection in the layout
+     - GUI with drop-down menu from all labels in the layout
+     - argument to the function, opt_in_selection_text, array of opt_in labels (strings)
+    '''
     from .utils import find_automated_measurement_labels
-    text_out, opt_in = find_automated_measurement_labels()
+    text_out, opt_in = find_automated_measurement_labels(topcell=cell)
     if not opt_in:
         print(' No opt_in labels found in the layout')
         return False, False
@@ -3535,7 +3075,52 @@ def button(self):
             wdg.show()
 
 
-def replace_cell(layout, cell_x_name, cell_y_name, cell_y_file=None, cell_y_library=None, Exact = True, RequiredCharacter = '$', debug = False):
+def layout_diff(cell1, cell2, tol = 1, verbose=True):
+    '''
+    Check two cells to make sure they are identical, within a tolerance.
+    Arguments:
+        cell1, cell2: pya.Cell()
+        tol = 1 nm
+    Returns:
+        Number of differences
+    Limitations:
+        Both cells should be part of the same layout.
+
+    Based on https://github.com/atait/lytest
+    '''
+
+    if not cell1.layout() == cell2.layout():
+        raise Exception ('SiEPIC.scripts.layout_diff is only implement for cells in the same layout.')
+
+    # Count the differences
+    diff_count = 0
+
+    # Get a list of the layers
+    layers = []
+    layout = cell1.layout()
+    for li in layout.layer_indices():
+        layers.append ( li )
+
+    # Do geometry checks on each layer
+    for li in layers:
+        r1 = pya.Region(cell1.begin_shapes_rec(li))
+        r2 = pya.Region(cell2.begin_shapes_rec(li))
+
+        rxor = r1 ^ r2
+
+        if tol > 0:
+            rxor.size(-tol)
+
+        if not rxor.is_empty():
+            diff_count += rxor.size()
+            if verbose:
+                print(
+                    f" - SiEPIC.scripts.layout_diff: {rxor.size()} differences found in {cell1.name} on layer {layout.get_info(li)}."
+                )
+    return diff_count
+    
+    
+def replace_cell(layout, cell_x_name, cell_y_name, cell_y_file=None, cell_y_library=None, Exact = True, RequiredCharacter = '$', run_layout_diff = True, debug = False):
     '''
     SiEPIC-Tools: scripts.replace_cell
     Search and replace: cell_x with cell_y
@@ -3555,9 +3140,9 @@ def replace_cell(layout, cell_x_name, cell_y_name, cell_y_file=None, cell_y_libr
     
     import os
     if debug:
-        print(" - cell replacement for: %s, with cell %s (%s), "  % (cell_x_name, cell_y_name, os.path.basename(cell_y_file)))
+        print(" - cell replacement for: %s, with cell %s (%s or %s), "  % (cell_x_name, cell_y_name, cell_y_file, cell_y_library))
     log = ''
-    log += "- cell replacement for: %s, with cell %s (%s)\n"  % (cell_x_name, cell_y_name, os.path.basename(cell_y_file))
+    log += "- cell replacement for: %s, with cell %s (%s or %s)\n"  % (cell_x_name, cell_y_name, cell_y_file, cell_y_library)
 
     # Find the cells that need replacement (cell_x)
     # find cell name exactly matching cell_x_name
@@ -3636,10 +3221,18 @@ def replace_cell(layout, cell_x_name, cell_y_name, cell_y_file=None, cell_y_libr
                         print('   - looking for cell. %s, %s, %s' % (cell_y_name, cell_y, layout.cell(cell_y_name)))
                         log += '   - Warning: cell destroyed, skipping replacement\n'
                         break  # skip this cell
+                    # Check if the BB cells are the same, by doing an XOR operation
+                    # from . import layout_diff
+                    if run_layout_diff:
+                        if layout_diff(inst.cell, cell_x, tol=0):
+                            if debug:
+                                print("    - black box cells are different: %s vs %s" % (inst.cell.name, cell_x.name))
+                            raise Exception ("    - black box cells are different: %s vs %s" % (inst.cell.name, cell_x.name))
+                            break;                        
                     # replace with CELL_Y
                     if inst.is_regular_array():
                         if debug:
-                            print("    - replacing %s in %s, with cell array: %s" % (cell_x.name, cc.name, cell_y.name))
+                            print("    - checked, and replaced %s in %s, with cell array: %s" % (cell_x.name, cc.name, cell_y.name))
                         ci = inst.cell_inst
                         cc.replace(inst, pya.CellInstArray(cell_y.cell_index(),inst.trans, ci.a, ci.b, ci.na, ci.nb))
                     else:
@@ -3758,29 +3351,33 @@ def export_layout(topcell, path, filename, relative_path = '', format='oas', scr
             
     if not success:
         try:
-            layout.write(file_out,save_options)
+            topcell.write(file_out,save_options)
         except:
             try:
-                layout.write(file_out)
+                topcell.write(file_out)
             except:
                 raise Exception("Problem exporting your layout, %s." % file_out)
 
     return file_out
 
-def instantiate_all_library_cells(topcell, progress_bar = True):
+def instantiate_all_library_cells(topcell, terminator_cells = None, terminator_libraries = None, terminator_waveguide_types = None, progress_bar = True):
     '''
     Load all cells (fixed and PCells) and instantiate them on the layout. 
     One column per library, one column for fixed and PCells.
     topcell: is a cell in a pya.Layout that has already configured with Layout.technology_name 
+    terminator_cells: list of str, attach a terminator to each of the optical ports for each cell, and verify
+        terminator_libraries: list of str, the library name corresponding to each terminator
+        terminator_waveguide_types: list of str, waveguide type corresponding to each terminator
     progress_bar: True displays percentage
     '''
     
+    print('v2')
     
     from SiEPIC._globals import Python_Env
+    ly = topcell.layout()
     if True or Python_Env == "KLayout_GUI":
         # Count all the cells for the progress bar
         count = 0
-        ly = topcell.layout()
         for lib in pya.Library().library_ids():
             li = pya.Library().library_by_id(lib)
             if not li.is_for_technology(ly.technology_name) or li.name() == 'Basic':
@@ -3793,25 +3390,48 @@ def instantiate_all_library_cells(topcell, progress_bar = True):
                     count += 1
         p = pya.RelativeProgress("Instantiate all libraries' cells", count)
 
+    if terminator_cells:
+        # load terminator cell
+        from SiEPIC.utils import create_cell2
+        cell_terminators = []
+        for i in range(0, len(terminator_cells)):
+            cell_terminators.append(
+                create_cell2(ly, terminator_cells[i], terminator_libraries[i])
+            )
+        from SiEPIC.scripts import connect_cell
+        from SiEPIC import _globals
+        
     # all the libraries
     ly = topcell.layout()
     x,y,xmax=0,0,0
     for lib in pya.Library().library_ids():
         li = pya.Library().library_by_id(lib)
-        if not li.is_for_technology(ly.technology_name) or li.name() == 'Basic':
-            print(' - skipping: %s' % li.name())
+        if not li.is_for_technology(ly.technology_name) or li.name() == "Basic":
+            print(" - skipping library: %s" % li.name())
             continue
 
         # all the pcells
-        print('All PCells: %s' % li.layout().pcell_names())
+        print(" - Library: %s" % li.name())
+        print("   All PCells: %s" % li.layout().pcell_names())
         for n in li.layout().pcell_names():
-            print(" - PCell: ", li.name(), n)
-            pcell = ly.create_cell(n,li.name(), {})
+            print("   - PCell: ", li.name(), n)
+            pcell = ly.create_cell(n, li.name(), {})
             if pcell:
-                t = pya.Trans(pya.Trans.R0, x-pcell.bbox().left, y-pcell.bbox().bottom)
-                topcell.insert(pya.CellInstArray(pcell.cell_index(), t))
-                y += pcell.bbox().height()+2000
-                xmax = max(xmax, x+pcell.bbox().width()+2000)
+                subcell = ly.create_cell('c_'+n)
+                t = pya.Trans(pya.Trans.R0, pcell.bbox().left, pcell.bbox().bottom)
+                inst = subcell.insert(pya.CellInstArray(pcell.cell_index(), t))
+                # connect terminators
+                if terminator_cells:
+                    pins, _ = pcell.find_pins()
+                    if pins:
+                        for p1 in pins:
+                            if p1.type == _globals.PIN_TYPES.OPTICAL:
+                                connect_cell(inst,p1.pin_name, cell_terminators[0],'1',relaxed_pinnames=True)
+                t = pya.Trans(pya.Trans.R0, x-subcell.bbox().left, y-subcell.bbox().bottom)
+                inst = topcell.insert(pya.CellInstArray(subcell.cell_index(), t))
+
+                y += subcell.bbox().height()+2000
+                xmax = max(xmax, x+subcell.bbox().width()+2000)
             else:
                 print('Error in: %s' % n)
             p.inc()
@@ -3821,15 +3441,26 @@ def instantiate_all_library_cells(topcell, progress_bar = True):
         for c in li.layout().each_top_cell():
             # instantiate
             if not li.layout().cell(c).is_pcell_variant():
-                print(" - Fixed cell: ", li.name(), li.layout().cell(c).name)
-                pcell = ly.create_cell(li.layout().cell(c).name,li.name(), {})
+                print("   - Fixed cell: ", li.name(), li.layout().cell(c).name)
+                pcell = ly.create_cell(li.layout().cell(c).name, li.name(), {})
                 if not pcell:
                     pcell = ly.create_cell(li.layout().cell(c).name,li.name())
                 if pcell:
-                    t = pya.Trans(pya.Trans.R0, x-pcell.bbox().left, y-pcell.bbox().bottom)
-                    topcell.insert(pya.CellInstArray(pcell.cell_index(), t))
-                    y += pcell.bbox().height()+2000
-                    xmax = max(xmax, x+pcell.bbox().width()+2000)
+                    subcell = ly.create_cell('c_'+pcell.name)
+                    t = pya.Trans(pya.Trans.R0, pcell.bbox().left, pcell.bbox().bottom)
+                    inst = subcell.insert(pya.CellInstArray(pcell.cell_index(), t))
+                    # connect terminators
+                    if terminator_cells:
+                        pins, _ = pcell.find_pins()
+                        if pins:
+                            for p1 in pins:
+                                if p1.type == _globals.PIN_TYPES.OPTICAL:
+                                    connect_cell(inst,p1.pin_name, cell_terminators[0],'1',relaxed_pinnames=True)
+                    t = pya.Trans(pya.Trans.R0, x-subcell.bbox().left, y-subcell.bbox().bottom)
+                    inst = topcell.insert(pya.CellInstArray(subcell.cell_index(), t))
+
+                    y += subcell.bbox().height()+2000
+                    xmax = max(xmax, x+subcell.bbox().width()+2000)
                 else:
                     print('Error in: %s' % li.layout().cell(c).name)
             p.inc()
@@ -3838,3 +3469,294 @@ def instantiate_all_library_cells(topcell, progress_bar = True):
     if True or Python_Env == "KLayout_GUI":
         p.destroy
 
+def load_klayout_library(technology, library_name=None, library_description='', folder_gds=None, folder_pcell=None, verbose=True):
+    '''
+    Load KLayout Library
+        Loads PCells and fixed cells from sub folders, 
+        creates a KLayout pya.Library,
+        registers the library with the technology name.
+    Inputs:
+        technology: name of the technology, e.g., "EBeam", or pya.Technology
+        library_name: name of the library
+        library_description: description of the library
+        folder_gds: relative sub-folder (within the technology folder) from which to load .gds/.oas fixed cells
+        folder_pcell: relative sub-folder (within the technology folder) from which to load .py PCells
+            or absolute; check both.
+    returns:
+        pya.Library name
+    '''
+
+    if type(technology) == str:
+        tech = pya.Technology.technology_by_name(technology)
+        if not tech:
+            raise Exception('SiEPIC.load_klayout_library cannot load technology: %s' % technology)
+        tech_name = technology
+    elif type(technology) == pya.Technology:
+        tech = technology
+        if not tech:
+            raise Exception('SiEPIC.load_klayout_library cannot load technology: %s' % technology)
+        tech_name = technology.name
+    else:
+        raise Exception('SiEPIC.load_klayout_library requires a technology as input.')
+        
+    if not library_name:
+        library_name = tech_name
+        
+    import os
+    import pathlib
+    import sys
+
+    if verbose:
+        print(' - Technology path: %s' % tech.default_base_path)
+
+    import importlib.util
+    import sys
+    
+    def import_module_from_path(module_name, file_path):
+        '''
+        import a Python module given a path 
+        '''
+        import importlib.util
+        import sys
+        from pathlib import Path
+        
+        file_path = os.path.join(file_path, '__init__.py')
+        path = Path(file_path).resolve()
+        if verbose:
+            print(' - PCell init file: %s' % path)
+        spec = importlib.util.spec_from_file_location(module_name, path)
+        if not spec:
+            raise Exception('SiEPIC.load_klayout_library cannot import module: %s, from path: %s ' % (module_name,path))
+        module = importlib.util.module_from_spec(spec)
+        sys.modules[module_name] = module  # Add it to sys.modules
+        spec.loader.exec_module(module)  # Execute the module code   
+             
+        return module
+
+    # Load all Python PCells
+    if folder_pcell:
+        import importlib
+        importlib.invalidate_caches()
+
+        # Import the Python folder as a module
+        folder_pcell_abs = os.path.abspath(os.path.join(tech.default_base_path, folder_pcell))
+        if not os.path.exists (folder_pcell_abs):
+            if os.path.exists (folder_pcell):
+                folder_pcell_abs = folder_pcell
+            else:
+                raise Exception('Folder paths "%s" or "%s" do not exist.' % (folder_pcell_abs, folder_pcell))
+        if verbose:
+            print(' - PCell folder path: %s' % folder_pcell_abs)
+        module_name = os.path.split(folder_pcell)[-1]
+        if verbose:
+            print(' - PCell module name: %s' % module_name)
+        module = import_module_from_path(module_name, folder_pcell_abs)
+        globals()[module_name] = module
+        
+        # Import all the PCell python files
+        pcells_=[]
+        files = [f for f in os.listdir(folder_pcell_abs) if '.py' in pathlib.Path(f).suffixes  and '__init__' not in f]
+        for f in files:
+            submodule = '%s.%s' % (module_name, f.replace('.py','')) 
+            # m = importlib.import_module(submodule)  
+            m = importlib.import_module('.'+f.replace('.py',''), package=module_name)  
+            if not m:
+                raise Exception('SiEPIC.load_klayout_library cannot import module: %s, from path: %s ' % (submodule,folder_pcell_abs))
+            if verbose:
+                print(' - imported PCell: %s' % submodule)
+            pcells_.append(importlib.reload(m))
+        if verbose:
+            print(' - module dir(): %s' % dir(module))
+
+        if not type(module) == type(os):
+            raise Exception('SiEPIC.load_klayout_library cannot import module.')
+        
+    # Create the KLayout library, using GDS and Python PCells
+    class library(pya.Library):
+        def __init__(self):
+            self.technology=tech_name
+            if verbose:
+                print(" - Initializing '%s' Library." % library_name)
+
+            # Set the description
+            self.description = library_description 
+
+            self.register(library_name)
+
+            count_pcells = 0
+            count_fixed_cells = 0
+
+            # Import all the GDS/OASIS files from the tech folder
+            if folder_gds:
+                import os, fnmatch
+                dir_path = os.path.abspath(os.path.join(tech.default_base_path, folder_gds))
+                if not os.path.exists (dir_path):
+                    if os.path.exists (folder_gds):
+                        dir_path = folder_pcell
+                    else:
+                        raise Exception('Folder paths "%s" or "%s" do not exist.' % (dir_path, folder_gds))
+                if verbose:
+                    print(' - GDS/OAS folder path: %s' % dir_path)
+                search_strs = ['*.[Oo][Aa][Ss]', '*.[Gg][Dd][Ss]'] # OAS, GDS
+                found = False
+                for search_str in search_strs:
+                    for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
+                        for filename in fnmatch.filter(filenames, search_str):
+                            file1=os.path.join(root, filename)
+                            if verbose:
+                                print(" - reading %s" % filename )
+                            self.layout().read(file1)
+                            found = True
+                            count_fixed_cells += 1
+                if not found:
+                    print(' - Warning: no fixed GDS/OAS files found for library: %s, in folder: %s' % (library_name, dir_path))
+                else:
+                    if verbose:
+                        for c in self.layout().top_cells():
+                            print("   - cell: %s" % c.name )
+
+            # Create the PCell declarations
+            if folder_pcell:
+                if not pcells_:
+                    print(' - Warning: no PCells found for library: %s' % library_name)
+                for m in pcells_:
+                    mm = m.__name__.replace('%s.' % module_name,'')
+                    # mm2 = m.__name__+'.'+mm+'()'
+                    mm2 = 'module'+'.'+mm+'.'+mm+'()'
+                    if verbose:
+                        print(' - register_pcell %s, %s' % (mm,mm2))
+                    # self.layout().register_pcell(mm, eval(mm2))
+                    self.layout().register_pcell(mm, getattr(m,mm)())
+                    count_pcells += 1
+                            
+                if verbose:
+                    print(' - done loading pcells')
+            
+            # Register us the library with the technology name
+            # If a library with that name already existed, it will be replaced then.
+            self.register(library_name)
+            
+            self.count_fixed_cells = count_fixed_cells
+            self.count_pcells = count_pcells
+         
+    lib = library()
+    
+    # Return the library name, and number of cells loaded
+    return library_name, lib.count_fixed_cells, lib.count_pcells
+
+def technology_libraries(technology):
+    '''
+    Function to get a list of all the pya.Library associated with a pya.Technology
+    missing in KLayout: https://github.com/KLayout/klayout/issues/879
+                        https://www.klayout.de/doc-qt5/code/class_Technology.html
+    Inputs:
+        technology: name of the technology, e.g., "EBeam", or pya.Technology
+    '''
+
+    if type(technology) == str:
+        tech = pya.Technology.technology_by_name(technology)
+        if not tech:
+            raise Exception('SiEPIC.load_klayout_library cannot load technology: %s' % technology)
+        tech_name = technology
+    elif type(technology) == pya.Technology:
+        tech = technology
+        if not tech:
+            raise Exception('SiEPIC.load_klayout_library cannot load technology: %s' % technology)
+        tech_name = technology.name
+    else:
+        raise Exception('SiEPIC.load_klayout_library requires a technology as input.')
+        
+    tech_libs = []
+    libs = pya.Library.library_ids()
+    for lib in libs:
+        l = pya.Library.library_by_id(lib)
+        if tech_name in l.technologies():
+            tech_libs.append(l.name())
+
+    print('Libraries associated with Technology %s: %s' % (tech_name, tech_libs))     
+    
+def version_latest():
+    '''
+    Compare to the current version
+    '''
+    
+    import requests
+    import concurrent.futures
+
+    import time
+    # Start timer
+    start_time = time.time()
+
+
+    def get_latest_version(package_name, request_timeout=1):
+        #print(f"fetching version for {package_name}.")
+        url = f"https://pypi.org/pypi/{package_name}/json"
+        try:
+            response = requests.get(url, timeout=request_timeout)
+            if response.status_code == 200:
+                data = response.json()
+                return data["info"]["version"]
+        except requests.RequestException as e:
+            print(f"Error fetching version for {package_name}: {e}")
+        return None
+
+    # Function to run the version check asynchronously
+    def check_version_async(package_name, request_timeout=1):
+        with concurrent.futures.ThreadPoolExecutor() as executor:
+            future = executor.submit(get_latest_version, package_name, request_timeout)
+            # future.result() can be called later when needed, allowing for background execution
+            # print(f"future: {future}")
+            return future
+    
+    # Example usage:
+    package_name = "SiEPIC"
+    future = check_version_async(package_name)    
+    #print(f"future: {future}")
+    execution_time = time.time() - start_time
+    #print(f"Execution time: {execution_time} seconds")
+    return future
+    
+def version_check():
+    '''
+    Query the PyPI Python database to find out the latest version of SiEPIC
+    '''
+
+    import SiEPIC
+    version_future = SiEPIC.scripts.version_latest()
+    if not version_future:
+        return None
+
+    #from time import sleep 
+    #sleep(0.2)
+    
+    while not version_future.done():
+        print("Continuing with application startup tasks...")
+        time.sleep(0.05)  # Emulate work done in the main thread
+    
+    import concurrent.futures
+    # Later, when the version is needed, you can check the result (it may already be ready)
+    # with a timeout for the future (e.g., 1 second)
+    try:
+        latest_version = version_future.result(timeout=0.1)  # Set max wait for result
+    except concurrent.futures.TimeoutError:
+        print("The SiEPIC version check took too long and was cancelled.")
+        return 
+
+    if not latest_version:
+        return 
+    
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse(latest_version):
+        if Python_Env == 'KLayout_GUI':
+            pya.MessageBox.warning(
+                "Update SiEPIC-Tools", f'New version of SiEPIC-Tools is available ({latest_version} vs {SiEPIC.__version__}).' , pya.MessageBox.Ok)
+        else:
+            print(f'New version of SiEPIC-Tools is available ({latest_version} vs {SiEPIC.__version__}).' )
+    else:
+        print(f'SiEPIC-Tools is up to date ({latest_version} vs {SiEPIC.__version__}).' )
+        
+
+        
+    
\ No newline at end of file
diff --git a/klayout_dot_config/python/SiEPIC/setup.py b/klayout_dot_config/python/SiEPIC/setup.py
index 584b3e2fe..14469b4fb 100644
--- a/klayout_dot_config/python/SiEPIC/setup.py
+++ b/klayout_dot_config/python/SiEPIC/setup.py
@@ -149,6 +149,7 @@ def registerKeyBindings():
     # turn the hash back into a config string
     config = ''.join('{}:{};'.format(key, val) for key, val in sorted(mapping.items()))[:-1]
     pya.Application.instance().set_config('key-bindings', config)
+    
     pya.Application.instance().set_config('edit-connect-angle-mode', 'ortho')
     pya.Application.instance().set_config('edit-inst-angle', '0')
     pya.Application.instance().set_config('edit-move-angle-mode', 'diagonal')
@@ -160,6 +161,8 @@ def registerKeyBindings():
     pya.Application.instance().set_config('guiding-shape-line-width', '0')
     pya.Application.instance().set_config('rdb-marker-color', '#ff0000')
     pya.Application.instance().set_config('rdb-marker-line-width', '8')
+#    pya.Application.instance().set_config('compression-level', '10')
+#    pya.Application.instance().set_config('write-cblocks', 'true')
 #    pya.Application.instance().set_config('default-layer-properties', os.path.join(os.path.realpath(__file__), os.pardir, os.pardir, os.pardir, 'libraries', 'klayout_Layers_EBeam.lyp'))
 
     if pya.Application.instance().get_config('edit-mode') == 'false':
diff --git a/klayout_dot_config/python/SiEPIC/simulation/contraDC/contra_directional_coupler/ContraDC.py b/klayout_dot_config/python/SiEPIC/simulation/contraDC/contra_directional_coupler/ContraDC.py
index 2e1f19483..4ddd844e5 100644
--- a/klayout_dot_config/python/SiEPIC/simulation/contraDC/contra_directional_coupler/ContraDC.py
+++ b/klayout_dot_config/python/SiEPIC/simulation/contraDC/contra_directional_coupler/ContraDC.py
@@ -521,8 +521,7 @@ def gen_sparams(self, filepath='', make_plot=True):
 
         # from lumerical_tools import generate_dat
         # generate_dat()
-        self.generate_dat(filepath=filepath, make_plot=make_plot)
-        return self
+        return self.generate_dat(filepath=filepath, make_plot=make_plot)
 
     def generate_dat(self, filepath='', make_plot=True):
         import numpy as np
@@ -656,8 +655,7 @@ def generate_dat(self, filepath='', make_plot=True):
             period=self.period[0]
         else:
             period=self.period
-        filename =  "w1=" + "%.0f"%(w1*1e9) + ",w2=" + "%.0f"%(w2*1e9) + ",dW1=" + "%.0f"%(self.dw1*1e9) + ",dW2=" + "%.0f"%(self.dw2*1e9) +  ",gap=" + "%.0f"%(self.gap*1e9) + ",p=" + "%.1f"%(period*1e9) + ",N=" + str(self.N) + ",s=" + str(1 if self.sinusoidal else 0) +  ",a=" + "%.2f"%self.a +  ",rib=" + str(1 if self.rib else 0) + ",pol=" + str(0 if self.pol=='TE' else 1) + ",l1=" + "%.0f"%(self.wvl_range[0]*1e9) + ",l2=" + "%.0f"%(self.wvl_range[1]*1e9) + ",ln=" + str(self.resolution) + '.dat';
-        
+        filename = f"w1={w1*1e9:.0f},w2={w2*1e9:.0f},dW1={self.dw1*1e9:.0f},dW2={self.dw2*1e9:.0f},gap={self.gap*1e9:.0f},p={period*1e9:.1f},N={int(self.N)},s={1 if self.sinusoidal else 0},a={self.a:.2f},rib={1 if self.rib else 0},pol={0 if self.pol=='TE' else 1},l1={self.wvl_range[0]*1e9:.0f},l2={self.wvl_range[1]*1e9:.0f},ln={self.resolution}.dat"
         import os
         path = os.path.join(filepath,filename)
 
@@ -744,7 +742,7 @@ def generate_dat(self, filepath='', make_plot=True):
             f.write(f"('port 4',{mode_label},{mode_ID},'port 4',{mode_ID},'transmission')\n")
             f.write(f"({FREQ_PTS},3)\n")
             np.savetxt(f, S44_data, fmt="%.6e", delimiter=" ")
-        return path
+        return filename
 
     def getGroupDelay(self):
         """Calculates the group delay of the device,
diff --git a/klayout_dot_config/python/SiEPIC/simulation/contraDC/klayout_gui.py b/klayout_dot_config/python/SiEPIC/simulation/contraDC/klayout_gui.py
index b15459ab3..892cc0462 100644
--- a/klayout_dot_config/python/SiEPIC/simulation/contraDC/klayout_gui.py
+++ b/klayout_dot_config/python/SiEPIC/simulation/contraDC/klayout_gui.py
@@ -1,13 +1,18 @@
-
 # Required packages
 from SiEPIC.install import install
-if not install('scipy', requested_by='Contra Directional Coupler design'):
-  pya.MessageBox.warning(
-  "Missing package", "The simulator does not function without the package 'scipy'.",  pya.MessageBox.Ok)    
-if not install('plotly', requested_by='Contra Directional Coupler design'):
-  pya.MessageBox.warning(
-  "Missing package", "The simulator does not function without the package 'plotly'.",  pya.MessageBox.Ok)    
 
+if not install("scipy", requested_by="Contra Directional Coupler design"):
+    pya.MessageBox.warning(
+        "Missing package",
+        "The simulator does not function without the package 'scipy'.",
+        pya.MessageBox.Ok,
+    )
+if not install("plotly", requested_by="Contra Directional Coupler design"):
+    pya.MessageBox.warning(
+        "Missing package",
+        "The simulator does not function without the package 'plotly'.",
+        pya.MessageBox.Ok,
+    )
 
 
 from SiEPIC.simulation.contraDC.contra_directional_coupler.ContraDC import *
@@ -16,106 +21,117 @@
 import plotly.graph_objs as go
 import plotly.offline as pyo
 import plotly.io as pio
+
 pio.renderers.default = "browser"
 
+
 class MyWindow(pya.QWidget):
 
     def __init__(self):
         super().__init__()
-        self.setWindowTitle('Contra-directional coupler simulator')
+        self.setWindowTitle("Contra-directional coupler simulator")
         self.setMinimumSize(200, 200)
 
         # fetch pcell parameters
         if self.load_pcell_params() == 0:
             self.close()
-        
+
         # fetch technology parameters
         self.load_DFT()
 
-        #******************************************************
+        # ******************************************************
         # Create the layout_pcell and add the UI elements to it
-        from pya import QVBoxLayout, QPushButton, QLabel, QLineEdit, QCheckBox, QComboBox, QHBoxLayout
+        from pya import (
+            QVBoxLayout,
+            QPushButton,
+            QLabel,
+            QLineEdit,
+            QCheckBox,
+            QComboBox,
+            QHBoxLayout,
+        )
+
         layout_pcell = QVBoxLayout()
-        self.button = QPushButton('Refresh PCell')
+        self.button = QPushButton("Refresh PCell")
         # Connect the button to a callback function
         self.button.clicked(self.on_refresh_clicked)
 
-        self.label_pcell = QLabel('Parameterized cell definitions:')
+        self.label_pcell = QLabel("Parameterized cell definitions:")
         layout_pcell.addWidget(self.label_pcell)
 
-        self.pcell_N_label = QLabel('Number of gratings (N) (µm): ')
-        self.pcell_N_fill = QLineEdit(str(self.params['number_of_periods']))
+        self.pcell_N_label = QLabel("Number of gratings (N) (µm): ")
+        self.pcell_N_fill = QLineEdit(str(self.params["number_of_periods"]))
         self.pcell_N_fill.setReadOnly(True)
-        self.pcell_N_fill.setStyleSheet('color: gray')
+        self.pcell_N_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_N_label)
         layout_pcell.addWidget(self.pcell_N_fill)
 
-        self.pcell_period_label = QLabel('Gratings period (Λ) (µm): ')
-        self.pcell_period_fill = QLineEdit(str(self.params['grating_period']))
+        self.pcell_period_label = QLabel("Gratings period (Λ) (µm): ")
+        self.pcell_period_fill = QLineEdit(str(self.params["grating_period"]))
         self.pcell_period_fill.setReadOnly(True)
-        self.pcell_period_fill.setStyleSheet('color: gray')
+        self.pcell_period_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_period_label)
         layout_pcell.addWidget(self.pcell_period_fill)
 
-        self.pcell_gap_label = QLabel('Waveguides gap (G) (µm): ')
-        self.pcell_gap_fill = QLineEdit(str(self.params['gap']))
+        self.pcell_gap_label = QLabel("Waveguides gap (G) (µm): ")
+        self.pcell_gap_fill = QLineEdit(str(self.params["gap"]))
         self.pcell_gap_fill.setReadOnly(True)
-        self.pcell_gap_fill.setStyleSheet('color: gray')
+        self.pcell_gap_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_gap_label)
         layout_pcell.addWidget(self.pcell_gap_fill)
 
-        self.pcell_w1_label = QLabel('Waveguide 1 width (W1) (µm): ')
-        self.pcell_w1_fill = QLineEdit(str(self.params['wg1_width']))
+        self.pcell_w1_label = QLabel("Waveguide 1 width (W1) (µm): ")
+        self.pcell_w1_fill = QLineEdit(str(self.params["wg1_width"]))
         self.pcell_w1_fill.setReadOnly(True)
-        self.pcell_w1_fill.setStyleSheet('color: gray')
+        self.pcell_w1_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_w1_label)
         layout_pcell.addWidget(self.pcell_w1_fill)
 
-        self.pcell_dw1_label = QLabel('Waveguide 1 Δwidth (ΔW1) (µm): ')
-        self.pcell_dw1_fill = QLineEdit(str(self.params['corrugation1_width']))
+        self.pcell_dw1_label = QLabel("Waveguide 1 Δwidth (ΔW1) (µm): ")
+        self.pcell_dw1_fill = QLineEdit(str(self.params["corrugation1_width"]))
         self.pcell_dw1_fill.setReadOnly(True)
-        self.pcell_dw1_fill.setStyleSheet('color: gray')
+        self.pcell_dw1_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_dw1_label)
         layout_pcell.addWidget(self.pcell_dw1_fill)
 
-        self.pcell_w2_label = QLabel('Waveguide 2 width (W2) (µm): ')
-        self.pcell_w2_fill = QLineEdit(str(self.params['wg2_width']))
+        self.pcell_w2_label = QLabel("Waveguide 2 width (W2) (µm): ")
+        self.pcell_w2_fill = QLineEdit(str(self.params["wg2_width"]))
         self.pcell_w2_fill.setReadOnly(True)
-        self.pcell_w2_fill.setStyleSheet('color: gray')
+        self.pcell_w2_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_w2_label)
         layout_pcell.addWidget(self.pcell_w2_fill)
 
-        self.pcell_dw2_label = QLabel('Waveguide 2 Δwidth (ΔW2) (µm): ')
-        self.pcell_dw2_fill = QLineEdit(str(self.params['corrugation2_width']))
+        self.pcell_dw2_label = QLabel("Waveguide 2 Δwidth (ΔW2) (µm): ")
+        self.pcell_dw2_fill = QLineEdit(str(self.params["corrugation2_width"]))
         self.pcell_dw2_fill.setReadOnly(True)
-        self.pcell_dw2_fill.setStyleSheet('color: gray')
+        self.pcell_dw2_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_dw2_label)
         layout_pcell.addWidget(self.pcell_dw2_fill)
 
-        self.pcell_apod_label = QLabel('Apodization index (a): ')
-        self.pcell_apod_fill = QLineEdit(str(self.params['apodization_index']))
+        self.pcell_apod_label = QLabel("Apodization index (a): ")
+        self.pcell_apod_fill = QLineEdit(str(self.params["apodization_index"]))
         self.pcell_apod_fill.setReadOnly(True)
-        self.pcell_apod_fill.setStyleSheet('color: gray')
+        self.pcell_apod_fill.setStyleSheet("color: gray")
         layout_pcell.addWidget(self.pcell_apod_label)
         layout_pcell.addWidget(self.pcell_apod_fill)
 
-        self.pcell_rib_label = QLabel('Rib waveguides? ')
+        self.pcell_rib_label = QLabel("Rib waveguides? ")
         self.pcell_rib_fill = QCheckBox()
-        self.pcell_rib_fill.setChecked(self.params['rib'])
+        self.pcell_rib_fill.setChecked(self.params["rib"])
         layout_pcell.addWidget(self.pcell_rib_label)
         layout_pcell.addWidget(self.pcell_rib_fill)
         self.pcell_rib_fill.clicked(self.on_rib_click)
 
         layout_pcell.addWidget(self.button)
 
-        #******************************************************
+        # ******************************************************
         # add simulation box and add the UI elements to it
         layout_sim = QVBoxLayout()
-        self.label_sim = QLabel('Simulation definitions:')
+        self.label_sim = QLabel("Simulation definitions:")
         layout_sim.addWidget(self.label_sim)
 
         # Create a dropdown menu to select simulation import type
-        self.sim_import_label = QLabel('Import simulation definitions from:')
+        self.sim_import_label = QLabel("Import simulation definitions from:")
         self.sim_import = QComboBox()
         self.sim_import.addItem("PDK definitions")
         self.sim_import.addItem("Custom")
@@ -125,29 +141,29 @@ def __init__(self):
         # Connect the dropdown menu to a slot function
         self.sim_import.currentIndexChanged(self.on_sim_import)
 
-        self.sim_wavlstart_label = QLabel('Start wavelength (µm): ')
+        self.sim_wavlstart_label = QLabel("Start wavelength (µm): ")
         self.sim_wavlstart_fill = QLineEdit(str(self.wavl_start))
         self.sim_wavlstart_fill.setReadOnly(True)
-        self.sim_wavlstart_fill.setStyleSheet('color: gray')
+        self.sim_wavlstart_fill.setStyleSheet("color: gray")
         layout_sim.addWidget(self.sim_wavlstart_label)
         layout_sim.addWidget(self.sim_wavlstart_fill)
 
-        self.sim_wavlstop_label = QLabel('Stop wavelength (µm): ')
+        self.sim_wavlstop_label = QLabel("Stop wavelength (µm): ")
         self.sim_wavlstop_fill = QLineEdit(str(self.wavl_stop))
         self.sim_wavlstop_fill.setReadOnly(True)
-        self.sim_wavlstop_fill.setStyleSheet('color: gray')
+        self.sim_wavlstop_fill.setStyleSheet("color: gray")
         layout_sim.addWidget(self.sim_wavlstop_label)
         layout_sim.addWidget(self.sim_wavlstop_fill)
 
-        self.sim_wavlpts_label = QLabel('Wavelength points: ')
+        self.sim_wavlpts_label = QLabel("Wavelength points: ")
         self.sim_wavlpts_fill = QLineEdit(str(self.wavl_pts))
         self.sim_wavlpts_fill.setReadOnly(True)
-        self.sim_wavlpts_fill.setStyleSheet('color: gray')
+        self.sim_wavlpts_fill.setStyleSheet("color: gray")
         layout_sim.addWidget(self.sim_wavlpts_label)
         layout_sim.addWidget(self.sim_wavlpts_fill)
 
         # Polarization
-        self.sim_pol_label = QLabel('Polarization: ')
+        self.sim_pol_label = QLabel("Polarization: ")
         self.sim_pol_dropdown = QComboBox()
         self.sim_pol_dropdown.addItem("TE")
         self.sim_pol_dropdown.addItem("TM")
@@ -155,13 +171,13 @@ def __init__(self):
         layout_sim.addWidget(self.sim_pol_dropdown)
 
         # coupling coefficient
-        self.sim_kappa_label = QLabel('Coupling coefficient (κ, /m): ')
+        self.sim_kappa_label = QLabel("Coupling coefficient (κ, /m): ")
         self.sim_kappa_dropdown = QComboBox()
         self.sim_kappa_dropdown.addItem("User defined")
         self.sim_kappa_dropdown.addItem("Simulate")
-        self.sim_kappa_fill = QLineEdit('24000')
+        self.sim_kappa_fill = QLineEdit("24000")
         self.sim_kappa_fill.setReadOnly(True)
-        self.sim_kappa_fill.setStyleSheet('color: gray')
+        self.sim_kappa_fill.setStyleSheet("color: gray")
         layout_sim.addWidget(self.sim_kappa_label)
         layout_sim.addWidget(self.sim_kappa_dropdown)
         layout_sim.addWidget(self.sim_kappa_fill)
@@ -170,7 +186,7 @@ def __init__(self):
         self.sim_kappa_dropdown.currentIndexChanged(self.on_kappa_dropdown)
 
         # waveguide models
-        self.sim_wg_label = QLabel('Waveguide models: ')
+        self.sim_wg_label = QLabel("Waveguide models: ")
         self.sim_wg_dropdown = QComboBox()
         self.sim_wg_dropdown.addItem("Lookup table")
         self.sim_wg_dropdown.addItem("Simulate")
@@ -180,57 +196,57 @@ def __init__(self):
         # Connect the dropdown menu to a slot function
         self.sim_wg_dropdown.currentIndexChanged(self.on_wg_dropdown)
 
-        #******************************************************
+        # ******************************************************
         # add technology box and add the UI elements to it
         layout_tech = QVBoxLayout()
-        
-        self.label_tech = QLabel('Technology definitions:')
+
+        self.label_tech = QLabel("Technology definitions:")
         layout_tech.addWidget(self.label_tech)
 
         # Create a dropdown menu to select simulation import type
-        self.tech_import_label = QLabel('Import techonology definitions from: ')
+        self.tech_import_label = QLabel("Import techonology definitions from: ")
         self.tech_import = QComboBox()
         self.tech_import.addItem("PDK definitions")
         self.tech_import.addItem("Custom")
         layout_tech.addWidget(self.tech_import_label)
         layout_tech.addWidget(self.tech_import)
 
-        self.tech_devthick_label = QLabel('Waveguide thickness (µm): ')
-        self.tech_devthick_fill = QLineEdit('0.22')
+        self.tech_devthick_label = QLabel("Waveguide thickness (µm): ")
+        self.tech_devthick_fill = QLineEdit("0.22")
         self.tech_devthick_fill.setReadOnly(False)
         layout_tech.addWidget(self.tech_devthick_label)
         layout_tech.addWidget(self.tech_devthick_fill)
 
-        self.tech_ribthick_label = QLabel('Rib thickness (µm): ')
+        self.tech_ribthick_label = QLabel("Rib thickness (µm): ")
         if self.pcell_rib_fill.isChecked():
-          self.tech_ribthick_fill = QLineEdit('0.09')
-          self.tech_ribthick_fill.setReadOnly(False)
+            self.tech_ribthick_fill = QLineEdit("0.09")
+            self.tech_ribthick_fill.setReadOnly(False)
         else:
-          self.tech_ribthick_fill = QLineEdit('0.0')
-          self.tech_ribthick_fill.setReadOnly(True)
-          self.tech_ribthick_fill.setStyleSheet('color: gray')
+            self.tech_ribthick_fill = QLineEdit("0.0")
+            self.tech_ribthick_fill.setReadOnly(True)
+            self.tech_ribthick_fill.setStyleSheet("color: gray")
         layout_tech.addWidget(self.tech_ribthick_label)
         layout_tech.addWidget(self.tech_ribthick_fill)
 
-        self.tech_plot_label = QLabel('Plot result? ')
+        self.tech_plot_label = QLabel("Plot result? ")
         self.tech_plot_fill = QCheckBox()
         self.tech_plot_fill.setChecked(True)
         layout_tech.addWidget(self.tech_plot_label)
         layout_tech.addWidget(self.tech_plot_fill)
 
-        self.tech_cm_label = QLabel('Generate compact model? ')
+        self.tech_cm_label = QLabel("Generate compact model? ")
         self.tech_cm_fill = QCheckBox()
         self.tech_cm_fill.setChecked(True)
         layout_tech.addWidget(self.tech_cm_label)
         layout_tech.addWidget(self.tech_cm_fill)
 
-        self.simulate = QPushButton('Run simulation')
+        self.simulate = QPushButton("Run simulation")
         layout_tech.addWidget(self.simulate)
 
         # Connect the button to a callback function
         self.simulate.clicked(self.on_simulate_clicked)
 
-        #******************************************************
+        # ******************************************************
         # assemble and order the menus
         layout_pcell.addStretch()
         layout_sim.addStretch()
@@ -247,171 +263,267 @@ def __init__(self):
 
         self.setLayout(vbox)
 
+    def condition_xml(self, element, indent='    ', level=0):
+        """Recursively add indentation and line breaks to the XML tree."""
+        if element:  # checks if element is not None and not an empty string/list
+            if not element.text or not element.text.strip():
+                element.text = f"\n{indent * (level+1)}"
+            if not element.tail or not element.tail.strip():
+                element.tail = f"\n{indent * level}"
+            for elem in element:
+                self.condition_xml(elem, indent, level+1)
+        else:
+            if level and (not element.tail or not element.tail.strip()):
+                element.tail = f"\n{indent * (level-1)}"
 
     def on_simulate_clicked(self):
-        
-        
-
-        N = float(self.pcell_N_fill.text)        
-        period = float(self.pcell_period_fill.text)*1e-6
-        gap = float(self.pcell_gap_fill.text)*1e-6
-        w1 = float(self.pcell_w1_fill.text)*1e-6
-        w2 = float(self.pcell_w2_fill.text)*1e-6
-        dw1 = float(self.pcell_dw1_fill.text)*1e-6
-        dw2 = float(self.pcell_dw2_fill.text)*1e-6
+        import glob
+        import xml.etree.ElementTree as ET
+
+        N = float(self.pcell_N_fill.text)
+        period = float(self.pcell_period_fill.text) * 1e-6
+        gap = float(self.pcell_gap_fill.text) * 1e-6
+        w1 = float(self.pcell_w1_fill.text) * 1e-6
+        w2 = float(self.pcell_w2_fill.text) * 1e-6
+        dw1 = float(self.pcell_dw1_fill.text) * 1e-6
+        dw2 = float(self.pcell_dw2_fill.text) * 1e-6
         a = float(self.pcell_apod_fill.text)
         if self.sim_pol_dropdown.currentIndex == 0:
-            pol = 'TE'
+            pol = "TE"
         else:
-            pol = 'TM'
+            pol = "TM"
         if self.pcell_rib_fill.isChecked():
             rib = True
         else:
             rib = False
 
-        thickness_device = float(self.tech_devthick_fill.text)*1e-6
-        thickness_rib = float(self.tech_ribthick_fill.text)*1e-6
-        wvl_range = [float(self.sim_wavlstart_fill.text)*1e-9, float(self.sim_wavlstop_fill.text)*1e-9]
-
-        device = ContraDC(w1= w1, dw1=dw1, w2=w2, dw2=dw2, gap=gap, a=a, period=period, rib=rib,
-        pol=pol, thickness_device=thickness_device, thickness_rib=thickness_rib, wvl_range=wvl_range)
+        thickness_device = float(self.tech_devthick_fill.text) * 1e-6
+        thickness_rib = float(self.tech_ribthick_fill.text) * 1e-6
+        wvl_range = [
+            float(self.sim_wavlstart_fill.text) * 1e-9,
+            float(self.sim_wavlstop_fill.text) * 1e-9,
+        ]
+
+        device = ContraDC(
+            N=N,
+            w1=w1,
+            dw1=dw1,
+            w2=w2,
+            dw2=dw2,
+            gap=gap,
+            a=a,
+            period=period,
+            rib=rib,
+            pol=pol,
+            thickness_device=thickness_device,
+            thickness_rib=thickness_rib,
+            wvl_range=wvl_range,
+        )
 
         if self.sim_kappa_dropdown.currentIndex == 0:
             device.kappa = float(self.sim_kappa_fill.text)
         else:
             device.simulate_kappa()
 
-        self.simulate.setText('Simulating...')
+        self.simulate.setText("Simulating...")
         device.simulate()
 
         if self.tech_plot_fill.isChecked():
             import plotly.graph_objs as go
             import plotly.offline as pyo
             import plotly.io as pio
-            #pio.renderers.default = "browser"
-            
-            drop = go.Scatter(x=device.wavelength*1e9, y=device.drop, mode='lines', name='Through')
-            thru = go.Scatter(x=device.wavelength*1e9, y=device.thru, mode='lines', name='Drop')
-            layout = go.Layout(title='Contra-directional coupler device', xaxis=dict(title='X Axis'), yaxis=dict(title='Y Axis'))
+
+            # pio.renderers.default = "browser"
+
+            drop = go.Scatter(
+                x=device.wavelength * 1e9, y=device.drop, mode="lines", name="Through"
+            )
+            thru = go.Scatter(
+                x=device.wavelength * 1e9, y=device.thru, mode="lines", name="Drop"
+            )
+            layout = go.Layout(
+                title="Contra-directional coupler device",
+                xaxis=dict(title="X Axis"),
+                yaxis=dict(title="Y Axis"),
+            )
             fig = go.Figure(data=[thru, drop], layout=layout)
             fig.show()
-        
+
         if self.tech_cm_fill.isChecked():
             # Check if there is a layout open, so we know which technology to install
             lv = pya.Application.instance().main_window().current_view()
             if lv == None:
-                raise UserWarning("To save data to the Compact Model Library, first, please create a new layout and select the desired technology:\n  Menu: File > New Layout, and a Technology.\nThen repeat.")
-            
-            # Get the Technology 
+                raise UserWarning(
+                    "To save data to the Compact Model Library, first, please create a new layout and select the desired technology:\n  Menu: File > New Layout, and a Technology.\nThen repeat."
+                )
+
+            # Get the Technology
             from SiEPIC.utils import get_layout_variables
+
             TECHNOLOGY, lv, ly, top_cell = get_layout_variables()
-            
+
             # Check if there is a CML folder in the Technology folder
             import os
-            base_path = ly.technology().base_path()    
-            folder_CML = os.path.join(base_path,'CML/%s/source_data/contraDC' % ly.technology().name)
+
+            base_path = ly.technology().base_path()
+            folder_CML = os.path.join(
+                base_path, "CML/%s/source_data/contraDC" % ly.technology().name
+            )
             if not os.path.exists(folder_CML):
-                raise UserWarning("The folder %s does not exist. \nCannot save to the Compact Model Library." %folder_CML)
-            
+                raise UserWarning(
+                    "The folder %s does not exist. \nCannot save to the Compact Model Library."
+                    % folder_CML
+                )
+
             # Generate compact model for Lumerical INTERCONNECT
             # return self.path_dat, .dat file that was created
-            device.gen_sparams(filepath=folder_CML, make_plot=False) # this will create a ContraDC_sparams.dat file to import into INTC
+            filename = device.gen_sparams(
+                filepath=folder_CML, make_plot=False
+            )  # this will create a ContraDC_sparams.dat file to import into INTC
+
+            # append data to xml
+            # Search for an XML file in folder_CML
+            xml_files = glob.glob(os.path.join(folder_CML, '*.xml'))
+            if not xml_files:
+                raise UserWarning(
+                    f"No XML file found in the folder {folder_CML}. "
+                    "Cannot save to the Compact Model Library."
+                )
+            xml_file = xml_files[0]  # Take the first XML file found
+
+            # Load and parse the XML file
+            tree = ET.parse(xml_file)
+            root = tree.getroot()
+
+            # Use the same association details for the new entry
+            new_association = ET.fromstring(f'''
+                <association>
+                    <design>
+                        <value name="wg1_width" type="double">{w1}</value>
+                        <value name="wg2_width" type="double">{w2}</value>
+                        <value name="corrugation_width1" type="double">{dw1}</value>
+                        <value name="corrugation_width2" type="double">{dw2}</value>
+                        <value name="gap" type="double">{gap}</value>
+                        <value name="grating_period" type="double">{period}</value>
+                        <value name="number_of_periods" type="int">{int(N)}</value>
+                        <value name="sinusoidal" type="double">False</value>
+                        <value name="apodization_index" type="double">{a}</value>
+                        <value name="lambda_start" type="double">{float(self.sim_wavlstart_fill.text)}</value>
+                        <value name="lambda_end" type="double">{float(self.sim_wavlstop_fill.text)}</value>
+                        <value name="lambda_points" type="double">500</value>
+                    </design>
+                    <extracted>
+                        <value name="sparam" type="string">{filename}</value>
+                    </extracted>
+                </association>''')
+
+            # Add the new association to the root element
+            root.append(new_association)
+
+            # Prettify the entire XML tree
+            self.condition_xml(root)
+
+            # Write the updated and formatted XML back to the file
+            tree.write(xml_file, encoding='utf-8', xml_declaration=True)
+            print(xml_file)
 
 
-        self.device = device
-        self.simulate.setText('Done simulating.')
 
+        self.device = device
+        self.simulate.setText("Done simulating.")
 
     def on_refresh_clicked(self):
         # fetch pcell parameters
         if self.load_pcell_params() == 0:
             return
-        self.pcell_N_fill.setText(str(self.params['number_of_periods']))
-        self.pcell_period_fill.setText(str(self.params['grating_period']))
-        self.pcell_gap_fill.setText(str(self.params['gap']))
-        self.pcell_w1_fill.setText(str(self.params['wg1_width']))
-        self.pcell_dw1_fill.setText(str(self.params['corrugation1_width']))
-        self.pcell_w2_fill.setText(str(self.params['wg2_width']))
-        self.pcell_dw2_fill.setText(str(self.params['corrugation2_width']))
-        self.pcell_apod_fill.setText(str(self.params['apodization_index']))
-        self.pcell_rib_fill.setChecked(self.params['rib'])
-
-        self.label_pcell.setText('Parameterized cell refreshed...')
-        self.simulate.setText('Simulate')
+        self.pcell_N_fill.setText(str(self.params["number_of_periods"]))
+        self.pcell_period_fill.setText(str(self.params["grating_period"]))
+        self.pcell_gap_fill.setText(str(self.params["gap"]))
+        self.pcell_w1_fill.setText(str(self.params["wg1_width"]))
+        self.pcell_dw1_fill.setText(str(self.params["corrugation1_width"]))
+        self.pcell_w2_fill.setText(str(self.params["wg2_width"]))
+        self.pcell_dw2_fill.setText(str(self.params["corrugation2_width"]))
+        self.pcell_apod_fill.setText(str(self.params["apodization_index"]))
+        self.pcell_rib_fill.setChecked(self.params["rib"])
+
+        self.label_pcell.setText("Parameterized cell refreshed...")
+        self.simulate.setText("Simulate")
 
     def on_sim_import(self):
         if self.sim_import.currentIndex == 0:
             # import simulation parameters from DFT
             self.load_DFT()
-            self.sim_import_label.setText('Simulation definitions: PDK')
+            self.sim_import_label.setText("Simulation definitions: PDK")
             self.sim_wavlstart_fill.setText(str(self.wavl_start))
             self.sim_wavlstart_fill.setReadOnly(True)
-            self.sim_wavlstart_fill.setStyleSheet('color: gray')
+            self.sim_wavlstart_fill.setStyleSheet("color: gray")
 
             self.sim_wavlstop_fill.setText(str(self.wavl_stop))
             self.sim_wavlstop_fill.setReadOnly(True)
-            self.sim_wavlstop_fill.setStyleSheet('color: gray')
+            self.sim_wavlstop_fill.setStyleSheet("color: gray")
 
             self.sim_wavlpts_fill.setText(str(self.wavl_pts))
             self.sim_wavlpts_fill.setReadOnly(True)
-            self.sim_wavlpts_fill.setStyleSheet('color: gray')
+            self.sim_wavlpts_fill.setStyleSheet("color: gray")
         else:
             # let user pick and ungrey the boxes
-            self.sim_import_label.setText('Simulation definitions: Custom')
+            self.sim_import_label.setText("Simulation definitions: Custom")
             self.sim_wavlstart_fill.setReadOnly(False)
-            self.sim_wavlstart_fill.setStyleSheet('color: black')
+            self.sim_wavlstart_fill.setStyleSheet("color: black")
 
             self.sim_wavlstop_fill.setReadOnly(False)
-            self.sim_wavlstop_fill.setStyleSheet('color: black')
+            self.sim_wavlstop_fill.setStyleSheet("color: black")
 
             self.sim_wavlpts_fill.setReadOnly(False)
-            self.sim_wavlpts_fill.setStyleSheet('color: black')
+            self.sim_wavlpts_fill.setStyleSheet("color: black")
 
     def on_kappa_dropdown(self):
         if self.sim_kappa_dropdown.currentIndex == 0:
             # query user for input
-            self.sim_kappa_label.setText('Coupling coefficient (κ): Custom')
+            self.sim_kappa_label.setText("Coupling coefficient (κ): Custom")
             self.sim_kappa_fill.setReadOnly(False)
-            self.sim_kappa_fill.setStyleSheet('color: black')
-            self.sim_kappa_fill.setText('Kappa (/m)')
+            self.sim_kappa_fill.setStyleSheet("color: black")
+            self.sim_kappa_fill.setText("Kappa (/m)")
         else:
             # simulate kappa using Lumerical
-            self.sim_kappa_label.setText('Coupling coefficient (κ): Simulate')
+            self.sim_kappa_label.setText("Coupling coefficient (κ): Simulate")
             self.sim_kappa_fill.setReadOnly(True)
-            self.sim_kappa_fill.setStyleSheet('color: gray')
-            self.sim_kappa_fill.setText('simulation')
+            self.sim_kappa_fill.setStyleSheet("color: gray")
+            self.sim_kappa_fill.setText("simulation")
 
     def on_wg_dropdown(self):
         if self.sim_wg_dropdown.currentIndex == 0:
             # look up if value is within lookup table index
-            self.sim_wg_label.setText('Waveguide models: LUT')
+            self.sim_wg_label.setText("Waveguide models: LUT")
         else:
             # simulate modes using Lumerical
-            self.sim_wg_label.setText('Waveguide models: Simulate')
+            self.sim_wg_label.setText("Waveguide models: Simulate")
 
     def on_rib_click(self):
         if self.pcell_rib_fill.isChecked():
-            self.tech_ribthick_fill.setText('rib thickness (µm)')
-            self.tech_ribthick_fill.setStyleSheet('color: black')
+            self.tech_ribthick_fill.setText("rib thickness (µm)")
+            self.tech_ribthick_fill.setStyleSheet("color: black")
             self.tech_ribthick_fill.setReadOnly(False)
         else:
-            self.tech_ribthick_fill.setText('0 nm')
+            self.tech_ribthick_fill.setText("0 nm")
             self.tech_ribthick_fill.setReadOnly(True)
-            self.tech_ribthick_fill.setStyleSheet('color: gray')
+            self.tech_ribthick_fill.setStyleSheet("color: gray")
 
     def load_DFT(self):
         from SiEPIC.utils import load_DFT
+
         DFT = load_DFT()
-        self.wavl_start = DFT['design-for-test']['tunable-laser'][0]['wavelength-start']
-        self.wavl_stop = DFT['design-for-test']['tunable-laser'][0]['wavelength-stop']
-        self.wavl_pts = DFT['design-for-test']['tunable-laser'][0]['wavelength-points']
-        self.pol = DFT['design-for-test']['tunable-laser'][0]['polarization']
+        self.wavl_start = DFT["design-for-test"]["tunable-laser"][0]["wavelength-start"]
+        self.wavl_stop = DFT["design-for-test"]["tunable-laser"][0]["wavelength-stop"]
+        self.wavl_pts = DFT["design-for-test"]["tunable-laser"][0]["wavelength-points"]
+        self.pol = DFT["design-for-test"]["tunable-laser"][0]["polarization"]
 
     def load_pcell_params(self):
         # get selected instances; only one
         from SiEPIC.utils import select_instances, get_layout_variables
+
         TECHNOLOGY, lv, ly, cell = get_layout_variables()
-    
+
         # print error message if no or more than one component selected
         selected_instances = select_instances()
         error = pya.QMessageBox()
@@ -420,16 +532,18 @@ def load_pcell_params(self):
             error.setText("Error: Need to have one component selected.")
             response = error.exec_()
             return 0
-    
+
         for obj in selected_instances:
             c = cell.find_components(cell_selected=[obj.inst().cell], verbose=True)
-    
+
         # check if selected PCell is a contra DC
         if c[0].cell.basic_name() != "contra_directional_coupler":
-            error.setText("Error: selected component must be a contra_directional_coupler PCell.")
+            error.setText(
+                "Error: selected component must be a contra_directional_coupler PCell."
+            )
             response = error.exec_()
             return 0
-    
+
         # parse PCell parameters into params array
         if c[0].cell.is_pcell_variant():
             self.params = c[0].cell.pcell_parameters_by_name()
@@ -442,18 +556,20 @@ def load_pcell_params(self):
     def exit(self):
         self.close()
 
+
 def cdc_gui():
     app = pya.QApplication.instance()
     if app is None:
         app = pya.QApplication([])
 
-#    import SiEPIC._globals
-#    SiEPIC._globals.GUI_cdc = MyWindow()
-#    print(SiEPIC._globals.GUI_cdc)
-#    SiEPIC._globals.GUI_cdc.show()
+    #    import SiEPIC._globals
+    #    SiEPIC._globals.GUI_cdc = MyWindow()
+    #    print(SiEPIC._globals.GUI_cdc)
+    #    SiEPIC._globals.GUI_cdc.show()
     GUI_cdc.show()
-    
+
     app.exec_()
 
+
 GUI_cdc = MyWindow()
-print('CDC Gui: %s' % GUI_cdc)
+print("CDC Gui: %s" % GUI_cdc)
diff --git a/klayout_dot_config/python/SiEPIC/tidy3d/requirements/dev.txt b/klayout_dot_config/python/SiEPIC/tidy3d/requirements/dev.txt
index b13efbfce..8b517ee94 100644
--- a/klayout_dot_config/python/SiEPIC/tidy3d/requirements/dev.txt
+++ b/klayout_dot_config/python/SiEPIC/tidy3d/requirements/dev.txt
@@ -7,7 +7,7 @@
 
 # required for development
 pre-commit
-black==22.3.0
+black==24.3.0
 pylint
 tox
 pytest
diff --git a/klayout_dot_config/python/SiEPIC/utils/__init__.py b/klayout_dot_config/python/SiEPIC/utils/__init__.py
index f6da49445..4553e5d73 100644
--- a/klayout_dot_config/python/SiEPIC/utils/__init__.py
+++ b/klayout_dot_config/python/SiEPIC/utils/__init__.py
@@ -4,7 +4,8 @@
 '''
 List of functions:
 
-
+load_layout
+create_cell2
 advance_iterator
 get_library_names
 get_technology_by_name
@@ -13,6 +14,7 @@
 load_Waveguides_by_Tech
 load_Calibre
 load_Monte_Carlo
+load_Verification
 load_DFT
 load_FDTD_settings
 load_GC_settings
@@ -45,7 +47,7 @@
 svg_from_component
 sample_function
 pointlist_to_path
-
+waveguide_length
 
 '''
 
@@ -62,6 +64,65 @@
     import pya
 '''
 
+
+
+def load_layout(layout, path, filename, single_topcell = True, Verbose = False):
+    '''
+    Load a GDS or OASIS file from path/file, and copies the top cell(s) into the specified layout. 
+    Input:
+      layout: pya.Layout, into which the top cell(s) will by copied
+      path: os.path
+      file: str
+      single_topcell: return only a single top cell. if there are more, pick the first one.
+            potential future improvement: one with the highest number of subcells
+    Returns:
+      cell, or [cell list]
+    '''
+    # Load the layout file
+    layout2 = pya.Layout()
+    layout2.read(os.path.join(path,filename))
+
+    subcells = []
+    for cell in layout2.top_cells():
+        if Verbose: 
+            print(" top cell name: %s" % cell.name)
+        # Create sub-cell in the layout
+        subcell = layout.create_cell(cell.name)
+        # Copy top cell into the sub-cell
+        subcell.copy_tree(cell)  
+        if single_topcell:
+            return subcell
+        subcells.append (subcell)
+    return subcells
+
+
+def create_cell2(ly, cell_name, library_name, load_check=True):
+    '''
+    Wrapper for KLayout Layout.create_cell(name, library),
+    with error handling, and debugging information if unsuccessful. 
+        ly: pya.Layout
+        cell_name: string name for pya.Cell
+        library_name: string name for a pya.Library
+    '''
+    # check if it is already loaded
+    if load_check and ly.cell(cell_name):
+        return ly.cell(cell_name)
+    # load the cell from the library
+    pcell = ly.create_cell(cell_name, library_name)
+    if not pcell:
+        if library_name not in pya.Library().library_names():
+            raise Exception('Error: library (%s) not available. Libraries for technology (%s) are: %s.' % (library_name, ly.technology().name, pya.Library().library_names()))
+        ly_library = pya.Library().library_by_name(library_name,ly.technology().name).layout()
+        library_cells = [ly_library.cell(a).name for a in ly_library.each_top_cell()]
+        if cell_name not in library_cells:
+            raise Exception('Error: cell (%s) not available in library (%s) for technology (%s). Cells are: %s.' % (cell_name, library_name, ly.technology().name, library_cells))
+
+        raise Exception('Error: loading cell (%s) from library (%s)' % (cell_name, library_name))
+        
+    return pcell
+
+
+
 # Python 2 vs 3 issues:  http://python3porting.com/differences.html
 # Python 2: iterator.next()
 # Python 3: next(iterator)
@@ -181,7 +242,7 @@ def get_technology_by_name(tech_name, verbose=False):
 
     if not tech_name:
         raise Exception(
-            "Problem with Technology", "Problem with active Technology: please activate a technology (not Default)", pya.MessageBox.Ok)
+            "Problem with Technology", "Problem with active Technology: please activate a technology (not Default)")
 
     from .._globals import KLAYOUT_VERSION
     technology = {}
@@ -520,8 +581,7 @@ def load_Verification(TECHNOLOGY=None, debug=True):
     matches = []
     for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
         for filename in fnmatch.filter(filenames, search_str):
-            if tech_name in root:
-                matches.append(os.path.join(root, filename))
+            matches.append(os.path.join(root, filename))
     if matches:
         if debug:
             print(' - load_Verification, matches: %s' %matches ) 
@@ -721,6 +781,9 @@ def find_paths(layer, cell=None):
 def selected_opt_in_text():
     '''KLayout Application use. Return all selected opt_in Text labels.
     # example usage: selected_opt_in_text()[0].shape.text.string'''
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        raise Exception('This function can only be executed in KLayout Application GUI mode.')
     from . import get_layout_variables
     TECHNOLOGY, lv, ly, cell = get_layout_variables()
 
@@ -1046,7 +1109,7 @@ def arc_bezier(radius, start, stop, bezier, DevRec=None, dbu=0.001):
         t = i * diff
         pts.append(pya.Point(-L, 0) + pya.Point(t**3 * xA + t**2 * xB +
                                                 t * xC + xD, t**3 * yA + t**2 * yB + t * yC + yD))
-    pts.extend([pya.Point(0, L - 1), pya.Point(0, L)])
+    pts.extend([pya.Point(0, L)])
     return pts
 
 
@@ -1056,9 +1119,25 @@ def arc_to_waveguide(pts, width):
 
 
 def translate_from_normal(pts, trans):
-    '''Translate each point by its normal a distance 'trans' '''
+    '''Translate each point by its normal a distance 'trans' 
+    
+    Args:
+        pts: list of pya.Point (nanometers)
+            or       pya.DPoint (microns)
+        trans: <float> (matching pts, either nm or microns)
+
+    Returns:
+        list of pya.Point or pya.DPoint, matching Arg pts type.
+    '''
     #  pts = [pya.DPoint(pt) for pt in pts]
-    pts = [pt.to_dtype(1) for pt in pts]
+    if type(pts[0]) == pya.Point:
+        # convert to float pya.DPoint
+        pts = [pt.to_dtype(1) for pt in pts]
+        in_type = 'Point'
+    elif type(pts[0]) == pya.DPoint:
+        in_type = 'DPoint'
+    else:
+        raise Exception('SiEPIC.utils.translate_from_normal expects pts=[pya.Point,...] or [pya.DPoint,...]')
     if len(pts) < 2:
         return pts    
     from math import cos, sin, pi
@@ -1083,8 +1162,10 @@ def translate_from_normal(pts, trans):
     else:
         tpts[-1].x = pts[-1].x
 #  return [pya.Point(pt) for pt in tpts]
-    return [pt.to_itype(1) for pt in tpts]
-
+    if in_type == 'Point':
+        return [pt.to_itype(1) for pt in tpts]
+    else:
+        return tpts
 
 
 def pt_intersects_segment(a, b, c):
@@ -1145,8 +1226,15 @@ def find_automated_measurement_labels(topcell=None, LayerTextN=None, TECHNOLOGY=
     
     import string
     if TECHNOLOGY == None:
-        from . import get_technology, find_paths
-        TECHNOLOGY = get_technology()
+        if topcell:
+            if 'TECHNOLOGY' in dir(topcell.layout()):
+                TECHNOLOGY = topcell.layout().TECHNOLOGY
+            else:
+                from . import get_technology_by_name
+                TECHNOLOGY = get_technology_by_name(topcell.layout().technology().name)
+        else:
+            from . import get_technology
+            TECHNOLOGY = get_technology()
     dbu = TECHNOLOGY['dbu']
     if LayerTextN == None:
         LayerTextN = TECHNOLOGY['Text']
@@ -1507,4 +1595,15 @@ def pointlist_to_path(pointlist, dbu):
     path = pya.Path(points)
     return path
 
-
+def waveguide_length(cell):
+    '''
+    Extract the waveguide length from the layout cell Spice parameters
+    input: pya.Cell or pya.Instance
+    '''
+    
+    if type(cell) == pya.Instance:
+        cell = cell.cell
+    if type(cell) == pya.Cell:
+        return float(cell.find_components()[0].params.split('wg_length=')[1].split(' ')[0])*1e6
+    else:
+        raise Exception ('SiEPIC.utils.waveguide_length: input needs to be a Cell or Instance.')
diff --git a/klayout_dot_config/python/SiEPIC/utils/geometry.py b/klayout_dot_config/python/SiEPIC/utils/geometry.py
index 45427ac6e..5c0caa1a4 100644
--- a/klayout_dot_config/python/SiEPIC/utils/geometry.py
+++ b/klayout_dot_config/python/SiEPIC/utils/geometry.py
@@ -7,6 +7,21 @@
   Author: Lukas Chrostowski
 
 
+Functions:
+
+GeometryError
+Point
+Line
+
+bezier_line
+curvature_bezier
+max_curvature
+min_curvature
+curve_length
+bezier_optimal
+translate_from_normal2
+box_bezier_corners
+
 """
 import numpy as np
 from numpy import sqrt
@@ -61,8 +76,12 @@ def __str__(self):
         return str(Point({self.x}, {self.y}))
 
     def norm(self):
+        '''Euclidean length'''
         return sqrt(self.x**2 + self.y**2)
 
+    def long_edge_length(self):
+        '''return the longest segment of a Manhattan distance'''
+        return max(self.x, self.y)
 
 class Line(Point):
     """ Defines a line """
@@ -150,6 +169,13 @@ def max_curvature(P0, P1, P2, P3):
     max_curv = np.max(np.abs(curv.flatten()))
     return max_curv
 
+def min_curvature(P0, P1, P2, P3):
+    """Gets the minimum curvature of Bezier curve"""
+    t = np.linspace(0, 1, 300)
+    curv = curvature_bezier(P0, P1, P2, P3)(t)
+    min_curv = np.min(np.abs(curv.flatten()))
+    return min_curv
+
 
 def _curvature_penalty(P0, P1, P2, P3):
     """Penalty on the curvyness of Bezier curve"""
@@ -348,7 +374,7 @@ def bezier_parallel(P0, P3, angle):
     import pya
     _bezier_optimal_pure = bezier_optimal
 
-    def bezier_optimal(P0, P3, *args, **kwargs):
+    def bezier_optimal(P0, P3, *args, accuracy = 0.001, **kwargs):
         P0 = Point(P0.x, P0.y)
         P3 = Point(P3.x, P3.y)
         scale = (P3 - P0).norm()  # rough length.
@@ -359,14 +385,14 @@ def bezier_optimal(P0, P3, *args, **kwargs):
         bezier_point_coordinates = lambda t: np.array([new_bezier_line(t).x, new_bezier_line(t).y])
 
         _, bezier_point_coordinates_sampled = \
-            sample_function(bezier_point_coordinates, [0, 1], tol=0.001 / scale)  # tol about 1 nm
+            sample_function(bezier_point_coordinates, [0, 1], tol=accuracy / scale)  # tol about 1 nm
 
         # # This yields a better polygon
         bezier_point_coordinates_sampled = \
-            np.insert(bezier_point_coordinates_sampled, 1, bezier_point_coordinates(.001 / scale),
+            np.insert(bezier_point_coordinates_sampled, 1, bezier_point_coordinates(accuracy / scale),
                       axis=1)  # add a point right after the first one
         bezier_point_coordinates_sampled = \
-            np.insert(bezier_point_coordinates_sampled, -1, bezier_point_coordinates(1 - .001 / scale),
+            np.insert(bezier_point_coordinates_sampled, -1, bezier_point_coordinates(1 - accuracy / scale),
                       axis=1)  # add a point right before the last one
         # bezier_point_coordinates_sampled = \
         #     np.append(bezier_point_coordinates_sampled, np.atleast_2d(bezier_point_coordinates(1 + .001 / scale)).T,
@@ -377,6 +403,71 @@ def bezier_optimal(P0, P3, *args, **kwargs):
 except ImportError:
     pass
 
+def bezier_cubic(P0, P3, angle0, angle3, a, b, accuracy = 0.001, verbose=False, plot=False, *args, **kwargs):
+    '''
+    Calculate a cubic Bezier curve between Points P0 and P3, 
+    where the control point positions P1 and P2 are determined by
+    the angles at P0 (angle0) and P3 (angle3), 
+    at a distance of a * scale from P0, and b * scale from P3,
+    where scale is the longest segment in a Manhattan route between P0 and P3.
+
+    Args:
+        P0, P3: pya.DPoint (in microns)
+        angle0, angle3: radians
+        a, b: <float>. 0 corresponds to P1=P0, and 1 corresponds to P1 at the corner of a 90º bend
+        accuracy: 0.001 = 1 nm
+
+    Returns:
+        list of pya.DPoint
+
+    Example:
+        Bezier curve can approximate a 1/4 circle (arc) for a=b=0.553
+            # https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
+    '''
+
+    P0 = Point(P0.x, P0.y)
+    P3 = Point(P3.x, P3.y)
+    scale = (P3 - P0).long_edge_length()  # longest distance between the two end points
+    P1 = a * scale * Point(np.cos(angle0), np.sin(angle0)) + P0
+    P2 = P3 - b * scale * Point(np.cos(angle3), np.sin(angle3))
+    new_bezier_line = bezier_line(P0, P1, P2, P3)
+    # new_bezier_line = _bezier_optimal_pure(P0, P3, *args, **kwargs)
+    bezier_point_coordinates = lambda t: np.array([new_bezier_line(t).x, new_bezier_line(t).y])
+
+    _, bezier_point_coordinates_sampled = \
+        sample_function(bezier_point_coordinates, [0, 1], tol=accuracy / scale) 
+
+    # # This yields a better polygon
+    bezier_point_coordinates_sampled = \
+        np.insert(bezier_point_coordinates_sampled, 1, bezier_point_coordinates(accuracy / scale),
+                    axis=1)  # add a point right after the first one
+    bezier_point_coordinates_sampled = \
+        np.insert(bezier_point_coordinates_sampled, -1, bezier_point_coordinates(1 - accuracy / scale),
+                    axis=1)  # add a point right before the last one
+
+    if verbose:
+        # print the minimum/maximum curvature
+        print ('SiEPIC.utils.geometry.bezier_cubic: minimum radius of curvature = %0.3g' % (1/max_curvature(P0, P1, P2, P3)))
+        print ('SiEPIC.utils.geometry.bezier_cubic: maximum radius of curvature = %0.3g' % (1/min_curvature(P0, P1, P2, P3)))
+    if plot:
+        t = np.linspace(0, 1, 300)
+        curv = curvature_bezier(P0, P1, P2, P3)(t)
+        rc = 1./curv.flatten()
+        import matplotlib.pyplot as plt
+        plt.plot(t, rc, '--pb', label='a=%3g, b=%3g' % (a,b), linewidth=1.5)
+        SizeFont = 19
+        plt.xlabel('Position along path (t)', fontsize=SizeFont)
+        plt.ylabel('Radius of curvature (microns)', fontsize=SizeFont)
+        plt.legend(fontsize=SizeFont)
+        plt.xticks(fontsize=SizeFont)
+        plt.ylim(bottom=0)
+        plt.yticks(fontsize=SizeFont)
+        plt.show()
+
+    return [pya.DPoint(x, y) for (x, y) in zip(*(bezier_point_coordinates_sampled))]
+
+
+
 # ####################### SIEPIC EXTENSION ##########################
 
 
@@ -471,3 +562,38 @@ def translate_from_normal2(pts, trans, trans2=None):
     return tpts
 
 
+def box_bezier_corners(width, height, dt_bezier_corner, accuracy = 0.1):
+    '''
+    Input width, height: in microns
+    Return a pya.DPolygon of a box with rounded corners, using an optimal Bezier curve
+    with the control points being a fraction (dt_bezier_corner from 0 to 1) away from each corner
+    accuracy 0.001 is 1 nm
+    
+    Some call them squircles: https://arun.is/blog/apple-rounded-corners/
+    '''
+    if dt_bezier_corner == 0:
+        return pya.DBox(-width/2,-height/2,width/2,height/2)
+    else:
+        # top left corner
+        pts = []
+        pts = bezier_optimal(
+            pya.DPoint(-width/2, height/2 - dt_bezier_corner * height),
+            pya.DPoint(-width/2 + dt_bezier_corner * width, height/2),
+            90, 0, 
+            accuracy = accuracy)
+        pts += bezier_optimal(
+            pya.DPoint(width/2 - dt_bezier_corner * width, height/2),
+            pya.DPoint(width/2, height/2 - dt_bezier_corner * height),
+            0, 270,
+            accuracy = accuracy)
+        pts += bezier_optimal(
+            pya.DPoint(width/2, - height/2 + dt_bezier_corner * height),
+            pya.DPoint(width/2 - dt_bezier_corner * width, - height/2),
+            270, 180,
+            accuracy = accuracy)
+        pts += bezier_optimal(
+            pya.DPoint(-width/2 + dt_bezier_corner * width, - height/2),
+            pya.DPoint(-width/2, - height/2 + dt_bezier_corner * height),
+            180, 90,
+            accuracy = accuracy)
+        return pya.DPolygon(pts)
diff --git a/klayout_dot_config/python/SiEPIC/utils/layout.py b/klayout_dot_config/python/SiEPIC/utils/layout.py
index e615da1aa..3bae0bd37 100644
--- a/klayout_dot_config/python/SiEPIC/utils/layout.py
+++ b/klayout_dot_config/python/SiEPIC/utils/layout.py
@@ -8,6 +8,8 @@
 
 Functions:
 
+layout_waveguide4
+layout_waveguide3
 layout_waveguide2
 layout_waveguide
 layout_waveguide_sbend_bezier
@@ -15,7 +17,9 @@
 y_splitter_tree
 floorplan(topcell, x, y)
 new_layout(tech, topcell_name, overwrite = False)
-
+strip2rib
+FaML_two
+coupler_array
 
 TODO: enhance documentation
 TODO: make some of the functions in util use these.
@@ -209,24 +213,24 @@ def layout_waveguide4(cell, dpath, waveguide_type, debug=False):
     return waveguide_length
 
 
-'''
-Create a waveguide, in a specific technology
-inputs
-- cell: into which Cell we add the waveguide
-    from SiEPIC.utils import get_layout_variables
-    TECHNOLOGY, lv, layout, cell = get_layout_variables()
-- pts
-- params, obtained from load_Waveguides_by_Tech and Waveguides.XML
-    must be a primitive waveguide type containing <component> info
-output:
-- waveguide
-- DevRec, PinRec
-by Lukas Chrostowski
-'''
 
 
-def layout_waveguide3(cell, pts, params, debug=False):
 
+def layout_waveguide3(cell, pts, params, debug=False, drawRec=True):
+    '''
+    Create a waveguide, in a specific technology
+    inputs
+    - cell: into which Cell we add the waveguide
+        from SiEPIC.utils import get_layout_variables
+        TECHNOLOGY, lv, layout, cell = get_layout_variables()
+    - pts: a list of pya.Points, in database units (e.g., nm)
+    - params, obtained from load_Waveguides_by_Tech and Waveguides.XML
+        must be a primitive waveguide type containing <component> info
+    output:
+    - waveguide
+    - DevRec, PinRec
+    by Lukas Chrostowski
+    '''
     if debug:
         print('SiEPIC.utils.layout.layout_waveguide3: ')
 
@@ -239,10 +243,18 @@ def layout_waveguide3(cell, pts, params, debug=False):
     from SiEPIC.extend import to_itype
     wg_width = to_itype(params['width'], dbu)
     radius = float(params['radius'])
-    model = params['model']
+    if 'model' not in params.keys():
+        params['model'] = ''
     cellName = 'Waveguide'
-    CML = params['CML']
-    waveguide_type = params['waveguide_type']
+    if 'CML' not in params.keys():
+        params['CML'] = ''
+    if 'bezier' not in params.keys():
+        params['adiabatic'] = False
+        params['bezier'] = ''
+    if 'waveguide_type' in params.keys():
+        waveguide_type = params['waveguide_type']
+    else:
+        waveguide_type = params['name']
 
     if debug:
         print(' - waveguide params: %s' % (params))
@@ -260,25 +272,15 @@ def layout_waveguide3(cell, pts, params, debug=False):
 
     # Draw the marking layers
     from SiEPIC.utils import angle_vector
-    LayerPinRecN = layout.layer(TECHNOLOGY['PinRec'])
-
-    make_pin(cell, 'opt1', pts[0], wg_width, LayerPinRecN, angle_vector(pts[0]-pts[1]) % 360)
-    make_pin(cell, 'opt2', pts[-1], wg_width, LayerPinRecN,
-             angle_vector(pts[-1]-pts[-2]) % 360)
-
-    from pya import Trans, Text, Path, Point
-
-    '''
-    t1 = Trans(angle_vector(pts[0]-pts[1])/90, False, pts[0])
-    cell.shapes(LayerPinRecN).insert(Path([Point(-10, 0), Point(10, 0)], wg_width).transformed(t1))
-    cell.shapes(LayerPinRecN).insert(Text("opt1", t1, 0.3/dbu, -1))
     
-    t = Trans(angle_vector(pts[-1]-pts[-2])/90, False, pts[-1])
-    cell.shapes(LayerPinRecN).insert(Path([Point(-10, 0), Point(10, 0)], wg_width).transformed(t))
-    cell.shapes(LayerPinRecN).insert(Text("opt2", t, 0.3/dbu, -1))
-    '''
+    if drawRec:
+        LayerPinRecN = layout.layer(TECHNOLOGY['PinRec'])
+        make_pin(cell, 'opt1', pts[0], wg_width, LayerPinRecN, angle_vector(pts[0]-pts[1]) % 360)
+        make_pin(cell, 'opt2', pts[-1], wg_width, LayerPinRecN,
+                 angle_vector(pts[-1]-pts[-2]) % 360)
+        LayerDevRecN = layout.layer(TECHNOLOGY['DevRec'])
 
-    LayerDevRecN = layout.layer(TECHNOLOGY['DevRec'])
+    from pya import Trans, Text, Path, Point
 
     # Compact model information
     angle_vec = angle_vector(pts[0]-pts[1])/90
@@ -309,64 +311,68 @@ def layout_waveguide3(cell, pts, params, debug=False):
 
     t = Trans(angle, False, pt3)
     import re
-    CML = re.sub('design kits/', '', CML, flags=re.IGNORECASE)
-#    CML = CML.lower().replace('design kits/','') # lower: to make it case insensitive, in case WAVEGUIDES.XML contains "Design Kits/" rather than "Design kits/"
-    text = Text('Lumerical_INTERCONNECT_library=Design kits/%s' % CML, t, 0.1*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
-    t = Trans(angle, False, pt2)
-    text = Text('Component=%s' % model, t, 0.1*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
-    t = Trans(angle, False, pt5)
-    text = Text('cellName=%s' % cellName, t, 0.1*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
-    t = Trans(angle, False, pts[0])
-    pts_txt = str([[round(p.to_dtype(dbu).x, 3), round(p.to_dtype(dbu).y, 3)]
-                  for p in pts]).replace(', ', ',')
-    text = Text(
-        'Spice_param:wg_length=%.9f wg_width=%.3g points="%s" radius=%.3g' %
-        (waveguide_length*1e-6, wg_width*1e-9, pts_txt, radius*1e-6), t, 0.1*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
-    t = Trans(angle, False, pt4)
-    text = Text(
-        'Length=%.3f (microns)' % (waveguide_length), t, 0.5*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
-    t = Trans(angle, False, pt6)
-    text = Text('waveguide_type=%s' % waveguide_type, t, 0.1*wg_width, -1)
-    text.halign = halign
-    shape = cell.shapes(LayerDevRecN).insert(text)
+    if params['CML']:
+        CML = re.sub('design kits/', '', params['CML'], flags=re.IGNORECASE)
+    #    CML = CML.lower().replace('design kits/','') # lower: to make it case insensitive, in case WAVEGUIDES.XML contains "Design Kits/" rather than "Design kits/"
+        text = Text('Lumerical_INTERCONNECT_library=Design kits/%s' % CML, t, 0.1*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
+        t = Trans(angle, False, pt2)
+        text = Text('Component=%s' % params['model'], t, 0.1*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
+        t = Trans(angle, False, pt5)
+        text = Text('cellName=%s' % cellName, t, 0.1*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
+        t = Trans(angle, False, pts[0])
+
+    if drawRec:
+        pts_txt = str([[round(p.to_dtype(dbu).x, 3), round(p.to_dtype(dbu).y, 3)]
+                      for p in pts]).replace(', ', ',')
+        text = Text(
+            'Spice_param:wg_length=%.9f wg_width=%.3g points="%s" radius=%.3g' %
+            (waveguide_length*1e-6, wg_width*1e-9, pts_txt, radius*1e-6), t, 0.1*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
+        t = Trans(angle, False, pt4)
+        text = Text(
+            'Length=%.3f (microns)' % (waveguide_length), t, 0.5*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
+        t = Trans(angle, False, pt6)
+        text = Text('waveguide_type=%s' % waveguide_type, t, 0.1*wg_width, -1)
+        text.halign = halign
+        shape = cell.shapes(LayerDevRecN).insert(text)
 
     return waveguide_length
 
 
-'''
-Create a waveguide, in a specific technology
-inputs
-- TECHNOLOGY, layout, cell:
-    from SiEPIC.utils import get_layout_variables
-    TECHNOLOGY, lv, layout, cell = get_layout_variables()
-- layers: list of text names, e.g., ['Waveguide']
-- widths: list of floats in units Microns, e.g., [0.50]
-- offsets: list of floats in units Microns, e.g., [0]
-- pts: a list of pya.Points, e.g. 
-    L=15/dbu
-    pts = [Point(0,0), Point(L,0), Point(L,L)]
-- radius: in Microns, e.g., 5
-- adiab: 1 = Bezier curve, 0 = radial bend (arc)
-- bezier: the bezier parameter, between 0 and 0.45 (almost a radial bend)
-- sbends (optional): sbends (Boolean)
-Note: bezier parameters need to be simulated and optimized, and will depend on 
-    wavelength, polarization, width, etc.  TM and rib waveguides don't benefit from bezier curves
-    most useful for TE 
-by Lukas Chrostowski
-'''
 
 
 def layout_waveguide2(TECHNOLOGY, layout, cell, layers, widths, offsets, pts, radius, adiab, bezier, sbends = True):
+    '''
+    Create a waveguide, in a specific technology
+    inputs
+    - TECHNOLOGY, layout, cell:
+        from SiEPIC.utils import get_layout_variables
+        TECHNOLOGY, lv, layout, cell = get_layout_variables()
+    - layers: list of text names, e.g., ['Waveguide']
+    - widths: list of floats in units Microns, e.g., [0.50]
+    - offsets: list of floats in units Microns, e.g., [0]
+    - pts: a list of pya.Points, in database units (e.g., nm)
+        e.g. 
+        L=15/dbu
+        pts = [Point(0,0), Point(L,0), Point(L,L)]
+    - radius: in Microns, e.g., 5
+    - adiab: 1 = Bezier curve, 0 = radial bend (arc)
+    - bezier: the bezier parameter, between 0 and 0.45 (almost a radial bend)
+    - sbends (optional): sbends (Boolean)
+    Note: bezier parameters need to be simulated and optimized, and will depend on 
+        wavelength, polarization, width, etc.  TM and rib waveguides don't benefit from bezier curves
+        most useful for TE 
+    by Lukas Chrostowski
+    '''
     from SiEPIC.utils import arc_xy, arc_bezier, angle_vector, angle_b_vectors, inner_angle_b_vectors, translate_from_normal
     from SiEPIC.extend import to_itype
     from SiEPIC.utils.geometry import bezier_parallel
@@ -490,6 +496,9 @@ def layout_waveguide2(TECHNOLOGY, layout, cell, layers, widths, offsets, pts, ra
 
         wg_pts += [pts[-1]]
         wg_pts = pya.Path(wg_pts, 0).unique_points().get_points()
+        if len(wg_pts) < 2:
+            print (' - warning: SiEPIC.utils.layout.layout_waveguide2: less than 2 points.')
+            return 0            
         wg_polygon = Polygon(translate_from_normal(wg_pts, width/2 + (offset if turn > 0 else - offset)) +
                              translate_from_normal(wg_pts, -width/2 + (offset if turn > 0 else - offset))[::-1])
         cell.shapes(layer).insert(wg_polygon)
@@ -655,18 +664,22 @@ def smooth_append(point_list, point):
 
 
 def layout_ring(cell, layer, center, r, w):
-    # function to produce the layout of a ring
-    # cell: layout cell to place the layout
-    # layer: which layer to use
-    # center: origin DPoint
-    # r: radius
-    # w: waveguide width
-    # units in microns
-
-    # example usage.  Places the ring layout in the presently selected cell.
-    # cell = pya.Application.instance().main_window().current_view().active_cellview().cell
-    # layout_ring(cell, cell.layout().layer(LayerInfo(1, 0)), pya.DPoint(0,0), 10, 0.5)
-
+    '''
+    Produce the layout of a ring
+    
+    Args:
+        cell: layout cell to place the layout
+        layer: layer index to use, cell.layout.layer()
+        center: origin pya.DPoint
+        r: radius, units in microns
+        w: waveguide width, units in microns
+
+    Example usage:  
+    Places the ring layout in the presently selected cell.
+    cell = pya.Application.instance().main_window().current_view().active_cellview().cell
+    layout_ring(cell, cell.layout().layer(LayerInfo(1, 0)), pya.DPoint(0,0), 10, 0.5)
+    '''
+    
     layout_arc(cell, layer, center, r, w, 0, 2 * np.pi)
 
 
@@ -1028,17 +1041,19 @@ def make_pin(cell, name, center, w, layer, direction, debug=False):
     name: text label for the pin
     center: location, int [x,y]
     w: pin width
-    layer: layout.layer() type
+    layer: layout.layer() integer type, or string
     direction = 
         0: right
         90: up
         180: left
         270: down
 
-    Units: input can be float for microns, or int for nm
+    Units: input can be float for microns, or int for database units (typ. nm)
     '''
 
-
+    if type(layer) == str:
+        layer = cell.layout().layer(cell.layout().TECHNOLOGY[layer])
+         
     from SiEPIC.extend import to_itype
     from pya import Point, DPoint
     import numpy
@@ -1293,3 +1308,114 @@ def new_layout(tech, topcell_name, GUI=True, overwrite = False):
 
     return topcell, ly
 
+
+def strip2rib(cell, trans, w_slab, w_rib, w_slab_tip, w_strip, length, LayerRib, LayerSlab):
+    '''
+    PCell: Strip to Rib converter (linear)
+        cell: pya.Cell
+        trans: pya.Trans
+        w_slab: width of the slab region of the rib waveguide, in microns
+        w_rib: width of the rib region of the rib waveguide, in microns
+        w_slab_tip: width of the rib tip of the strip waveguide region, in microns
+        w_strip: width of the strip of the strip waveguide, in microns
+        length: taper length, in microns
+            at (0,0): strip waveguide
+        LayerRib, LayerSlab: string, layer names
+        
+    '''
+    from pya import DPoint, DPolygon
+    # waveguide rib
+    nLayerRib = cell.layout().layer(cell.layout().TECHNOLOGY[LayerRib])
+    # box = pya.DBox(0, -w_rib/2, length, w_rib/2)
+    # cell.shapes(LayerRib).insert(box.transformed(trans))
+    poly = DPolygon([DPoint(length,-w_rib/2), DPoint(length,w_rib/2), DPoint(0, w_strip/2), DPoint(0, -w_strip/2)])
+    cell.shapes(nLayerRib).insert(poly.transformed(trans))
+    # waveguide slab
+    nLayerSlab = cell.layout().layer(cell.layout().TECHNOLOGY[LayerSlab])
+    poly = DPolygon([DPoint(length,-w_slab/2), DPoint(length,w_slab/2), DPoint(0, w_slab_tip/2), DPoint(0, -w_slab_tip/2)])
+    cell.shapes(nLayerSlab).insert(poly.transformed(trans))
+
+
+def FaML_two(cell, 
+             label='opt_in_TE_1550_FaML_TestCircuit', 
+             x_offset=0, 
+             y_offset=127e3/2-5e3,
+             pitch = 127e3,
+             cell_name = 'ebeam_dream_FaML_SiN_1550_BB',
+             cell_library = 'EBeam-Dream',
+             cell_params =  {},
+             ):
+    '''
+    Create a layout consisting of two facet-attached micro-lenses (FaML)
+    return the two instances
+    '''
+    from pya import Trans, CellInstArray, Text
+    ly = cell.layout()
+    # Load cell from library
+    if cell_params:
+        cell_ebeam_faml = ly.create_cell(cell_name, cell_library, cell_params)
+    else:
+        cell_ebeam_faml = ly.create_cell(cell_name, cell_library)
+    if not cell_ebeam_faml:
+        raise Exception ('Cannot load cell (%s) from library (%s) with parameters (%s).' % (cell_name, cell_library, cell_params))
+    # lens for the output to the detector
+    t = Trans(Trans.R0, x_offset, y_offset)
+    inst_faml2 = cell.insert(CellInstArray(cell_ebeam_faml.cell_index(), t))
+    # lens for the input from the laser
+    t = Trans(Trans.R0,x_offset,pitch + y_offset)
+    inst_faml1 = cell.insert(CellInstArray(cell_ebeam_faml.cell_index(), t))
+    # automated test label
+    text = Text (label, t)
+    cell.shapes(ly.layer(ly.TECHNOLOGY['Text'])).insert(text).text_size = 5/ly.dbu
+    return [inst_faml1, inst_faml2]
+
+def coupler_array(cell, 
+             x_offset=0, 
+             y_offset=127e3/2-5e3,
+             pitch = 127e3,
+             count = 4,
+             label='opt_in_TE_1550_device_test', 
+             label_location = 2, 
+             label_size = 5,
+             cell_name = 'GC_TE_1550_8degOxide_BB',
+             cell_library = 'EBeam',
+             cell_params =  {},
+             ):
+    '''
+    Create a layout consisting of an array of optical couplers
+    return the instances
+    include automated test labels
+    
+    cell: into which to place the components
+    x_offset, y_offset: location to place them, bottom coupler
+    pitch: the pitch for the coupler array
+    
+    label: on Text layer
+    label_location: 1 is the top
+    label_size: font size
+    
+    cell_name, _library, _params: can be a fixed cell, or a PCell
+    
+    '''
+    from pya import Trans, CellInstArray, Text
+    ly = cell.layout()
+    
+    # Load cell from library, either fixed or PCell
+    cell_coupler = ly.create_cell(cell_name, cell_library, cell_params)
+    if not cell_coupler:
+        cell_coupler = ly.create_cell(cell_name, cell_library)
+    if not cell_coupler:
+        raise Exception ('Cannot load coupler cell (%s) from library (%s) with parameters (%s).' % (cell_name, cell_library, cell_params))
+
+    inst_couplers = []
+    for i in range(count):
+        t = Trans(Trans.R0, x_offset, y_offset + (count-i-1)*pitch)
+        inst_couplers.append( 
+            cell.insert(CellInstArray(cell_coupler.cell_index(), t))
+        )
+        if i==label_location-1:
+            # automated test label
+            text = Text (label, t)
+            cell.shapes(ly.layer(ly.TECHNOLOGY['Text'])).insert(text).text_size = label_size/ly.dbu
+        
+    return inst_couplers
diff --git a/klayout_dot_config/python/SiEPIC/utils/sampling.py b/klayout_dot_config/python/SiEPIC/utils/sampling.py
index 2fb7c531a..0d577e71e 100644
--- a/klayout_dot_config/python/SiEPIC/utils/sampling.py
+++ b/klayout_dot_config/python/SiEPIC/utils/sampling.py
@@ -119,8 +119,8 @@ def _sample_function(func, points, values=None, mask=None, tol=0.05,
 
         sz = (p.shape[0] - 1) // 2
 
-        xscale = x_2.ptp(axis=-1)
-        yscale = np.abs(y_2_val.ptp(axis=-1)).ravel()
+        xscale = np.ptp(x_2, axis=-1)
+        yscale = np.abs(np.ptp(y_2_val,axis=-1)).ravel()
 
         p[0] /= xscale
 
diff --git a/klayout_dot_config/python/SiEPIC/verification.py b/klayout_dot_config/python/SiEPIC/verification.py
index c2e2bad29..b6fd6e93a 100644
--- a/klayout_dot_config/python/SiEPIC/verification.py
+++ b/klayout_dot_config/python/SiEPIC/verification.py
@@ -7,9 +7,13 @@
 '''
 
 
-def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = None):
-    '''Functional Verification:
-    cell=pya.cell, file_rbd=<str> path.
+def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = None, verify_DFT = True):
+    '''Functional Verification.
+    Input
+        cell: pya.Cell
+        file_rbd: <str> path
+        verify_DFT: True for design for test verification, False to skip.
+        
     Verification of things that are specific to photonic integrated circuits, including
     - Waveguides: paths, radius, bend points, Manhattan
     - Component checking: overlapping, avoiding crosstalk
@@ -184,7 +188,10 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
 
     # Design for Test checking
     from SiEPIC.utils import load_DFT
-    DFT = load_DFT(TECHNOLOGY=TECHNOLOGY)
+    if verify_DFT:
+        DFT = load_DFT(TECHNOLOGY=TECHNOLOGY)
+    else:
+        DFT = None
     if DFT:
         if verbose:
             print(DFT)
@@ -239,6 +246,12 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
         rdb_cat_id_GCarrayconfig.description = "Circuit must be connected such that there is at most %s Grating Coupler(s) %s the opt_in label (laser injection port) and at most %s Grating Coupler(s) %s the opt_in label. \nGrating couplers must be on a %s micron pitch, %s arranged." % (
             int(DFT['design-for-test']['grating-couplers']['detectors-above-laser']), dir1,int(DFT['design-for-test']['grating-couplers']['detectors-below-laser']), dir2,float(DFT['design-for-test']['grating-couplers']['gc-pitch']),dir3)
 
+        # minimum-gc-spacing        
+        if 'minimum-gc-spacing' in DFT['design-for-test']['grating-couplers'].keys():
+            rdb_cat_id_GC_min_spacing= rdb.create_category(rdb_cat_id, "Grating coupler: minimum spacing")
+            rdb_cat_id_GC_min_spacing.description = "The grating coupler spacing (pitch) must be at least %s microns." % float(DFT['design-for-test']['grating-couplers']['minimum-gc-spacing'])
+
+
     else:
         if verbose:
             print('  No DFT rules found.')
@@ -269,11 +282,8 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
     if pin_errors:
         for p in pin_errors:
             if p[0].polygon():
-                print (p)
-                print(p[0].polygon())
                 rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_comp_pinerrors.rdb_id())
                 rdb_item.add_value(pya.RdbItemValue(p[0].polygon().to_dtype(dbu)))
-                # .transformed(p[1].to_trans().to_itrans(dbu))
 
     if timing:
         print("*** layout_check(), timing; done invalid pins ")
@@ -297,7 +307,20 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
     from SiEPIC.utils import load_Verification
     verification = load_Verification(TECHNOLOGY=TECHNOLOGY)
     if verification:
-        print(verification)
+        if verbose:
+            print(verification)
+            
+        # perform minimum-radius-check: True or False
+        try:
+            minimum_radius_check = eval(
+                verification["verification"]["minimum-radius-check"]
+            )
+        except:
+            minimum_radius_check = True
+    else:
+        minimum_radius_check = True
+
+    if verification:
         # define device-only layers
         try:
             deviceonly_layers = eval(verification['verification']['shapes-inside-components']['deviceonly-layers'])
@@ -330,13 +353,17 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
             iter1.next()
         if verbose:
             print(" - found %s shape(s) not belonging to components " % len(extra_shapes) )
-            for e in extra_shapes:
-                print( "   - %s, %s" % (e[0], e[1]) )
         # add shapes into the results database
         for e in extra_shapes:
             if e[0].dpolygon:
+                if verbose:
+                    print( "   - %s: %s, %s: %s" % (type(e[1]), e[1], type(e[0]), e[0]) )
+                    print( "    - %s, %s" % (e[1].to_trans().to_itrans(dbu), e[0].dpolygon) )
                 rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_comp_shapesoutside.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue(e[0].dpolygon.transformed(e[1].to_trans().to_itrans(dbu))))
+                rdb_item.add_value(pya.RdbItemValue(e[0].polygon.transformed(e[1]).to_dtype(dbu)))
+
+
+
 
     if timing:
         print("*** layout_check(), timing; done shapes in component ")
@@ -344,16 +371,20 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
 
 
     # Experimental, attempt to break up the circuit into regions connected by DevRec layers
+    '''
     region = pya.Region()
     for i in range(0, len(components)):
         c = components[i]
         region += pya.Region(c.polygon)
-    print ('DevRec Regions: original %s, merged %s' % (region.count(), region.merge().count()))
+    if verbose:
+        print ('DevRec Regions: original %s, merged %s' % (region.count(), region.merge().count()))
+    '''
+
     '''
     Approach: create lists of components for each merged region, then do the verification on a per-merged-region basis
     reduce the O(n**2) to O((n/10)**2)  (assuming on average 10 components per circuit)
     '''
-
+    
     if timing:
         print("*** layout_check(), timing; counting merged DevRec regions")
         print('    Time elapsed: %s' % (time() - time1))    
@@ -370,19 +401,27 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
         # it could be done perhaps as a parameter (points)
         if c.basic_name == "Waveguide" and c.cell.is_pcell_variant():
             pcell_params = c.cell.pcell_parameters_by_name()
-            Dpath = pcell_params['path']
-            if 'radius' in pcell_params:
-                radius = pcell_params['radius']
-            else:
-                radius = 5
-            if verbose:
-                print(" - Waveguide: cell: %s, %s" % (c.cell.name, radius))
-
-            # Radius check:
-            if not Dpath.radius_check(radius):
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_wg_radius.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue( "The minimum radius is set at %s microns for this waveguide." % (radius) ))
-                rdb_item.add_value(pya.RdbItemValue(Dpath))
+            Dpath = pcell_params["path"]
+            if minimum_radius_check:
+                if "radius" in pcell_params:
+                    radius = pcell_params["radius"]
+                else:
+                    radius = 5
+                if verbose:
+                    print(" - Waveguide: cell: %s, %s" % (c.cell.name, radius))
+    
+                # Radius check:
+                if not Dpath.radius_check(radius):
+                    rdb_item = rdb.create_item(
+                        rdb_cell.rdb_id(), rdb_cat_id_wg_radius.rdb_id()
+                    )
+                    rdb_item.add_value(
+                        pya.RdbItemValue(
+                            "The minimum radius is set at %s microns for this waveguide."
+                            % (radius)
+                        )
+                    )
+                    rdb_item.add_value(pya.RdbItemValue(Dpath))
 
             # Check for waveguides with too few bend points
 
@@ -398,23 +437,41 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
             rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
 
         # check all the component's pins to check if they are assigned a net:
+        r1 = pya.Region(c.polygon) # Component's DevRec region
         for pin in c.pins:
-            if pin.type == _globals.PIN_TYPES.OPTICAL and pin.net.idx == None:
-                # disconnected optical pin
-                if verbose:
-                    print(" - Found disconnected pin, type %s, at (%s)" % (pin.type, pin.center))
-                rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_discpin.rdb_id())
-                rdb_item.add_value(pya.RdbItemValue(pin.path.to_dtype(dbu)))
+            if pin.type == _globals.PIN_TYPES.OPTICAL:
+                if pin.net.idx == None:
+                    # disconnected optical pin
+                    if verbose:
+                        print(" - Found disconnected pin, type %s, at (%s)" % (pin.type, pin.center))
+                        pin.display()
+                    rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_discpin.rdb_id())
+                    rdb_item.add_value(pya.RdbItemValue(pin.path.to_dtype(dbu)))
+
+                # Check for pin errors, facing the wrong way in the Component
+                # ***** 
+                pts = pin.path.get_points()[0]
+                px, py = pts.x, pts.y
+                test_box = pya.Box(px - 1, py - 1, px + 1, py + 1)
+                r2 = pya.Region(test_box)
+                polygon_and = [p for p in r1 & r2]
+                if not polygon_and: 
+                    # Pin's first point is not inside the DevRec
+                    test_box = pya.Box(px - 5, py - 5, px + 5, py + 5)
+                    rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_comp_pinerrors.rdb_id())
+                    rdb_item.add_value(pya.RdbItemValue(pya.Polygon(test_box).to_dtype(dbu)))
+                    rdb_item.add_value(pya.RdbItemValue(
+                        "The components with the pin problem is: " + c.component))
+                    if verbose:
+                        print (str(test_box))
 
         # Verification: overlapping components (DevRec)
             # automatically takes care of waveguides crossing other waveguides & components
         # Region: put in two DevRec polygons (in raw), measure area, merge, check if are is the same
         #  checks for touching but not overlapping DevRecs
         for i2 in range(i + 1, len(components)):
-            
             c2 = components[i2]
-            r1 = pya.Region(c.polygon)
-            r2 = pya.Region(c2.polygon)
+            r2 = pya.Region(c2.polygon) # Component's DevRec region
             polygon_and = [p for p in r1 & r2]
             if polygon_and:
                 print(" - Found overlapping components: %s, %s" % (c.component, c2.component))
@@ -441,6 +498,28 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
                         rdb_item.add_value(pya.RdbItemValue( "Cell %s should be %s degrees" % (ci,DFT_GC_angle) ))
                         rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
 
+                # minimum-gc-spacing        
+                #  grating couplers need to be far enough apart for the automated probe station so it doesn't get confused
+                # check if the component "c" in the loop is a grating coupler:
+                if 'minimum-gc-spacing' in DFT['design-for-test']['grating-couplers'].keys():
+                    test = [ci.startswith(k) for k in DFT['design-for-test']['grating-couplers']['gc-orientation'].keys()]
+                    if any(test):
+                        min_gc_spacing = float(DFT['design-for-test']['grating-couplers']['minimum-gc-spacing']) / dbu
+                        for i2 in range(i + 1, len(components)):
+                            c2 = components[i2]
+                            c2i = c2.basic_name
+                            # check if the 2nd component "c2" in the loop is a grating coupler:
+                            test = [c2i.startswith(k) for k in DFT['design-for-test']['grating-couplers']['gc-orientation'].keys()]
+                            if any(test):
+                                # compare two grating coupler distances, versus the rule
+                                dist = (c.trans.disp-c2.trans.disp).abs()
+                                # print('dist: %s, %s' % (dist, min_gc_spacing))
+                                if dist < min_gc_spacing:
+                                    rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_GC_min_spacing.rdb_id())
+                                    rdb_item.add_value(pya.RdbItemValue( "Grating couplers should be at least %s microns apart (center-to-center pitch)" % (min_gc_spacing) ))
+                                    rdb_item.add_value(pya.RdbItemValue(c.polygon.to_dtype(dbu)))
+                                    rdb_item.add_value(pya.RdbItemValue(c2.polygon.to_dtype(dbu)))
+
 
         # Pre-simulation check: do components have models?
         # disabled by lukasc, 2021/05
@@ -497,7 +576,7 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
                 # find the GC closest to the opt_in label.
                 components_sorted = sorted([c for c in components if [p for p in c.pins if p.type == _globals.PIN_TYPES.OPTICALIO]],
                                            key=lambda x: x.trans.disp.to_p().distance(pya.Point(t.x, t.y).to_dtype(1)))
-                # GC too far check:
+                # GC opt_in label too far check:
                 if components_sorted:
                     dist_optin_c = components_sorted[0].trans.disp.to_p(
                     ).distance(pya.Point(t.x, t.y).to_dtype(1))
@@ -510,7 +589,10 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
                                   (components_sorted[0].instance, opt_in[ti1]['opt_in']))
                         rdb_item = rdb.create_item(rdb_cell.rdb_id(), rdb_cat_id_optin_toofar.rdb_id())
                         rdb_item.add_value(pya.RdbItemValue(pya.Polygon(box).to_dtype(dbu)))
-    
+                        rdb_item.add_value(pya.RdbItemValue(components_sorted[0].polygon.to_dtype(dbu)))
+                        # it would be nice to highlight the entire text, but bbox returns a point https://www.klayout.de/doc-qt5/code/class_DText.html#method18
+                        # rdb_item.add_value(pya.RdbItemValue(t.bbox()))
+
                     # starting with each opt_in label, identify the sub-circuit, then GCs, and
                     # check for GC spacing
                     trimmed_nets, trimmed_components = trim_netlist(
@@ -612,8 +694,9 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
             v = pya.MessageBox.warning(
                 "Errors", msg, pya.MessageBox.Ok)
             lv.show_rdb(rdb_i, cv.cell_index)
-        else: 
-            print(msg)
+        print(msg)
+        for e in rdb.each_item():
+            print('Error: %s: %s' % (rdb.category_by_id(e.category_id()).name(),rdb.category_by_id(e.category_id()).description))
     else:
         if Python_Env == 'KLayout_GUI':
             v = pya.MessageBox.warning("Errors", "No layout errors detected.", pya.MessageBox.Ok)
@@ -656,5 +739,5 @@ def layout_check(cell=None, verbose=False, GUI=False, timing=False, file_rdb = N
     print('SiEPIC-Tools functional verification')
     from SiEPIC.utils import get_layout_variables
     TECHNOLOGY, lv, layout, cell = get_layout_variables()  
-    num_errors = layout_check(cell=cell, verbose=True)
+    num_errors = layout_check(cell=cell, verbose=False)
     
\ No newline at end of file
diff --git a/klayout_dot_config/python/pyproject.toml b/klayout_dot_config/python/pyproject.toml
index 4df2d744a..e24bd3217 100644
--- a/klayout_dot_config/python/pyproject.toml
+++ b/klayout_dot_config/python/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "SiEPIC"
-version = "0.5.5"
+version = "0.5.16"
 authors = [
   { name="Lukas Chrostowski", email="lukasc@ece.ubc.ca" },
 ]
@@ -14,7 +14,10 @@ classifiers = [
 ]
 dependencies = [
   "numpy", 
-  "scipy"] 
+  "scipy",
+  "pandas",
+  "requests",
+  "klayout"] 
 
 [project.urls]
 Homepage = "https://github.com/SiEPIC/SiEPIC-Tools"
diff --git a/klayout_dot_config/tech/GSiP/DFT.xml b/klayout_dot_config/tech/GSiP/DFT.xml
index d041e1f1a..d9f366dab 100644
--- a/klayout_dot_config/tech/GSiP/DFT.xml
+++ b/klayout_dot_config/tech/GSiP/DFT.xml
@@ -19,6 +19,6 @@
 		<wavelength-start>1500</wavelength-start>
 		<wavelength-stop>1600</wavelength-stop>
 		<wavelength-points>3000</wavelength-points>
-		
+		<polarization>TE</polarization>		
 	</tunable-laser>
 </design-for-test>
diff --git a/klayout_dot_config/tech/GSiP/GSiP.lyt b/klayout_dot_config/tech/GSiP/GSiP.lyt
index 7f9105269..2067f4cbd 100644
--- a/klayout_dot_config/tech/GSiP/GSiP.lyt
+++ b/klayout_dot_config/tech/GSiP/GSiP.lyt
@@ -4,8 +4,9 @@
  <description/>
  <group/>
  <dbu>0.001</dbu>
+ <default-grids/>
  <base-path/>
- <original-base-path>/home/lukasc/Documents/GitHub/SiEPIC-Tools/klayout_dot_config/tech/GSiP</original-base-path>
+ <original-base-path>/Users/lukasc/Documents/GitHub/SiEPIC-Tools/klayout_dot_config/tech/GSiP</original-base-path>
  <layer-properties_file>klayout_Layers_GSiP.lyp</layer-properties_file>
  <add-other-layers>true</add-other-layers>
  <reader-options>
@@ -70,23 +71,9 @@
    <read-lef-with-def>true</read-lef-with-def>
    <macro-resolution-mode>default</macro-resolution-mode>
    <separate-groups>false</separate-groups>
+   <joined-paths>false</joined-paths>
    <map-file/>
   </lefdef>
-  <mebes>
-   <invert>false</invert>
-   <subresolution>true</subresolution>
-   <produce-boundary>true</produce-boundary>
-   <num-stripes-per-cell>64</num-stripes-per-cell>
-   <num-shapes-per-cell>0</num-shapes-per-cell>
-   <data-layer>1</data-layer>
-   <data-datatype>0</data-datatype>
-   <data-name>DATA</data-name>
-   <boundary-layer>0</boundary-layer>
-   <boundary-datatype>0</boundary-datatype>
-   <boundary-name>BORDER</boundary-name>
-   <layer-map>layer_map()</layer-map>
-   <create-other-layers>true</create-other-layers>
-  </mebes>
   <dxf>
    <dbu>0.001</dbu>
    <unit>1</unit>
diff --git a/klayout_dot_config/tech/GSiP/WAVEGUIDES.xml b/klayout_dot_config/tech/GSiP/WAVEGUIDES.xml
index 8b9e66dcf..08f948ccf 100755
--- a/klayout_dot_config/tech/GSiP/WAVEGUIDES.xml
+++ b/klayout_dot_config/tech/GSiP/WAVEGUIDES.xml
@@ -11,6 +11,11 @@
       <width>0.5</width>
       <offset>0.0</offset>
     </component>
+	  <component>
+	    <layer>DevRec</layer>
+	    <width>1.5</width>
+	    <offset>0.0</offset>
+	  </component>
   </waveguide>
   <waveguide>
     <name>Slot</name>
diff --git a/klayout_dot_config/tech/GSiP/__init__.py b/klayout_dot_config/tech/GSiP/__init__.py
index fa595dd81..3b205410d 100644
--- a/klayout_dot_config/tech/GSiP/__init__.py
+++ b/klayout_dot_config/tech/GSiP/__init__.py
@@ -1,4 +1,4 @@
-print('SiEPIC-GSiP PDK Python module: siepic_gsip_pdk, KLayout technology: GSipP')
+print('SiEPIC-GSiP PDK Python module: siepic_gsip_pdk, KLayout technology: GSiP')
 
 # Load the KLayout technology, when running in Script mode
 import pya, os
diff --git a/klayout_dot_config/tech/GSiP/klayout_Layers_GSiP.lyp b/klayout_dot_config/tech/GSiP/klayout_Layers_GSiP.lyp
index 75c645812..c667a20bb 100644
--- a/klayout_dot_config/tech/GSiP/klayout_Layers_GSiP.lyp
+++ b/klayout_dot_config/tech/GSiP/klayout_Layers_GSiP.lyp
@@ -528,6 +528,24 @@
   <name>DevRec</name>
   <source>68/0@1</source>
  </properties>
+ <properties>
+    <frame-color>#004080</frame-color>
+    <fill-color>#004080</fill-color>
+    <frame-brightness>0</frame-brightness>
+    <fill-brightness>0</fill-brightness>
+    <dither-pattern>I0</dither-pattern>
+    <line-style/>
+    <valid>true</valid>
+    <visible>true</visible>
+    <transparent>false</transparent>
+    <width>1</width>
+    <marked>false</marked>
+    <xfill>false</xfill>
+    <animation>0</animation>
+    <name>BlackBox</name>
+    <source>998/0@1</source>
+</properties>
+
  <properties>
   <frame-color/>
   <fill-color/>
diff --git a/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py b/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py
deleted file mode 100644
index 769343820..000000000
--- a/klayout_dot_config/tech/GSiP/pymacros/GSiP_Library.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""
-This file is part of the SiEPIC-Tools and SiEPIC-GSiP PDK
-by Lukas Chrostowski (c) 2015-2017
-
-This Python file implements a library called "GSiP" for scripted and GUI-based layout flows. 
-
-Crash warning:
- https://www.klayout.de/forum/comments.php?DiscussionID=734&amp;page=1#Item_13
- This library has nested PCells. Running this macro with a layout open may
- cause it to crash. Close the layout first before running.
-
-Version history:
-
-Mustafa Hammood 2020/6/25
-- Refactored PCells out of library files into individual files in a subdirectory
-
-Jaspreet Jhoja 2020/5/23
-- Refactored PCells to make them compatible with both, GUI and script-based layout operations
-
-Stefan Preble and Karl McNulty (RIT) 2019/6/13
- - Wireguide : Path to metal wires 
-
-Lukas Chrostowski 2017/12/16
- - compatibility with KLayout 0.25 and SiEPIC-Tools
-
-Lukas Chrostowski
- - GDS cells (detector, etc) and PCells (ring modulator, filter)
- 
-todo:
-replace:     
- layout_arc_wg_dbu(self.cell, Layerm1N, x0,y0, r_m1_in, w_m1_in, angle_min_doping, angle_max_doping)
-with:
- self.cell.shapes(Layerm1N).insert(pya.Polygon(arc(w_m1_in, angle_min_doping, angle_max_doping) + [pya.Point(0, 0)]).transformed(t))
-"""
-
-folder = 'pcells_GSiP'
-verbose = False
-
-import os, sys, pathlib
-
-dir_path = os.path.dirname(os.path.realpath(__file__))
-if dir_path not in sys.path:
-    sys.path.append(dir_path)
-
-try:
-    import SiEPIC
-except:
-    dir_path_SiEPIC = os.path.join(dir_path, '../../../python')
-    sys.path.append(dir_path_SiEPIC)
-    import SiEPIC
-
-from SiEPIC._globals import KLAYOUT_VERSION, KLAYOUT_VERSION_3
-if KLAYOUT_VERSION < 28:
-    question = pya.QMessageBox()
-    question.setStandardButtons(pya.QMessageBox.Ok)
-    question.setText("This PDK is not compatible with older versions (<0.28) of KLayout.")
-    KLayout_link0='https://www.klayout.de/build.html'
-    question.setInformativeText("\nThis PDK is not compatible with older versions (<0.28) of KLayout.\nPlease download an install the latest version, from %s" % (KLayout_link0))
-    pya.QMessageBox_StandardButton(question.exec_())
-
-files = [f for f in os.listdir(os.path.join(os.path.dirname(
-    os.path.realpath(__file__)),folder)) if '.py' in pathlib.Path(f).suffixes  and '__init__' not in f]
-import importlib
-pcells_GSiP = importlib.import_module(folder) 
-importlib.invalidate_caches()
-pcells_=[]
-for f in files:
-    module = '%s.%s' % (folder, f.replace('.py',''))  ### folder name ###
-    if verbose:
-        print(' - found module: %s' % module)
-    m = importlib.import_module(module) 
-    if verbose:
-        print(m)
-    pcells_.append(importlib.reload(m))
-
-import pya
-
-class GSiP(pya.Library):
-  def __init__(self):
-    tech_name = "GSiP"
-    library = tech_name
-    self.technology=tech_name
-
-
-    if verbose:
-        print("Initializing '%s' Library." % library)
-
-    # Set the description
-    self.description = "SiEPIC Generic SiP"
-
-    # Save the path, used for loading WAVEGUIDES.XML
-    import os
-    self.path = os.path.dirname(os.path.realpath(__file__))
-
-    # Import all the GDS files from the tech folder
-    import os, fnmatch
-    dir_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../gds/building_blocks"))
-    if verbose:
-        print('  library path: %s' % dir_path)
-    search_str = '*.[Oo][Aa][Ss]' # OAS
-    for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
-        for filename in fnmatch.filter(filenames, search_str):
-            file1=os.path.join(root, filename)
-            if verbose:
-                print(" - reading %s" % file1 )
-            self.layout().read(file1)
-    search_str = '*.[Gg][Dd][Ss]' # GDS
-    for root, dirnames, filenames in os.walk(dir_path, followlinks=True):
-        for filename in fnmatch.filter(filenames, search_str):
-            file1=os.path.join(root, filename)
-            if verbose:
-                print(" - reading %s" % file1 )
-            self.layout().read(file1)
-                        
-    # Create the PCell declarations
-    for m in pcells_:
-        mm = m.__name__.replace('%s.' % folder,'')
-        mm2 = m.__name__+'.'+mm+'()'
-        if verbose:
-            print(' - register_pcell %s, %s' % (mm,mm2))
-        self.layout().register_pcell(mm, eval(mm2))
-                
-    if verbose:
-        print(' done with pcells')
-    
-    # Register us the library with the technology name
-    # If a library with that name already existed, it will be replaced then.
-    self.register(library)
-     
-  
-GSiP()
diff --git a/klayout_dot_config/tech/GSiP/pymacros/__init__.py b/klayout_dot_config/tech/GSiP/pymacros/__init__.py
index 0c62ae9ab..83b3191d6 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/__init__.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/__init__.py
@@ -1,8 +1,51 @@
-print('SiEPIC-GSiP PDK Python module: pymacros')
+# $autorun
+"""
+This file is part of the SiEPIC-Tools and SiEPIC-GSiP PDK
+by Lukas Chrostowski (c) 2015-2017
 
-from . import GSiP_Library
+This Python file implements a library called "GSiP" for scripted and GUI-based layout flows. 
 
+Crash warning:
+ https://www.klayout.de/forum/comments.php?DiscussionID=734&amp;page=1#Item_13
+ This library has nested PCells. Running this macro with a layout open may
+ cause it to crash. Close the layout first before running.
 
+Version history:
 
+Mustafa Hammood 2020/6/25
+- Refactored PCells out of library files into individual files in a subdirectory
 
+Jaspreet Jhoja 2020/5/23
+- Refactored PCells to make them compatible with both, GUI and script-based layout operations
+
+Stefan Preble and Karl McNulty (RIT) 2019/6/13
+ - Wireguide : Path to metal wires 
+
+Lukas Chrostowski 2017/12/16
+ - compatibility with KLayout 0.25 and SiEPIC-Tools
+
+Lukas Chrostowski
+ - GDS cells (detector, etc) and PCells (ring modulator, filter)
+ 
+Lukas 2023/11
+ - compatibility with PyPI usage of KLayout
+ 
+Lukas 2024/10
+ - moving all the library loading code into SiEPIC.scripts.load_klayout_library
+ 
+"""
+
+print('SiEPIC-GSiP PDK Python module: load library GDS/OAS cells and PCells')
+
+verbose = False
+
+tech = 'GSiP'
+
+from SiEPIC.scripts import load_klayout_library , technology_libraries
+
+# Load the library
+load_klayout_library(tech, tech, 'SiEPIC Generic SiP, v1.1', 'gds/building_blocks','pymacros/pcells_GSiP', verbose=verbose)
+
+# List the libraries loaded       
+technology_libraries(tech)
 
diff --git a/klayout_dot_config/tech/GSiP/pymacros/benchmarking/benchmark_MZIs.py b/klayout_dot_config/tech/GSiP/pymacros/benchmarking/benchmark_MZIs.py
new file mode 100644
index 000000000..66c03d617
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/benchmarking/benchmark_MZIs.py
@@ -0,0 +1,127 @@
+'''
+--- Benchmarking: MZI ---
+  
+by Lukas Chrostowski, 2024
+ 
+
+Example simple script to
+ - create a new layout with a top cell
+ - create many MZI variants
+ - export to OASIS for submission to fabrication
+
+using SiEPIC-Tools function including connect_pins_with_waveguide and connect_cell
+
+Use instructions:
+
+Run in Python, e.g., VSCode
+
+pip install required packages:
+ - klayout, SiEPIC, GSiP, numpy
+
+To install the GSiP PDK from a GitHub Desktop local copy: 
+pip install -e GitHub/SiEPIC-Tools/klayout_dot_config/tech
+
+'''
+
+import time
+# Start timer
+start_time = time.time()
+
+top_cell_name = 'GSiP_MZI'
+export_type = 'static'  # static: for fabrication, PCell: include PCells in file
+
+import pya
+from pya import *
+
+import SiEPIC
+from SiEPIC._globals import Python_Env
+from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+from SiEPIC.utils.layout import new_layout, floorplan
+from SiEPIC.extend import to_itype
+from SiEPIC.verification import layout_check
+
+import os
+
+if Python_Env == 'Script':
+    # For external Python mode, when installed using pip install siepic_ebeam_pdk
+    import GSiP
+
+# Calculate and print execution time
+execution_time = time.time() - start_time
+print(f"Execution time (load PDK and Py modules): {execution_time} seconds")
+
+tech_name = 'GSiP'
+
+'''
+Create a new layout using the EBeam technology,
+with a top cell
+and Draw the floor plan
+'''    
+cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
+# floorplan(cell, 605e3, 410e3)
+
+dbu = ly.dbu
+
+from SiEPIC.scripts import connect_pins_with_waveguide, connect_cell
+waveguide_type='Strip'
+
+# Load cells from library
+cell_ebeam_y = ly.create_cell('YBranch_te1550', tech_name)
+
+# Array of MZI, and spacing:
+n_x, n_y = 10, 10
+dx, dy = 100e3, 100e3
+# Each MZI 
+dx_mzi = 50e3
+dl_ij = 0.5
+for i in range(n_x):
+    for j in range(n_y):
+        
+        # Y branches:
+        # Version 1: place it at an absolute position:
+        t = Trans(dx*i,dy*j)
+        instY1 = cell.insert(CellInstArray(cell_ebeam_y.cell_index(), t))
+        t = Trans(Trans.R180,dx*i+dx_mzi,dy*j)
+        instY2 = cell.insert(CellInstArray(cell_ebeam_y.cell_index(), t))
+
+        # Waveguides:
+        connect_pins_with_waveguide(instY1, 'opt2', instY2, 'opt3', waveguide_type=waveguide_type,turtle_A=[5,90, 0, 90], turtle_B=[5,-90, 10+dl_ij*(i*n_y+j),90])
+        connect_pins_with_waveguide(instY1, 'opt3', instY2, 'opt2', waveguide_type=waveguide_type,turtle_A=[5,-90, 0, -90], turtle_B=[5,90, 10,-90])
+
+# Zoom out
+zoom_out(cell)
+
+
+# Export for fabrication, removing PCells
+path = os.path.dirname(os.path.realpath(__file__))
+filename, extension = os.path.splitext(os.path.basename(__file__))
+if export_type == 'static':
+    file_out = export_layout(cell, path, filename, relative_path = '..', format='oas', screenshot=True)
+else:
+    file_out = os.path.join(path,'..',filename+'.oas')
+    ly.write(file_out)
+
+
+# Calculate and print execution time
+end_time = time.time()
+execution_time = end_time - start_time
+print(f"Execution time (generate and save GDS): {execution_time} seconds")
+
+# Verify
+Verify = False
+if Verify:
+    file_lyrdb = os.path.join(path,filename+'.lyrdb')
+    num_errors = layout_check(cell = cell, verbose=False, GUI=True, file_rdb=file_lyrdb)
+    print('Number of errors: %s' % num_errors)
+else:
+    file_lyrdb = None
+    
+# Display the layout in KLayout, using KLayout Package "klive", which needs to be installed in the KLayout Application
+if Python_Env == 'Script':
+    from SiEPIC.utils import klive
+    klive.show(file_out, lyrdb_filename=file_lyrdb, technology=tech_name)
+
+# Calculate and print execution time
+end_time = time.time()
+execution_time = end_time - start_time
+print(f"Execution time: {execution_time} seconds")
diff --git a/klayout_dot_config/tech/GSiP/pymacros/GSiP Library.lym b/klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP Library.lym
similarity index 98%
rename from klayout_dot_config/tech/GSiP/pymacros/GSiP Library.lym
rename to klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP Library.lym
index f71cd429b..c31ccb46c 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/GSiP Library.lym
+++ b/klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP Library.lym
@@ -6,8 +6,9 @@
  <prolog/>
  <epilog/>
  <doc/>
- <autorun>true</autorun>
+ <autorun>false</autorun>
  <autorun-early>false</autorun-early>
+ <priority>0</priority>
  <shortcut/>
  <show-in-menu>false</show-in-menu>
  <group-name/>
diff --git a/klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP_Library.py b/klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP_Library.py
new file mode 100644
index 000000000..f79687b0a
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/deprecated/GSiP_Library.py
@@ -0,0 +1,39 @@
+"""
+This file is part of the SiEPIC-Tools and SiEPIC-GSiP PDK
+by Lukas Chrostowski (c) 2015-2017
+
+This Python file implements a library called "GSiP" for scripted and GUI-based layout flows. 
+
+Crash warning:
+ https://www.klayout.de/forum/comments.php?DiscussionID=734&amp;page=1#Item_13
+ This library has nested PCells. Running this macro with a layout open may
+ cause it to crash. Close the layout first before running.
+
+Version history:
+
+Mustafa Hammood 2020/6/25
+- Refactored PCells out of library files into individual files in a subdirectory
+
+Jaspreet Jhoja 2020/5/23
+- Refactored PCells to make them compatible with both, GUI and script-based layout operations
+
+Stefan Preble and Karl McNulty (RIT) 2019/6/13
+ - Wireguide : Path to metal wires 
+
+Lukas Chrostowski 2017/12/16
+ - compatibility with KLayout 0.25 and SiEPIC-Tools
+
+Lukas Chrostowski
+ - GDS cells (detector, etc) and PCells (ring modulator, filter)
+ 
+Lukas 2023/11
+ - compatibility with PyPI usage of KLayout
+ 
+Lukas 2024/10
+ - moving all the library loading code into SiEPIC.scripts.load_klayout_library
+ 
+"""
+
+from SiEPIC.scripts import load_klayout_library
+
+load_klayout_library('GSiP', 'GSiP', 'SiEPIC Generic SiP, v1.1', 'gds/building_blocks','pymacros/pcells_GSiP', verbose=False)
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring.py b/klayout_dot_config/tech/GSiP/pymacros/deprecated/Ring.py
similarity index 100%
rename from klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring.py
rename to klayout_dot_config/tech/GSiP/pymacros/deprecated/Ring.py
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/FaML_Si_1550_BB.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/FaML_Si_1550_BB.py
new file mode 100644
index 000000000..57f9cb205
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/FaML_Si_1550_BB.py
@@ -0,0 +1,184 @@
+import pya
+import math
+from SiEPIC._globals import PIN_LENGTH as pin_length
+from SiEPIC.extend import to_itype
+from SiEPIC.scripts import path_to_waveguide, connect_pins_with_waveguide, connect_cell
+from SiEPIC.utils import get_technology_by_name, load_Waveguides_by_Tech, get_layout_variables
+
+class FaML_Si_1550_BB(pya.PCellDeclarationHelper):
+    """
+    The PCell declaration for black box cell
+    Facet-attached Micro-Lens (FaML)
+    for Silicon 
+    for 1550 nm operation
+    
+    Authors: Dream Photonics
+    """
+
+    def __init__(self):
+
+        # Important: initialize the super class
+        super(FaML_Si_1550_BB, self).__init__()
+
+        self.technology_name = 'GSiP'
+        TECHNOLOGY = get_technology_by_name(self.technology_name)
+
+        # declare the parameters
+        self.param("num_channels", self.TypeInt, "Number of Channels (0 - 16)", default = 2)
+
+        self.param("ref_wg", self.TypeBoolean, "Include reference waveguide", default=False)
+
+        #declare the layers
+        self.param("silayer", self.TypeLayer, "Si Layer", default = TECHNOLOGY['Si'], hidden=False)
+        self.param("pinrec", self.TypeLayer, "PinRec Layer", default = TECHNOLOGY['PinRec'], hidden=True)
+        self.param("devrec", self.TypeLayer, "DevRec Layer", default = TECHNOLOGY['DevRec'], hidden=True)
+        self.param("fibertarget", self.TypeLayer, "Fiber Target Layer", default=TECHNOLOGY['FbrTgt'], hidden=True)
+        self.param("textl", self.TypeLayer, "Text Layer", default=TECHNOLOGY['Text'], hidden=True)
+        self.param("bb",self.TypeLayer,"BB Layer", default=TECHNOLOGY['BlackBox'], hidden=True)
+
+    def can_create_from_shape_impl(self):
+        return False
+
+
+    def produce(self, layout, layers, parameters, cell):
+        # This is the main part of the implementation: create the layout
+        self.cell = cell
+        self._param_values = parameters
+        self.layout = layout
+
+        #fetch the parameters
+        dbu = self.layout.dbu
+        ly = self.layout
+        shapes = self.cell.shapes
+        
+        LayerSiN = ly.layer(self.silayer)
+        LayerPinRecN = ly.layer(self.pinrec)
+        LayerDevRecN = ly.layer(self.devrec)
+        LayerFbrTgtN = ly.layer(self.fibertarget)
+        LayerTEXTN = ly.layer(self.textl)
+        LayerBBN = ly.layer(self.bb)
+
+        num_channels = self.num_channels
+        offset = to_itype(0,dbu)
+        pitch = to_itype(127,dbu)
+        l_taper = 60e3
+        Lw2 = to_itype(15,dbu)
+        Lw3 = Lw2 + to_itype(20,dbu)
+
+        wavelength = 1550
+
+        waveguide_type = 'Strip'
+        w_waveguide = 500 # nm
+
+        if num_channels < 0:
+            num_channels = 0
+        if num_channels > 16:
+            num_channels = 16
+
+        def circle(x,y,r):
+            npts = 180
+            theta = 2*math.pi/npts
+            pts = []
+            for i in range(0,npts):
+              pts.append(pya.Point.from_dpoint(pya.DPoint((x+r*math.cos(i*theta))/1,(y+r*math.sin(i*theta))/1)))
+            return pts
+
+        #draw one loopback device
+        if self.ref_wg:
+            for ref_loop in range(2):
+                #draw fibre target circle
+                align_circle = circle(offset,-pitch*(ref_loop+1),2/dbu)
+                #place fibre target circle
+                shapes(LayerFbrTgtN).insert(pya.Polygon(align_circle))
+
+        if self.ref_wg:
+            #create waveguide to for loopback
+            loopback_path = pya.DPath([pya.DPoint(0,-127),pya.DPoint((offset + l_taper + Lw2)*dbu+15,-127),pya.DPoint((offset + l_taper + Lw2)*dbu+15,-254),pya.DPoint(0,-254)],0.5)
+            self.layout.technology_name = self.technology_name #required otherwise "create_cell" doesn't load
+            pcell = self.layout.create_cell("Waveguide",self.technology_name,{"path": loopback_path, "waveguide_type": waveguide_type})
+            t = pya.Trans(pya.Trans.R0,0,0)
+            self.cell.copy(pcell,LayerSiN,LayerBBN)
+            wg = self.cell.insert(pya.CellInstArray(pcell.cell_index(),t))
+            wg.flatten()
+            self.cell.clear(LayerDevRecN)
+            self.cell.clear(LayerPinRecN)
+            self.cell.clear(LayerSiN)
+
+
+        ##########################################################################################################################################################################
+        #draw N tapers
+        x = offset + l_taper + Lw3
+        for n_ch in range(int(num_channels)):
+
+            #draw the taper
+            taper_pts = [pya.Point(0,-w_waveguide/2+pitch*n_ch),pya.Point(0,w_waveguide/2+pitch*n_ch),pya.Point(offset + l_taper + Lw3,w_waveguide/2+pitch*n_ch),pya.Point(offset + l_taper + Lw3,-w_waveguide/2+pitch*n_ch)]
+
+            #place the taper
+            shapes(LayerBBN).insert(pya.Polygon(taper_pts))
+
+            #draw and place pin on the waveguide:
+            t = pya.Trans(pya.Trans.R0, x, pitch*n_ch)
+            pin = pya.Path([pya.Point(-pin_length/2,0),pya.Point(pin_length/2,0)], w_waveguide)
+            pin_t = pin.transformed(t)
+            shapes(LayerPinRecN).insert(pin_t)
+            text = pya.Text(f"opt{n_ch+1}",t)
+            shape = shapes(LayerPinRecN).insert(text)
+            shape.text_size = 3/dbu
+   
+            #draw fibre target circle
+            align_circle = circle(offset,pitch*n_ch,2/dbu)
+
+            #place fibre target circle
+            shapes(LayerFbrTgtN).insert(pya.Polygon(align_circle))
+
+        #draw devrec box
+        n_ch = (num_channels-1)
+        if self.ref_wg:
+            devrec_pts = [pya.Point(0,pitch*n_ch+30/dbu),pya.Point(x,pitch*n_ch+30/dbu),pya.Point(x,-pitch*2-30/dbu),pya.Point(0,-pitch*2-30/dbu)]
+        else:
+            devrec_pts = [pya.Point(0,pitch*n_ch+30/dbu),pya.Point(x,pitch*n_ch+30/dbu),pya.Point(x,-30/dbu),pya.Point(0,-30/dbu)]
+        
+        #place devrec box
+        shapes(LayerDevRecN).insert(pya.Polygon(devrec_pts))
+
+        #edge of chip text
+        t = pya.Trans(pya.Trans.R0,0,1/dbu)
+        text = pya.Text("<- Edge of chip",t)
+        shape = shapes(LayerTEXTN).insert(text)
+        shape.text_size = 3/dbu
+
+        #BB description
+        t = pya.Trans(pya.Trans.R0,0,-15/dbu)
+        text = pya.Text("  Number of Channel(s): " + str(num_channels) + "\n  Center Wavelength: " + str(wavelength) + " nm",t)
+        shape = shapes(LayerTEXTN).insert(text)
+        shape.text_size = 3/dbu
+
+        #BB description
+        t = pya.Trans(pya.Trans.R0, 0,-25/dbu)
+        text = pya.Text("<- 25 MFD lens",t)
+        shape = shapes(LayerTEXTN).insert(text)
+        shape.text_size = 3/dbu
+
+        #draw lenses
+        width_lens = to_itype(50, dbu)
+        length_lens = to_itype(50, dbu)
+
+        if self.ref_wg:
+            for n_ch in range(int(num_channels+2)):
+                lens_pts = [pya.Point(0,-width_lens/2+pitch*n_ch-2*pitch), pya.Point(0,width_lens/2+pitch*n_ch-2*pitch), pya.Point(-length_lens,width_lens/2+pitch*n_ch-2*pitch),pya.Point(-length_lens,-width_lens/2+pitch*n_ch-2*pitch)]
+                shapes(LayerBBN).insert(pya.Polygon(lens_pts))
+                lens = circle(-length_lens,pitch*n_ch-2*pitch,25/dbu)
+                shapes(LayerBBN).insert(pya.Polygon(lens))
+        else:
+            for n_ch in range(int(num_channels)):
+                lens_pts = [pya.Point(0,-width_lens/2+pitch*n_ch), pya.Point(0,width_lens/2+pitch*n_ch), pya.Point(-length_lens,width_lens/2+pitch*n_ch),pya.Point(-length_lens,-width_lens/2+pitch*n_ch)]
+                shapes(LayerBBN).insert(pya.Polygon(lens_pts))
+                lens = circle(-length_lens,pitch*n_ch,25/dbu)
+                shapes(LayerBBN).insert(pya.Polygon(lens))
+
+    def display_text_impl(self):
+        # Provide a descriptive text for the cell
+        return "FaML_Si_1550_BB_%s" % (
+            self.num_channels,
+        )
+
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Filter_DB.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Filter_DB.py
index 7bbd82ca1..821d6fb97 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Filter_DB.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Filter_DB.py
@@ -41,10 +41,10 @@ def produce_impl(self):
     from SiEPIC.extend import to_itype
     
     # fetch the parameters
-#    TECHNOLOGY = get_technology_by_name('GSiP')
     dbu = self.layout.dbu
     ly = self.layout
     shapes = self.cell.shapes
+    ly.technology_name='GSiP'
     
     LayerSi = self.silayer
     LayerSi3 = ly.layer(self.si3layer)
@@ -104,9 +104,12 @@ def produce_impl(self):
     # Generate the layout:
    
     # Create the ring resonator
+    from SiEPIC.utils.layout import layout_ring
+    layout_ring(self.cell, LayerSiN, pya.DPoint((self.r+self.w/2), (self.r+self.g+self.w)), self.r, self.w)
+    # ring centre:
     t = pya.Trans(pya.Trans.R0,(self.r+self.w/2)/dbu, (self.r+self.g+self.w)/dbu)
-    pcell = ly.create_cell("Ring", "GSiP", { "layer": LayerSi, "radius": self.r, "width": self.w } )
-    self.cell.insert(pya.CellInstArray(pcell.cell_index(), t))
+#    pcell = ly.create_cell("Ring", "GSiP", { "layer": LayerSi, "radius": self.r, "width": self.w } )
+#    self.cell.insert(pya.CellInstArray(pcell.cell_index(), t))
     
     # Create the two waveguides
     wg1 = pya.Box(x0 - (w_Si3 / 2 + taper_length), -w/2, x0 + (w_Si3 / 2 + taper_length), w/2)
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Modulator_DB.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Modulator_DB.py
index 588c7ed60..1d022ed4a 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Modulator_DB.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Ring_Modulator_DB.py
@@ -147,9 +147,12 @@ def produce_impl(self):
     # Generate the layout:
    
     # Create the ring resonator
+    from SiEPIC.utils.layout import layout_ring
+    layout_ring(self.cell, LayerSiN, pya.DPoint(x0*dbu,y0*dbu), self.r, self.w)
+    # ring centre:
     t = pya.Trans(pya.Trans.R0,x0, y0)
-    pcell = ly.create_cell("Ring", "GSiP", { "layer": self.silayer, "radius": self.r, "width": self.w } )
-    self.cell.insert(pya.CellInstArray(pcell.cell_index(), t))
+    
+    #pcell = ly.create_cell("Ring", "GSiP", { "layer": self.silayer, "radius": self.r, "width": self.w } )
 
     
     # Create the two waveguides
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Waveguide.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Waveguide.py
index af110e4c3..f6a8f5ea9 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Waveguide.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Waveguide.py
@@ -14,9 +14,9 @@ def __init__(self):
     self.param("width", self.TypeDouble, "Width", default = 0.5)
     self.param("adiab", self.TypeBoolean, "Adiabatic", default = False)
     self.param("bezier", self.TypeDouble, "Bezier Parameter", default = 0.35)
-    self.param("layers", self.TypeList, "Layers", default = ['Waveguide'])
-    self.param("widths", self.TypeList, "Widths", default =  [0.5])
-    self.param("offsets", self.TypeList, "Offsets", default = [0])
+    self.param("layers", self.TypeList, "Layers", default = ['Waveguide','DevRec'])
+    self.param("widths", self.TypeList, "Widths", default =  [0.5,1.5])
+    self.param("offsets", self.TypeList, "Offsets", default = [0,0])
     self.param("CML", self.TypeString, "Compact Model Library (CML)", default = 'GSiP')
     self.param("model", self.TypeString, "CML Model name", default = 'wg_strip_integral_1550') 
     self.cellName="Waveguide"
@@ -26,6 +26,8 @@ def display_text_impl(self):
     return "%s_%s" % (self.cellName, self.path)
   
   def coerce_parameters_impl(self):
+    return
+    
     from SiEPIC.extend import to_itype
     print("GSiP.Waveguide coerce parameters")
     
@@ -133,12 +135,12 @@ def produce_impl(self):
     t = Trans(angle, False, pts[0])
     pts_txt = str([ [round(p.to_dtype(dbu).x,3), round(p.to_dtype(dbu).y,3)] for p in pts ]).replace(', ',',')
     text = Text ( \
-      'Spice_param:wg_length=%.3fu wg_width=%.3fu points="%s" radius=%s' %\
-        (waveguide_length, self.width, pts_txt,self.radius ), t, 0.1*wg_width, -1  )
+      'Spice_param:wg_length=%.6f wg_width=%.3fu points="%s" radius=%s' %\
+        (waveguide_length*dbu, self.width, pts_txt,self.radius ), t, 0.1*wg_width, -1  )
     text.halign=halign
     shape = self.cell.shapes(LayerDevRecN).insert(text)
     t = Trans(angle, False, pt4)
     text = Text ( \
-      'Length=%.3fu' %(waveguide_length), t, 0.5*wg_width, -1  )
+      'Length=%.6f' %(waveguide_length*dbu), t, 0.5*wg_width, -1  )
     text.halign=halign
     shape = self.cell.shapes(LayerDevRecN).insert(text)
\ No newline at end of file
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Wireguide.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Wireguide.py
index c444d859a..5cb4bd0c3 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Wireguide.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/Wireguide.py
@@ -13,9 +13,9 @@ def __init__(self):
     self.radius = 0
     self.param("width", self.TypeDouble, "Width", default = 0.5)
     self.adiab = False
-    self.param("layers", self.TypeList, "Layers", default = ['Waveguide'])
-    self.param("widths", self.TypeList, "Widths", default =  [0.5])
-    self.param("offsets", self.TypeList, "Offsets", default = [0])
+    self.param("layers", self.TypeList, "Layers", default = ['ML','DevRec'])
+    self.param("widths", self.TypeList, "Widths", default =  [0.5, 1])
+    self.param("offsets", self.TypeList, "Offsets", default = [0,0])
    
   def display_text_impl(self):
     # Provide a descriptive text for the cell
@@ -42,7 +42,8 @@ def produce_impl(self):
     
     print("GSiP.Wireguide")
     
-    TECHNOLOGY = get_technology_by_name('GSiP') if op_tag=="GUI" else Tech.load_from_xml(lyp_filepath).layers
+    from SiEPIC.utils import get_technology_by_name    
+    TECHNOLOGY = get_technology_by_name('GSiP')
     dbu = self.layout.dbu
     wg_width = to_itype(self.width,dbu)
     path = self.path.to_itype(dbu)
@@ -81,12 +82,12 @@ def produce_impl(self):
             pt_radius = dis2/2
         # wireguide bends:
         if(self.adiab):
-          wg_pts += Path(arc_bezier(pt_radius, 270, 270 + inner_angle_b_vectors(pts[i-1]-pts[i], pts[i+1]-pts[i]), self.bezier, DevRec='DevRec' in self.layers[lr]), 0).transformed(Trans(angle, turn < 0, pts[i])).get_points()
+          wg_pts += Path(arc_bezier(pt_radius, 270, 270 + inner_angle_b_vectors(pts[i-1]-pts[i], pts[i+1]-pts[i]), self.bezier), 0).transformed(Trans(angle, turn < 0, pts[i])).get_points()
         else:
-          wg_pts += Path(arc_xy(-pt_radius, pt_radius, pt_radius, 270, 270 + inner_angle_b_vectors(pts[i-1]-pts[i], pts[i+1]-pts[i]),DevRec='DevRec' in self.layers[lr]), 0).transformed(Trans(angle, turn < 0, pts[i])).get_points()
+          wg_pts += Path(arc_xy(-pt_radius, pt_radius, pt_radius, 270, 270 + inner_angle_b_vectors(pts[i-1]-pts[i], pts[i+1]-pts[i])), 0).transformed(Trans(angle, turn < 0, pts[i])).get_points()
       wg_pts += [pts[-1]]
       wg_pts = pya.Path(wg_pts, 0).unique_points().get_points()
-      wg_polygon = Path(wg_pts, wg_width)
+      wg_polygon = Path(wg_pts, width)
       self.cell.shapes(layer).insert(wg_polygon) # insert the wireguide
        
       #if self.layout.layer(TECHNOLOGY['Wireguide']) == layer:
@@ -103,7 +104,7 @@ def produce_impl(self):
     t = Trans(angle_vector(pts[-1]-pts[-2])/90, False, pts[-1])
     self.cell.shapes(LayerPinRecN).insert(Path([Point(-50, 0), Point(50, 0)], wg_width).transformed(t))
     self.cell.shapes(LayerPinRecN).insert(Text("pin2", t, 0.3/dbu, -1))
-	
+
     LayerDevRecN = self.layout.layer(TECHNOLOGY['DevRec'])
     
     # Compact model information
diff --git a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py
index bc72996d9..8b563f7d8 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py
@@ -1,4 +1,4 @@
-import os, sys
-import SiEPIC
+# import os, sys
+# import SiEPIC
 
-from SiEPIC.utils import get_technology_by_name
+# from SiEPIC.utils import get_technology_by_name
diff --git a/klayout_dot_config/tech/pyproject.toml b/klayout_dot_config/tech/pyproject.toml
new file mode 100644
index 000000000..94e755110
--- /dev/null
+++ b/klayout_dot_config/tech/pyproject.toml
@@ -0,0 +1,31 @@
+[tool.setuptools]
+packages = [
+  "GSiP",
+]
+
+
+[project]
+name = "GSiP"
+version = "0.4.24"
+authors = [
+  { name="Lukas Chrostowski", email="lukasc@ece.ubc.ca" },
+]
+description = "GSiP: generatic process design kit (PDK). "
+readme = "README.md"
+requires-python = ">=3.8"
+classifiers = [
+    "Programming Language :: Python :: 3",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+]
+dependencies = [
+  "SiEPIC>=0.5.4",
+  "numpy",
+  "packaging",
+]
+
+
+[project.urls]
+Homepage = "https://github.com/SiEPIC/SiEPIC-Tools"
+Issues = "https://github.com/SiEPIC/SiEPIC-Tools/issues"
+
diff --git a/pcell_python_GDSfactory.lym b/pcell_python_GDSfactory.lym
index ddd762aac..c1376c4c2 100644
--- a/pcell_python_GDSfactory.lym
+++ b/pcell_python_GDSfactory.lym
@@ -1,231 +1,231 @@
-<?xml version="1.0" encoding="utf-8"?>
-<klayout-macro>
- <description/>
- <version/>
- <category>pymacros</category>
- <prolog/>
- <epilog/>
- <doc/>
- <autorun>true</autorun>
- <autorun-early>false</autorun-early>
- <priority>0</priority>
- <shortcut/>
- <show-in-menu>false</show-in-menu>
- <group-name/>
- <menu-path/>
- <interpreter>python</interpreter>
- <dsl-interpreter-name/>
- <text>
-'''
-PCell created using geometries from GDSfactory
-by: Lukas Chrostowski, 2023
-
-- query the GDSfactory PCell to find out the parameters and defaults
-  use those to populate the GUI
-- pass on the KLayout parameters to GDSfactory
-- generates the GDSfactory layout, and load it into the KLayout PCell
-
-'''
-
-# klayout api
-import pya
-import gdsfactory as gf
-import inspect
-from SiEPIC.utils.layout import make_pin
-from SiEPIC.utils import get_technology_by_name
-from gdsfactory.add_padding import get_padding_points
-
-class mmi1x2(pya.PCellDeclarationHelper):
-
-  def __init__(self):
-
-    # Important: initialize the super class
-    super(mmi1x2, self).__init__()
-
-    # query the GDSfactory PCell to get parameters and add to KLayout
-    #import gdsfactory as gf
-    #import inspect
-    sig = inspect.signature(gf.components.mmi1x2)
-    params = sig.parameters
-    for p in params.values():
-        if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
-            param_name = p.name
-            param_default = p.default
-            param_type = type(param_default)
-            if param_type is type(1.0):
-                self.param(param_name, self.TypeDouble, param_name, default = param_default)
-            if param_type is type(1):
-                self.param(param_name, self.TypeInt, param_name, default = param_default)
-            if param_type is type(True):
-                self.param(param_name, self.TypeBoolean, param_name, default = param_default)
-            # self.TypeLayer, TypeList, TypeNone, TypeShape, TypeString
-            # no layer parameters
-            
-    # add layer parameters
-    # TO DO: lots of this code is duplicate with the code in produce_impl()
-    #        - create a function to take in the gdsfactory component name and return 
-    #          the labels, layers, ports
-    
-    # extracting all pcell information from gdsfactory pcell
-    pcell = gf.components.mmi1x2()
-
-    # get dictionary mapping layers to the polygons (represented by points) contained within
-    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
-
-    # get ports 
-    ports = pcell.get_ports()
-
-    # get gdsfactory layer information
-    layers = pcell.get_layers()
-    layer_names = pcell.get_layer_names()
-
-    # get labels
-    labels = pcell.get_labels()
-    
-    # get bounding box points from gdsfactory
-    bbox_points = get_padding_points(pcell)
-    #bbox = gf.components.bbox(bbox=points, layer=(68,0))
-
-    # translate extracted information into klayout pcell
-    
-    # add layer parameters to pcell decl
-    layers = list(layer_to_polygonpts.keys())
-
-    # include layers for pins (get them from the ports)
-    for port in ports:
-      if port.layer not in layers:
-        layers.append(port.layer)
-
-    # include layers for labels
-    for label in labels:
-      layer = label.layer
-      if layer not in layers:
-        layers.append(layer)
-
-    for l in layers:
-      # how to get the name?
-      # use the layer map?
-      PDK = gf.get_active_pdk()
-      name_to_layer = PDK.layers
-      layer_to_name = {v: k for k, v in name_to_layer.items()}
-      name = layer_to_name[l]
-
-      self.param(name, self.TypeLayer, "{} Layer".format(name), default = LayerInfo(l[0], l[1]))
-
-    # create devrec layer, what about pin rec layers?
-    # for pin layers: checked the layers for each port and created layer parameters for them
-    self.technology_name = 'EBeam'
-    TECHNOLOGY = get_technology_by_name(self.technology_name)
-    self.param("devrec", self.TypeLayer, "DevRec Layer", default = TECHNOLOGY['DevRec'])
-            
-  def display_text_impl(self):
-    # Provide a descriptive text for the cell
-
-    param_list = ''
-    for p in self.get_parameters():
-        param_list += '_' + p.name + '_' + str(eval('self.%s' % p.name))
-
-    return "mmi1x2" + param_list
-    
-  def produce_impl(self):
-
-    # extracting all pcell information from gdsfactory pcell
-    pcell = gf.components.mmi1x2()
-
-    # get dictionary mapping layers to the polygons (represented by points) contained within
-    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
-
-    # get ports 
-    ports = pcell.get_ports()
-
-    # get gdsfactory layer information
-    layers = pcell.get_layers()
-    layer_names = pcell.get_layer_names()
-
-    # get labels
-    labels = pcell.get_labels()
-    
-    # get bounding box points from gdsfactory
-    bbox_points = get_padding_points(pcell)
-
-    # translate extracted information into klayout pcell
-    
-    # add layer parameters to pcell decl
-    layers = list(layer_to_polygonpts.keys())
-
-    # include layers for pins (get them from the ports)
-    for port in ports:
-      if port.layer not in layers:
-        layers.append(port.layer)
-
-    # include layers for labels
-    for label in labels:
-      layer = label.layer
-      if layer not in layers:
-        layers.append(layer)
-
-    # map layer numbers to their name
-    PDK = gf.get_active_pdk()
-    name_to_layer = PDK.layers
-    layer_to_name = {v: k for k, v in name_to_layer.items()}
-    
-    
-    # add polygons to pcell in correct layer 
-    for l in layer_to_polygonpts.keys():
-      polygonpts = layer_to_polygonpts[l]
-      layer_name = layer_to_name[l]
-      
-      layer = self.layout.layer(getattr(self, layer_name))
-
-      # need to convert to a Point array since this is how points are passed into Polygons
-      for pts in polygonpts:
-        klayout_points = []
-        for p in pts:
-          klayout_points.append(Point(p[0],p[1]))
-
-        self.cell.shapes(layer).insert(Polygon(klayout_points))
-      
-    # add pins: get the appropriate port information, then call make_pin() 
-    for port in ports:
-      port_layer_name = layer_to_name[port.layer]
-      port_layer = self.layout.layer(getattr(self, port_layer_name))
-      make_pin(self.cell, port.name, port.center, port.width, port_layer, port.orientation)
-
-    # insert labels into correct layer and position
-    for label in labels:
-      layer_name = layer_to_name[label.layer]
-      klayout_label = Text(label.text, label.origin)
-      self.cell.shapes(self.layout.layer(self.layer_name)).insert(klayout_label)
-
-    # add bounding box to dev rec layer
-    # using bbox points make simple polygon to represent bbox
-    # convert points to a klayout Points array
-    bbox_klayout_points = []
-    for p in bbox_points:
-      bbox_klayout_points.append(Point(p[0],p[1]))
-
-    bbox = SimplePolygon(bbox_klayout_points)
-
-    self.cell.shapes(self.layout.layer(self.devrec)).insert(bbox)
-    
-class GDSfactory_PCellLib(pya.Library):
-
-  def __init__(self):
-  
-    # TODO: change the description
-    self.description = "Generic library"
-    
-    # register the PCell declarations
-    self.layout().register_pcell("mmi1x2", mmi1x2())
-    
-    # register our library
-    self.register("GDSfactory PCells")
-
-    
-# instantiate and register the library
-GDSfactory_PCellLib()
-
-
-
-</text>
-</klayout-macro>
+<?xml version="1.0" encoding="utf-8"?>
+<klayout-macro>
+ <description/>
+ <version/>
+ <category>pymacros</category>
+ <prolog/>
+ <epilog/>
+ <doc/>
+ <autorun>true</autorun>
+ <autorun-early>false</autorun-early>
+ <priority>0</priority>
+ <shortcut/>
+ <show-in-menu>false</show-in-menu>
+ <group-name/>
+ <menu-path/>
+ <interpreter>python</interpreter>
+ <dsl-interpreter-name/>
+ <text>
+'''
+PCell created using geometries from GDSfactory
+by: Lukas Chrostowski, 2023
+
+- query the GDSfactory PCell to find out the parameters and defaults
+  use those to populate the GUI
+- pass on the KLayout parameters to GDSfactory
+- generates the GDSfactory layout, and load it into the KLayout PCell
+
+'''
+
+# klayout api
+import pya
+import gdsfactory as gf
+import inspect
+from SiEPIC.utils.layout import make_pin
+from SiEPIC.utils import get_technology_by_name
+from gdsfactory.add_padding import get_padding_points
+
+class mmi1x2(pya.PCellDeclarationHelper):
+
+  def __init__(self):
+
+    # Important: initialize the super class
+    super(mmi1x2, self).__init__()
+
+    # query the GDSfactory PCell to get parameters and add to KLayout
+    #import gdsfactory as gf
+    #import inspect
+    sig = inspect.signature(gf.components.mmi1x2)
+    params = sig.parameters
+    for p in params.values():
+        if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
+            param_name = p.name
+            param_default = p.default
+            param_type = type(param_default)
+            if param_type is type(1.0):
+                self.param(param_name, self.TypeDouble, param_name, default = param_default)
+            if param_type is type(1):
+                self.param(param_name, self.TypeInt, param_name, default = param_default)
+            if param_type is type(True):
+                self.param(param_name, self.TypeBoolean, param_name, default = param_default)
+            # self.TypeLayer, TypeList, TypeNone, TypeShape, TypeString
+            # no layer parameters
+            
+    # add layer parameters
+    # TO DO: lots of this code is duplicate with the code in produce_impl()
+    #        - create a function to take in the gdsfactory component name and return 
+    #          the labels, layers, ports
+    
+    # extracting all pcell information from gdsfactory pcell
+    pcell = gf.components.mmi1x2()
+
+    # get dictionary mapping layers to the polygons (represented by points) contained within
+    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
+
+    # get ports 
+    ports = pcell.get_ports()
+
+    # get gdsfactory layer information
+    layers = pcell.get_layers()
+    layer_names = pcell.get_layer_names()
+
+    # get labels
+    labels = pcell.get_labels()
+    
+    # get bounding box points from gdsfactory
+    bbox_points = get_padding_points(pcell)
+    #bbox = gf.components.bbox(bbox=points, layer=(68,0))
+
+    # translate extracted information into klayout pcell
+    
+    # add layer parameters to pcell decl
+    layers = list(layer_to_polygonpts.keys())
+
+    # include layers for pins (get them from the ports)
+    for port in ports:
+      if port.layer not in layers:
+        layers.append(port.layer)
+
+    # include layers for labels
+    for label in labels:
+      layer = label.layer
+      if layer not in layers:
+        layers.append(layer)
+
+    for l in layers:
+      # how to get the name?
+      # use the layer map?
+      PDK = gf.get_active_pdk()
+      name_to_layer = PDK.layers
+      layer_to_name = {v: k for k, v in name_to_layer.items()}
+      name = layer_to_name[l]
+
+      self.param(name, self.TypeLayer, "{} Layer".format(name), default = LayerInfo(l[0], l[1]))
+
+    # create devrec layer, what about pin rec layers?
+    # for pin layers: checked the layers for each port and created layer parameters for them
+    self.technology_name = 'EBeam'
+    TECHNOLOGY = get_technology_by_name(self.technology_name)
+    self.param("devrec", self.TypeLayer, "DevRec Layer", default = TECHNOLOGY['DevRec'])
+            
+  def display_text_impl(self):
+    # Provide a descriptive text for the cell
+
+    param_list = ''
+    for p in self.get_parameters():
+        param_list += '_' + p.name + '_' + str(eval('self.%s' % p.name))
+
+    return "mmi1x2" + param_list
+    
+  def produce_impl(self):
+
+    # extracting all pcell information from gdsfactory pcell
+    pcell = gf.components.mmi1x2()
+
+    # get dictionary mapping layers to the polygons (represented by points) contained within
+    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
+
+    # get ports 
+    ports = pcell.get_ports()
+
+    # get gdsfactory layer information
+    layers = pcell.get_layers()
+    layer_names = pcell.get_layer_names()
+
+    # get labels
+    labels = pcell.get_labels()
+    
+    # get bounding box points from gdsfactory
+    bbox_points = get_padding_points(pcell)
+
+    # translate extracted information into klayout pcell
+    
+    # add layer parameters to pcell decl
+    layers = list(layer_to_polygonpts.keys())
+
+    # include layers for pins (get them from the ports)
+    for port in ports:
+      if port.layer not in layers:
+        layers.append(port.layer)
+
+    # include layers for labels
+    for label in labels:
+      layer = label.layer
+      if layer not in layers:
+        layers.append(layer)
+
+    # map layer numbers to their name
+    PDK = gf.get_active_pdk()
+    name_to_layer = PDK.layers
+    layer_to_name = {v: k for k, v in name_to_layer.items()}
+    
+    
+    # add polygons to pcell in correct layer 
+    for l in layer_to_polygonpts.keys():
+      polygonpts = layer_to_polygonpts[l]
+      layer_name = layer_to_name[l]
+      
+      layer = self.layout.layer(getattr(self, layer_name))
+
+      # need to convert to a Point array since this is how points are passed into Polygons
+      for pts in polygonpts:
+        klayout_points = []
+        for p in pts:
+          klayout_points.append(Point(p[0],p[1]))
+
+        self.cell.shapes(layer).insert(Polygon(klayout_points))
+      
+    # add pins: get the appropriate port information, then call make_pin() 
+    for port in ports:
+      port_layer_name = layer_to_name[port.layer]
+      port_layer = self.layout.layer(getattr(self, port_layer_name))
+      make_pin(self.cell, port.name, port.center, port.width, port_layer, port.orientation)
+
+    # insert labels into correct layer and position
+    for label in labels:
+      layer_name = layer_to_name[label.layer]
+      klayout_label = Text(label.text, label.origin)
+      self.cell.shapes(self.layout.layer(self.layer_name)).insert(klayout_label)
+
+    # add bounding box to dev rec layer
+    # using bbox points make simple polygon to represent bbox
+    # convert points to a klayout Points array
+    bbox_klayout_points = []
+    for p in bbox_points:
+      bbox_klayout_points.append(Point(p[0],p[1]))
+
+    bbox = SimplePolygon(bbox_klayout_points)
+
+    self.cell.shapes(self.layout.layer(self.devrec)).insert(bbox)
+    
+class GDSfactory_PCellLib(pya.Library):
+
+  def __init__(self):
+  
+    # TODO: change the description
+    self.description = "Generic library"
+    
+    # register the PCell declarations
+    self.layout().register_pcell("mmi1x2", mmi1x2())
+    
+    # register our library
+    self.register("GDSfactory PCells")
+
+    
+# instantiate and register the library
+GDSfactory_PCellLib()
+
+
+
+</text>
+</klayout-macro>
diff --git a/pcell_python_GDSfactory2.lym b/pcell_python_GDSfactory2.lym
new file mode 100644
index 000000000..c1376c4c2
--- /dev/null
+++ b/pcell_python_GDSfactory2.lym
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="utf-8"?>
+<klayout-macro>
+ <description/>
+ <version/>
+ <category>pymacros</category>
+ <prolog/>
+ <epilog/>
+ <doc/>
+ <autorun>true</autorun>
+ <autorun-early>false</autorun-early>
+ <priority>0</priority>
+ <shortcut/>
+ <show-in-menu>false</show-in-menu>
+ <group-name/>
+ <menu-path/>
+ <interpreter>python</interpreter>
+ <dsl-interpreter-name/>
+ <text>
+'''
+PCell created using geometries from GDSfactory
+by: Lukas Chrostowski, 2023
+
+- query the GDSfactory PCell to find out the parameters and defaults
+  use those to populate the GUI
+- pass on the KLayout parameters to GDSfactory
+- generates the GDSfactory layout, and load it into the KLayout PCell
+
+'''
+
+# klayout api
+import pya
+import gdsfactory as gf
+import inspect
+from SiEPIC.utils.layout import make_pin
+from SiEPIC.utils import get_technology_by_name
+from gdsfactory.add_padding import get_padding_points
+
+class mmi1x2(pya.PCellDeclarationHelper):
+
+  def __init__(self):
+
+    # Important: initialize the super class
+    super(mmi1x2, self).__init__()
+
+    # query the GDSfactory PCell to get parameters and add to KLayout
+    #import gdsfactory as gf
+    #import inspect
+    sig = inspect.signature(gf.components.mmi1x2)
+    params = sig.parameters
+    for p in params.values():
+        if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
+            param_name = p.name
+            param_default = p.default
+            param_type = type(param_default)
+            if param_type is type(1.0):
+                self.param(param_name, self.TypeDouble, param_name, default = param_default)
+            if param_type is type(1):
+                self.param(param_name, self.TypeInt, param_name, default = param_default)
+            if param_type is type(True):
+                self.param(param_name, self.TypeBoolean, param_name, default = param_default)
+            # self.TypeLayer, TypeList, TypeNone, TypeShape, TypeString
+            # no layer parameters
+            
+    # add layer parameters
+    # TO DO: lots of this code is duplicate with the code in produce_impl()
+    #        - create a function to take in the gdsfactory component name and return 
+    #          the labels, layers, ports
+    
+    # extracting all pcell information from gdsfactory pcell
+    pcell = gf.components.mmi1x2()
+
+    # get dictionary mapping layers to the polygons (represented by points) contained within
+    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
+
+    # get ports 
+    ports = pcell.get_ports()
+
+    # get gdsfactory layer information
+    layers = pcell.get_layers()
+    layer_names = pcell.get_layer_names()
+
+    # get labels
+    labels = pcell.get_labels()
+    
+    # get bounding box points from gdsfactory
+    bbox_points = get_padding_points(pcell)
+    #bbox = gf.components.bbox(bbox=points, layer=(68,0))
+
+    # translate extracted information into klayout pcell
+    
+    # add layer parameters to pcell decl
+    layers = list(layer_to_polygonpts.keys())
+
+    # include layers for pins (get them from the ports)
+    for port in ports:
+      if port.layer not in layers:
+        layers.append(port.layer)
+
+    # include layers for labels
+    for label in labels:
+      layer = label.layer
+      if layer not in layers:
+        layers.append(layer)
+
+    for l in layers:
+      # how to get the name?
+      # use the layer map?
+      PDK = gf.get_active_pdk()
+      name_to_layer = PDK.layers
+      layer_to_name = {v: k for k, v in name_to_layer.items()}
+      name = layer_to_name[l]
+
+      self.param(name, self.TypeLayer, "{} Layer".format(name), default = LayerInfo(l[0], l[1]))
+
+    # create devrec layer, what about pin rec layers?
+    # for pin layers: checked the layers for each port and created layer parameters for them
+    self.technology_name = 'EBeam'
+    TECHNOLOGY = get_technology_by_name(self.technology_name)
+    self.param("devrec", self.TypeLayer, "DevRec Layer", default = TECHNOLOGY['DevRec'])
+            
+  def display_text_impl(self):
+    # Provide a descriptive text for the cell
+
+    param_list = ''
+    for p in self.get_parameters():
+        param_list += '_' + p.name + '_' + str(eval('self.%s' % p.name))
+
+    return "mmi1x2" + param_list
+    
+  def produce_impl(self):
+
+    # extracting all pcell information from gdsfactory pcell
+    pcell = gf.components.mmi1x2()
+
+    # get dictionary mapping layers to the polygons (represented by points) contained within
+    layer_to_polygonpts = pcell.get_polygons(by_spec=True)
+
+    # get ports 
+    ports = pcell.get_ports()
+
+    # get gdsfactory layer information
+    layers = pcell.get_layers()
+    layer_names = pcell.get_layer_names()
+
+    # get labels
+    labels = pcell.get_labels()
+    
+    # get bounding box points from gdsfactory
+    bbox_points = get_padding_points(pcell)
+
+    # translate extracted information into klayout pcell
+    
+    # add layer parameters to pcell decl
+    layers = list(layer_to_polygonpts.keys())
+
+    # include layers for pins (get them from the ports)
+    for port in ports:
+      if port.layer not in layers:
+        layers.append(port.layer)
+
+    # include layers for labels
+    for label in labels:
+      layer = label.layer
+      if layer not in layers:
+        layers.append(layer)
+
+    # map layer numbers to their name
+    PDK = gf.get_active_pdk()
+    name_to_layer = PDK.layers
+    layer_to_name = {v: k for k, v in name_to_layer.items()}
+    
+    
+    # add polygons to pcell in correct layer 
+    for l in layer_to_polygonpts.keys():
+      polygonpts = layer_to_polygonpts[l]
+      layer_name = layer_to_name[l]
+      
+      layer = self.layout.layer(getattr(self, layer_name))
+
+      # need to convert to a Point array since this is how points are passed into Polygons
+      for pts in polygonpts:
+        klayout_points = []
+        for p in pts:
+          klayout_points.append(Point(p[0],p[1]))
+
+        self.cell.shapes(layer).insert(Polygon(klayout_points))
+      
+    # add pins: get the appropriate port information, then call make_pin() 
+    for port in ports:
+      port_layer_name = layer_to_name[port.layer]
+      port_layer = self.layout.layer(getattr(self, port_layer_name))
+      make_pin(self.cell, port.name, port.center, port.width, port_layer, port.orientation)
+
+    # insert labels into correct layer and position
+    for label in labels:
+      layer_name = layer_to_name[label.layer]
+      klayout_label = Text(label.text, label.origin)
+      self.cell.shapes(self.layout.layer(self.layer_name)).insert(klayout_label)
+
+    # add bounding box to dev rec layer
+    # using bbox points make simple polygon to represent bbox
+    # convert points to a klayout Points array
+    bbox_klayout_points = []
+    for p in bbox_points:
+      bbox_klayout_points.append(Point(p[0],p[1]))
+
+    bbox = SimplePolygon(bbox_klayout_points)
+
+    self.cell.shapes(self.layout.layer(self.devrec)).insert(bbox)
+    
+class GDSfactory_PCellLib(pya.Library):
+
+  def __init__(self):
+  
+    # TODO: change the description
+    self.description = "Generic library"
+    
+    # register the PCell declarations
+    self.layout().register_pcell("mmi1x2", mmi1x2())
+    
+    # register our library
+    self.register("GDSfactory PCells")
+
+    
+# instantiate and register the library
+GDSfactory_PCellLib()
+
+
+
+</text>
+</klayout-macro>


Test patch:
diff --git a/.github/workflows/run-layout-tests.yml b/.github/workflows/run-layout-tests.yml
index 801b95ce2..5ec0a73d4 100644
--- a/.github/workflows/run-layout-tests.yml
+++ b/.github/workflows/run-layout-tests.yml
@@ -31,30 +31,32 @@ jobs:
 
       - name: install python 3.11 packages
         run: |
-          python -m pip install --upgrade pip
-          pip install klayout numpy scipy pytest pytest-cov
+          pip install pytest pytest-cov IPython
+          pip install -e klayout_dot_config/python
 
       - name: Test with pytest, python 3.11
-        run: pytest --cov=klayout_dot_config/python/SiEPIC klayout_dot_config/tech --cov-report=xml
+        run: pytest --cov=klayout_dot_config/python/SiEPIC --ignore=klayout_dot_config/python/SiEPIC/lumerical --ignore=klayout_dot_config/python/SiEPIC/tidy3d klayout_dot_config --cov-report=xml
+      # run: pytest --cov=klayout_dot_config/python/SiEPIC klayout_dot_config/tech --cov-report=xml
   
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
+        if: github.repository_owner == 'SiEPIC'
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: ./coverage.xml
           fail_ci_if_error: true
 
-      - name: setup python 3.9
-        uses: actions/setup-python@v4
-        with:
-          python-version: '3.9'
-          cache: "pip"
-          cache-dependency-path: pyproject.toml
+      # - name: setup python 3.9
+      #   uses: actions/setup-python@v4
+      #   with:
+      #     python-version: '3.9'
+      #     cache: "pip"
+      #     cache-dependency-path: pyproject.toml
 
-      - name: install python 3.9 packages
-        run: |
-          python -m pip install --upgrade pip
-          pip install klayout numpy scipy pytest pytest-cov
+      # - name: install python 3.9 packages
+      #   run: |
+      #     python -m pip install --upgrade pip
+      #     pip install klayout numpy scipy pytest pytest-cov
 
-      - name: Test with pytest, python 3.9
-        run: pytest klayout_dot_config/tech
+      # - name: Test with pytest, python 3.9
+      #   run: pytest klayout_dot_config/tech
diff --git a/klayout_dot_config/python/SiEPIC/tests/test_box_bezier_corners.py b/klayout_dot_config/python/SiEPIC/tests/test_box_bezier_corners.py
new file mode 100644
index 000000000..b1e03b944
--- /dev/null
+++ b/klayout_dot_config/python/SiEPIC/tests/test_box_bezier_corners.py
@@ -0,0 +1,34 @@
+"""
+Test for SiEPIC.utils.geometry.box_bezier_corners
+
+by Lukas Chrostowski 2024
+
+"""
+
+def test_box_bezier_corners():
+    '''
+    Draw a box, measure the area, and check
+    '''
+
+    import pya
+    import SiEPIC
+    from SiEPIC.utils.geometry import box_bezier_corners
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse("0.5.4"):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    '''
+    Create a box with rounded corners
+    '''    
+    width, height = 100, 100
+    dt_bezier_corner = 0.1
+    dbu = 0.001
+    polygon = box_bezier_corners(width, height, dt_bezier_corner, accuracy = 0.001).to_itype(dbu).transformed(pya.Trans(-width/2,0))
+    
+    # return polygon.area(), polygon.num_points()
+    assert polygon.area() == 9937944371
+    assert polygon.num_points() == 844
+    
+if __name__ == "__main__":
+    test_box_bezier_corners()
diff --git a/klayout_dot_config/python/SiEPIC/tests/test_scripts_layout_diff.py b/klayout_dot_config/python/SiEPIC/tests/test_scripts_layout_diff.py
new file mode 100644
index 000000000..75ef0b878
--- /dev/null
+++ b/klayout_dot_config/python/SiEPIC/tests/test_scripts_layout_diff.py
@@ -0,0 +1,59 @@
+"""
+Test for SiEPIC.scripts.layout_diff
+
+by Lukas Chrostowski 2024
+
+"""
+
+def test_layout_diff():
+    '''
+
+    '''
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.utils.layout import new_layout
+    
+    import os
+
+    if Python_Env == 'Script':
+        # For external Python mode, when installed using pip install siepic_ebeam_pdk
+        import GSiP
+    
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse("0.5.4"):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    '''
+    Create a new layout using the EBeam technology,
+    with a top cell
+    '''    
+    cell, ly = new_layout(tech_name, 'top', GUI=True, overwrite = True)
+
+    waveguide_type_delay='SiN routing TE 1550 nm (compound waveguide)'
+
+    # Load cells from library
+    cell1 = ly.create_cell('Ring_Modulator_DB', 'GSiP',
+                                    {'r':10,
+                                    })
+    cell2 = ly.create_cell('Ring_Modulator_DB', 'GSiP',
+                                    {'r':11,
+                                    })
+
+    from SiEPIC.scripts import layout_diff
+    
+    num_diff = layout_diff(cell1, cell2)
+    print(num_diff)
+    assert num_diff == 123
+
+    cell3 = cell1.dup()
+    num_diff = layout_diff(cell1, cell3)
+    print(num_diff)
+    assert num_diff == 0
+
+if __name__ == "__main__":
+    test_layout_diff()
diff --git a/klayout_dot_config/tech/GSiP/pymacros/deprecated/test_load_libraries.py b/klayout_dot_config/tech/GSiP/pymacros/deprecated/test_load_libraries.py
new file mode 100644
index 000000000..e48b914f4
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/deprecated/test_load_libraries.py
@@ -0,0 +1,33 @@
+
+if 0: 
+    import os
+    print(os.path.split('pymacros/pcells_GSiP')[-1])
+
+if 1:
+    import os 
+    import SiEPIC
+    # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+    import sys
+    p = os.path.abspath(os.path.join(SiEPIC.__path__[0], '../..', 'tech'))
+    sys.path.insert(0,p)
+    #print (p)
+    #print(sys.path)
+    import GSiP
+
+
+
+if 0:
+    import importlib.util
+    import sys
+    from pathlib import Path
+    path = '/Users/lukasc/Documents/GitHub/SiEPIC-Tools/klayout_dot_config/tech/GSiP/pymacros/pcells_GSiP/__init__.py'
+    module_name = 'pcells_GSiP'
+    path = Path(path).resolve()
+    spec = importlib.util.spec_from_file_location(module_name, path)
+    module = importlib.util.module_from_spec(spec)
+    sys.modules[module_name] = module  # Add it to sys.modules
+    spec.loader.exec_module(module)  # Execute the module code
+    print(dir(module))
+    module.Ring_Filter_DB
+
+
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_FaML.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_FaML.py
new file mode 100644
index 000000000..addeaeb68
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_FaML.py
@@ -0,0 +1,114 @@
+"""
+Test for FaML
+
+by Lukas Chrostowski 2024
+
+"""
+
+def test_FaML_two():
+    '''
+    --- Simple MZI, tested using Facet-Attached Micro Lenses (FaML) ---
+    
+    by Lukas Chrostowski, 2024
+    
+    Example simple script to
+    - use the GSiP technology
+    - using KLayout and SiEPIC-Tools, with function including connect_pins_with_waveguide and connect_cell
+    - create a new layout with a top cell
+    - create a Mach-Zehnder Interferometer (MZI) circuits
+    - export to OASIS for submission to fabrication
+    - display the layout in KLayout using KLive
+    
+    Test plan
+    - count lenses from the bottom up (bottom is 1, top is 6, in this design)
+    - laser input on bottom lens (1), detector on second (2), for alignment
+    - MZI1: laser on 3, detector on 4, sweep
+    - MZI2: laser on 5, detector on 6, sweep
+    
+
+    Use instructions:
+
+    Run in Python, e.g., VSCode
+
+    pip install required packages:
+    - klayout, SiEPIC, siepic_ebeam_pdk, numpy
+
+    '''
+
+    designer_name = 'LukasChrostowski'
+    top_cell_name = 'EBeam_%s_MZI2_FaML' % designer_name
+    export_type = 'static'  # static: for fabrication, PCell: include PCells in file
+    #export_type = 'PCell'  # static: for fabrication, PCell: include PCells in file
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan, FaML_two
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+    
+    import os
+
+    if Python_Env == 'Script':
+        # For external Python mode, when installed using pip install siepic_ebeam_pdk
+        import GSiP
+
+    print('EBeam_LukasChrostowski_MZI2 layout script')
+    
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse("0.5.4"):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    '''
+    Create a new layout using the EBeam technology,
+    with a top cell
+    and Draw the floor plan
+    '''    
+    cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
+    floorplan(cell, 1000e3, 244e3)
+
+    waveguide_type1='Strip'
+
+    #######################
+    # Circuit #1 – Loopback
+    #######################
+    # draw two edge couplers for facet-attached micro-lenses
+    inst_faml = FaML_two(cell, 
+            label = "opt_in_TE_1550_FaML_%s_loopback" % designer_name,
+            cell_name = 'FaML_Si_1550_BB',
+            cell_library = 'GSiP',
+             cell_params =  {'num_channels':1,
+                             'ref_wg':False},
+            )    
+    # loopback waveguide
+    connect_pins_with_waveguide(inst_faml[0], 'opt1', inst_faml[1], 'opt1', waveguide_type=waveguide_type1)
+
+    # Export for fabrication, removing PCells
+    path = os.path.dirname(os.path.realpath(__file__))
+    filename, extension = os.path.splitext(os.path.basename(__file__))
+    if export_type == 'static':
+        file_out = export_layout(cell, path, filename, relative_path = '..', format='oas', screenshot=True)
+    else:
+        file_out = os.path.join(path,'..',filename+'.oas')
+        ly.write(file_out)
+
+    # Verify
+    file_lyrdb = os.path.join(path,filename+'.lyrdb')
+    num_errors = layout_check(cell = cell, verbose=False, GUI=True, file_rdb=file_lyrdb)
+    print('Number of errors: %s' % num_errors)
+
+    # Display the layout in KLayout, using KLayout Package "klive", which needs to be installed in the KLayout Application
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, lyrdb_filename=file_lyrdb, technology=tech_name)
+    os.remove(file_out)
+
+    if num_errors > 0:
+        raise Exception ('Errors found in test_FaML_two')
+
+if __name__ == "__main__":
+    test_FaML_two()
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_SiEPIC_utils.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_SiEPIC_utils.py
new file mode 100644
index 000000000..7a78c9c60
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_SiEPIC_utils.py
@@ -0,0 +1,140 @@
+'''
+Unit testing for SiEPIC.utils.__init__
+
+by Lukas Chrostowski, 2024
+
+
+usage:
+ - run this script in KLayout Application, or in standalone Python
+'''
+
+from pya import *
+
+def test_load_layout():
+    import pya
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    # "path" is the folder where this script is located
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    # Create a new layout
+    topcell, ly = new_layout(tech_name, "UnitTesting", overwrite = True)
+
+    # Load layout from file
+    from SiEPIC.utils import load_layout
+    load_layout(ly, 
+                os.path.abspath(os.path.join(path, '../../gds/building_blocks')), 
+                'Germanium_Detector_Floating.GDS', 
+                single_topcell = True, Verbose = False)
+
+
+def test_create_cell2():
+    import pya
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    # "path" is the folder where this script is located
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    # Create a new layout
+    topcell, ly = new_layout(tech_name, "UnitTesting", overwrite = True)
+
+    # Load a cell from the library
+    from SiEPIC.utils import create_cell2
+    cell_y = create_cell2(ly, 'YBranch_te1550', 'GSiP', load_check=True)
+
+    t = Trans(Trans.R0,0,0)
+    topcell.insert(CellInstArray(cell_y.cell_index(), t))
+
+
+def test_waveguide_length():
+    import pya
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    # "path" is the folder where this script is located
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    # Create a new layout
+    topcell, ly = new_layout(tech_name, "UnitTesting", overwrite = True)
+
+    # Create waveguide PCell
+    cell_wg = ly.create_cell('Waveguide', tech_name, {'layers':['Waveguide','DevRec'], 'widths':[0.5,2], 'offsets':[0,0]})
+    print(cell_wg)
+    if not cell_wg:
+        raise Exception('Waveguide not loaded')
+    t = Trans(Trans.R0,0,0)
+    topcell.insert(CellInstArray(cell_wg.cell_index(), t))
+
+    # Save
+    filename = os.path.splitext(os.path.basename(__file__))[0]
+    file_out = export_layout(topcell, path, filename, format='oas', screenshot=True)
+
+    # Display in KLayout
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, technology=tech_name, keep_position=True)
+    os.remove(file_out)
+
+
+    from SiEPIC.utils import waveguide_length
+    wgl = waveguide_length(cell_wg)
+    print(wgl)
+
+if __name__ == "__main__":
+    test_load_layout()
+    test_create_cell2()
+    test_waveguide_length()
\ No newline at end of file
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_bezier.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_bezier.py
new file mode 100644
index 000000000..e40d34779
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_bezier.py
@@ -0,0 +1,235 @@
+'''
+
+Bezier curve test structures
+
+by Lukas Chrostowski, 2024
+
+Example simple script to
+ - create a new layout with a top cell
+ - create Bezier S-Bend, 90º Bend, taper
+ - to do: U-turn
+
+usage:
+ - run this script in KLayout Application, or in standalone Python
+'''
+
+from pya import *
+
+def test_bezier_bends():
+    designer_name = 'Test_Bezier_bend'
+    top_cell_name = 'GSiP_%s' % designer_name
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    '''
+    Create a new layout
+    '''    
+    cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
+    dbu = ly.dbu
+    import numpy as np
+
+    from SiEPIC.utils.geometry import bezier_parallel, bezier_cubic
+    from SiEPIC.utils import translate_from_normal
+    from SiEPIC.utils import arc_xy, arc_bezier
+
+    accuracy = 0.005
+
+    w = 10
+    h = 1
+    width = 0.5
+    layer = ly.layer(ly.TECHNOLOGY['Waveguide'])
+    layer2 = ly.layer(ly.TECHNOLOGY['FloorPlan'])
+    print(' Layers: %s, %s' % (ly.TECHNOLOGY['Waveguide'], ly.TECHNOLOGY['FloorPlan']))
+
+
+    # Two S-bends, small ∆h
+    wg_Dpts = bezier_parallel(pya.DPoint(0, 0), pya.DPoint(w, h), 0)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer2).insert(wg_polygon)
+    a = 0.45
+    wg_Dpts = bezier_cubic(pya.DPoint(0, 0), pya.DPoint(w, h), 0, 0, a, a, accuracy = accuracy)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer).insert(wg_polygon)
+
+    # Two S-bends, large ∆h
+    h = 9
+    wg_Dpts = bezier_parallel(pya.DPoint(0, 0), pya.DPoint(w, h), 0)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer2).insert(wg_polygon)
+    a = 0.75
+    wg_Dpts = bezier_cubic(pya.DPoint(0, 0), pya.DPoint(w, h), 0, 0, a, a, accuracy = accuracy)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer).insert(wg_polygon)
+
+    # 90º bend
+    r = 5
+    bezier = 0.2
+    a = (1-bezier) # /np.sqrt(2)
+    wg_Dpts = bezier_cubic(pya.DPoint(0, 0), pya.DPoint(r, r), 0, 90/180*np.pi, a, a, accuracy = accuracy, verbose=True)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer).insert(wg_polygon)
+    wg_pts = arc_bezier(r/dbu, 0, 90, float(bezier))
+    wg_Dpts = [p.to_dtype(dbu) for p in wg_pts]
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             ).transformed(pya.Trans(r,0))
+    cell.shapes(layer2).insert(wg_polygon)
+
+    # U-turn bend
+    r = 5
+    h = 1.6 *r 
+    a = 2 / (h/r)
+    wg_Dpts = bezier_cubic(pya.DPoint(0, 0), pya.DPoint(0, h), 0, 180/180*np.pi, a, a, accuracy = accuracy, verbose=True)
+    wg_polygon = pya.DPolygon(translate_from_normal(wg_Dpts, width/2) +
+                             translate_from_normal(wg_Dpts, -width/2)[::-1]
+                             )
+    cell.shapes(layer).insert(wg_polygon)
+
+
+    # Save
+    filename = os.path.splitext(os.path.basename(__file__))[0]
+    file_out = export_layout(cell, path, filename+top_cell_name, format='oas', screenshot=True)
+
+    # Display in KLayout
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, technology=tech_name, keep_position=True)
+    os.remove(file_out)
+    # Plot 
+    # cell.plot() # in the browser
+
+
+def test_bezier_tapers():
+    designer_name = 'Test_Bezier_tapers'
+    top_cell_name = 'GSiP_%s' % designer_name
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    '''
+    Create a new layout
+    '''    
+    cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
+    dbu = ly.dbu
+    import numpy as np
+
+    from SiEPIC.utils.geometry import bezier_parallel, bezier_cubic
+    from SiEPIC.utils import translate_from_normal
+    from SiEPIC.utils import arc_xy, arc_bezier
+
+    accuracy = 0.005
+    layer = ly.layer(ly.TECHNOLOGY['Waveguide'])
+
+    # Bezier Taper
+    taper_length = 20
+    width0 = 0.5
+    width1 = 3.0
+    # a = 0.4 # bezier parameter
+    # b = a
+    # wg_Dpts =   bezier_cubic(pya.DPoint(0, width0/2), pya.DPoint(taper_length, width1/2), 0, 0, a, b, accuracy = accuracy) + \
+    #             bezier_cubic(pya.DPoint(0, -width0/2), pya.DPoint(taper_length, -width1/2), 0, 0, a, b, accuracy = accuracy)[::-1]
+    # wg_polygon = pya.DPolygon( wg_Dpts )
+    # cell.shapes(layer).insert(wg_polygon)
+    # a = 0.5 # bezier parameter
+    # b = a
+    # wg_Dpts =   bezier_cubic(pya.DPoint(0, width0/2), pya.DPoint(taper_length, width1/2), 0, 0, a, b, accuracy = accuracy) + \
+    #             bezier_cubic(pya.DPoint(0, -width0/2), pya.DPoint(taper_length, -width1/2), 0, 0, a, b, accuracy = accuracy)[::-1]
+    # wg_polygon = pya.DPolygon( wg_Dpts )
+    # cell.shapes(layer).insert(wg_polygon)
+    # a = 0.6 # bezier parameter
+    # b = a
+    # wg_Dpts =   bezier_cubic(pya.DPoint(0, width0/2), pya.DPoint(taper_length, width1/2), 0, 0, a, b, accuracy = accuracy) + \
+    #             bezier_cubic(pya.DPoint(0, -width0/2), pya.DPoint(taper_length, -width1/2), 0, 0, a, b, accuracy = accuracy)[::-1]
+    # wg_polygon = pya.DPolygon( wg_Dpts )
+    # cell.shapes(layer).insert(wg_polygon)
+
+    # matching a Sinusoidal taper, per Sean Lam in EBeam_Beta
+    a = 0.37 # bezier parameter
+    b = 0.37 # 0.385
+    taper_length = 20
+    wg_Dpts =   bezier_cubic(pya.DPoint(0, width0/2), pya.DPoint(taper_length, width1/2), 0, 0, a, b, accuracy = accuracy) + \
+                bezier_cubic(pya.DPoint(0, -width0/2), pya.DPoint(taper_length, -width1/2), 0, 0, a, b, accuracy = accuracy)[::-1]
+    wg_polygon = pya.DPolygon( wg_Dpts )
+    cell.shapes(layer).insert(wg_polygon)
+
+    # a = 0.95 # bezier parameter
+    # b = 0.05 # bezier parameter
+    # wg_Dpts =   bezier_cubic(pya.DPoint(0, width0/2), pya.DPoint(taper_length, width1/2), 0, 0, a, b, accuracy = accuracy) + \
+    #             bezier_cubic(pya.DPoint(0, -width0/2), pya.DPoint(taper_length, -width1/2), 0, 0, a, b, accuracy = accuracy)[::-1]
+    # wg_polygon = pya.DPolygon( wg_Dpts )
+    # cell.shapes(layer).insert(wg_polygon)
+
+
+    # Save
+    filename = os.path.splitext(os.path.basename(__file__))[0]
+    file_out = export_layout(cell, path, filename+top_cell_name, format='oas', screenshot=True)
+
+    # Display in KLayout
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, technology=tech_name, keep_position=True)
+    os.remove(file_out)
+
+    # Plot 
+    # cell.plot() # in the browser
+
+
+if __name__ == "__main__":
+    # test_bezier_bends()
+    test_bezier_tapers()
+
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_coupler_array.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_coupler_array.py
new file mode 100644
index 000000000..c81cd6869
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_coupler_array.py
@@ -0,0 +1,113 @@
+"""
+Test for FaML
+
+by Lukas Chrostowski 2024
+
+"""
+
+def test_coupler_array():
+    '''
+    --- Simple MZI, tested using Facet-Attached Micro Lenses (FaML) ---
+    
+    by Lukas Chrostowski, 2024
+    
+    Example simple script to
+    - use the GSiP technology
+    - using KLayout and SiEPIC-Tools, with function including connect_pins_with_waveguide and connect_cell
+    - create a new layout with a top cell
+    - create a Mach-Zehnder Interferometer (MZI) circuits
+    - export to OASIS for submission to fabrication
+    - display the layout in KLayout using KLive
+    
+    Test plan
+    - count lenses from the bottom up (bottom is 1, top is 6, in this design)
+    - laser input on bottom lens (1), detector on second (2), for alignment
+    - MZI1: laser on 3, detector on 4, sweep
+    - MZI2: laser on 5, detector on 6, sweep
+    
+
+    Use instructions:
+
+    Run in Python, e.g., VSCode
+
+    pip install required packages:
+    - klayout, SiEPIC, siepic_ebeam_pdk, numpy
+
+    '''
+
+    designer_name = 'LukasChrostowski'
+    top_cell_name = 'EBeam_%s_MZI2_FaML' % designer_name
+    export_type = 'static'  # static: for fabrication, PCell: include PCells in file
+    #export_type = 'PCell'  # static: for fabrication, PCell: include PCells in file
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan, coupler_array
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+    
+    import os
+
+    if Python_Env == 'Script':
+        # For external Python mode, when installed using pip install siepic_ebeam_pdk
+        import GSiP
+
+    print('EBeam_LukasChrostowski_MZI2 layout script')
+    
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse("0.5.4"):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    '''
+    Create a new layout using the EBeam technology,
+    with a top cell
+    and Draw the floor plan
+    '''    
+    cell, ly = new_layout(tech_name, top_cell_name, GUI=True, overwrite = True)
+
+    waveguide_type1='Strip'
+
+    #######################
+    # Circuit #1 – Loopback
+    #######################
+    # draw two edge couplers for facet-attached micro-lenses
+    inst = coupler_array(cell, 
+            label = "opt_in_TE_1550_FaML_%s_loopback" % designer_name,
+            cell_name = 'Grating_Coupler_13deg_TE_1550_Oxide',
+            cell_library = 'GSiP',
+             cell_params =  {},
+             count=2,
+            )    
+    # loopback waveguide
+    connect_pins_with_waveguide(inst[0], 'opt_wg', inst[1], 'opt_wg', waveguide_type=waveguide_type1)
+
+    # Export for fabrication, removing PCells
+    path = os.path.dirname(os.path.realpath(__file__))
+    filename, extension = os.path.splitext(os.path.basename(__file__))
+    if export_type == 'static':
+        file_out = export_layout(cell, path, filename, relative_path = '..', format='oas', screenshot=True)
+    else:
+        file_out = os.path.join(path,'..',filename+'.oas')
+        ly.write(file_out)
+
+    # Verify
+    file_lyrdb = os.path.join(path,filename+'.lyrdb')
+    num_errors = layout_check(cell = cell, verbose=False, GUI=True, file_rdb=file_lyrdb)
+    print('Number of errors: %s' % num_errors)
+
+    # Display the layout in KLayout, using KLayout Package "klive", which needs to be installed in the KLayout Application
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, lyrdb_filename=file_lyrdb, technology=tech_name)
+    os.remove(file_out)
+
+    if num_errors > 0:
+        raise Exception ('Errors found in test_coupler_array')
+
+if __name__ == "__main__":
+    test_coupler_array()
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py
index a535065f4..29829dee8 100644
--- a/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_example_circuit.py
@@ -1,7 +1,7 @@
 '''
 --- Simple MZI ---
 
-by Lukas Chrostowski, 2020-2023
+by Lukas Chrostowski, 2020-2024
 
 Example simple script to
  - create a new layout with a top cell
@@ -11,7 +11,7 @@
 using SiEPIC-Tools function including connect_pins_with_waveguide and connect_cell
 
 usage:
- - run this script in KLayout Application
+ - run this script in KLayout Application, or in standalone Python
 '''
 
 from pya import *
@@ -34,7 +34,8 @@ def example_circuit():
 
     tech_name = 'GSiP'
 
-    if SiEPIC.__version__ < '0.5.4':
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
         raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
 
     if Python_Env == 'Script':
@@ -72,26 +73,64 @@ def example_circuit():
     # Y branches:
     instY1 = connect_cell(instGC1, 'opt_wg', cell_ebeam_y, 'opt1')
     instY1.transform(Trans(20000,0))
-    instY2 = connect_cell(instGC2, 'opt_wg', cell_ebeam_y, 'opt1')
+    # 'pin1' is not a pin on this cell, but rather 'opt1':
+    instY2 = connect_cell(instGC2, 'opt_wg', cell_ebeam_y, 'pin1', relaxed_pinnames=True)
     instY2.transform(Trans(20000,0))
 
+    # move the splitters into a subcell, to make an MZI sub-cell:
+    cell_MZI = ly.create_cell('MZI')
+    t = Trans(Trans.R0,0,0)
+    inst_MZI = cell.insert(CellInstArray(cell_MZI.cell_index(), t))
+    instY1.parent_cell = cell_MZI
+    instY2.parent_cell = cell_MZI
+
     # Waveguides:
-    connect_pins_with_waveguide(instGC1, 'opt_wg', instY1, 'opt1', waveguide_type=waveguide_type)
-    connect_pins_with_waveguide(instGC2, 'opt_wg', instY2, 'opt1', waveguide_type=waveguide_type)
-    connect_pins_with_waveguide(instY1, 'opt2', instY2, 'opt3', waveguide_type=waveguide_type)
+    connect_pins_with_waveguide(instGC1, 'opt_wg', instY1, 'opt1', waveguide_type=waveguide_type, verbose=True, error_min_bend_radius=False)
+    connect_pins_with_waveguide(instGC2, 'opt_wg', instY2, 'opt1', waveguide_type=waveguide_type, verbose=True, error_min_bend_radius=True)
+    connect_pins_with_waveguide(instY1, '2', instY2, '3', waveguide_type=waveguide_type, relaxed_pinnames=True)
     connect_pins_with_waveguide(instY1, 'opt3', instY2, 'opt2', waveguide_type=waveguide_type,turtle_B=[25,-90])
     
     # Zoom out
     zoom_out(cell)
 
+    # Delete extra top cells
+    from SiEPIC.scripts import delete_extra_topcells
+    delete_extra_topcells(ly, cell)
+
+    # Find the automated measurement coordinates:
+    from SiEPIC.utils import find_automated_measurement_labels
+    text_out, opt_in = find_automated_measurement_labels(cell)
+    print(opt_in)
+
+    # calculate areas
+    from SiEPIC.scripts import calculate_area
+    text = calculate_area(ly,cell)
+    print (text)
+
     # Verify
-    num_errors = layout_check(cell=cell, verbose=True, GUI=True)
+    num_errors = layout_check(cell=cell, verbose=False, GUI=True)
     print('Number of errors: %s' % num_errors)
 
+    # Netlist
+    from SiEPIC.netlist import export_spice
+    filename_netlist, filename_subckt = export_spice(cell, opt_in_selection_text=["opt_in_TE_1550_device_%s_MZI1" % designer_name])
+    print(filename_netlist)
+
     # Save
     filename = os.path.splitext(os.path.basename(__file__))[0]
     file_out = export_layout(cell, path, filename, format='oas', screenshot=True)
 
+    # Display in KLayout
+    from SiEPIC._globals import Python_Env
+    if Python_Env == 'Script':
+        from SiEPIC.utils import klive
+        klive.show(file_out, technology=tech_name, keep_position=True)
+    os.remove(file_out)
+
+    # Plot 
+    cell.plot() # in the browser
+
+
     return num_errors
 
 def test_example_circuit():
diff --git a/klayout_dot_config/tech/GSiP/pymacros/tests/test_instantiate_all_cells.py b/klayout_dot_config/tech/GSiP/pymacros/tests/test_instantiate_all_cells.py
new file mode 100644
index 000000000..76aaaf9a6
--- /dev/null
+++ b/klayout_dot_config/tech/GSiP/pymacros/tests/test_instantiate_all_cells.py
@@ -0,0 +1,67 @@
+'''
+Instiate all cells in the library
+
+by Lukas Chrostowski, 2024
+
+
+usage:
+ - run this script in KLayout Application, or in standalone Python
+'''
+
+from pya import *
+
+def test_all_library_cells():
+    designer_name = 'Test'
+    top_cell_name = 'GSiP_%s' % designer_name
+
+    import pya
+
+    import SiEPIC
+    from SiEPIC._globals import Python_Env
+    from SiEPIC.scripts import connect_cell, connect_pins_with_waveguide, zoom_out, export_layout
+    from SiEPIC.utils.layout import new_layout, floorplan
+    from SiEPIC.extend import to_itype
+    from SiEPIC.verification import layout_check
+
+    import os
+    path = os.path.dirname(os.path.realpath(__file__))
+
+    tech_name = 'GSiP'
+
+    from packaging import version
+    if version.parse(SiEPIC.__version__) < version.parse('0.5.4'):
+        raise Exception("Errors", "This example requires SiEPIC-Tools version 0.5.4 or greater.")
+
+    if Python_Env == 'Script':
+        # Load the PDK from a folder, e.g, GitHub, when running externally from the KLayout Application
+        import sys
+        sys.path.insert(0,os.path.abspath(os.path.join(path, '../../..')))
+        import GSiP
+
+    # Create a new layout
+    topcell, ly = new_layout(tech_name, "UnitTesting", overwrite = True)
+
+    # Instantiate all cells
+    from SiEPIC.scripts import instantiate_all_library_cells
+    instantiate_all_library_cells(topcell, terminator_cells = ['Terminator_TE_1550'], terminator_libraries = ['GSiP'], terminator_waveguide_types = ['Strip'])
+
+    # Check if there are any errors
+    for cell_id in topcell.called_cells():
+        c = ly.cell(cell_id)
+        error_shapes = c.shapes(ly.error_layer())
+        for error in error_shapes.each():
+            raise Exception('Error in cell: %s, %s' % (c.name, error.text))
+        if c.is_empty() or c.bbox().area() == 0:
+            raise Exception('Empty cell: %s' % c.name)
+
+    topcell.show()
+
+    # Verify
+    num_errors = layout_check(cell=topcell, verify_DFT=False, verbose=False, GUI=True)
+    if num_errors:
+        raise Exception('Number of errors: %s' % num_errors)
+    print('Number of errors: %s' % num_errors)
+
+
+if __name__ == "__main__":
+    test_all_library_cells()
diff --git a/run_pytest b/run_pytest
new file mode 100755
index 000000000..7080c8a91
--- /dev/null
+++ b/run_pytest
@@ -0,0 +1,6 @@
+pytest --cov=klayout_dot_config/python/SiEPIC \
+	--ignore=klayout_dot_config/python/SiEPIC/lumerical \
+	--ignore=klayout_dot_config/python/SiEPIC/tidy3d \
+	klayout_dot_config \
+	--cov-report=xml
+