├── docs
├── index.md
├── changelog.md
├── LXT_Icon.png
├── LXT_Logo.png
├── api.rst
├── _toc.yml
├── _config.yml
├── cells.rst
└── notebooks
│ ├── models.ipynb
│ └── circuit_layout.ipynb
├── .gitattributes
├── LXT_Logo.png
├── tests
├── gds_ref
│ ├── L_turn_bend.gds
│ ├── S_bend_vert.gds
│ ├── chip_frame.gds
│ ├── trail_cpw.gds
│ ├── bend_S_spline.gds
│ ├── CPW_pad_linear.gds
│ ├── U_bend_racetrack.gds
│ ├── eo_phase_shifter.gds
│ ├── heater_resistor.gds
│ ├── mzm_unbalanced.gds
│ ├── straight_rwg1000.gds
│ ├── straight_rwg3000.gds
│ ├── uni_cpw_straight.gds
│ ├── mmi1x2_optimized1550.gds
│ ├── mmi2x2_optimized1550.gds
│ ├── mmi2x2optimized1550.gds
│ ├── heater_straight_single.gds
│ ├── mzm_unbalanced_high_speed.gds
│ ├── bend_S_spline_varying_width.gds
│ ├── directional_coupler_balanced.gds
│ ├── double_linear_inverse_taper.gds
│ └── eo_phase_shifter_high_speed.gds
├── conftest.py
├── test_components
│ ├── test_settings_heater_resistor_.yml
│ ├── test_settings_chip_frame_.yml
│ ├── test_settings_S_bend_vert_.yml
│ ├── test_settings_CPW_pad_linear_.yml
│ ├── test_settings_bend_S_spline_varying_width_.yml
│ ├── test_settings_straight_rwg1000_.yml
│ ├── test_settings_straight_rwg3000_.yml
│ ├── test_settings_bend_S_spline_.yml
│ ├── test_settings_mmi2x2_optimized1550_.yml
│ ├── test_settings_mmi2x2optimized1550_.yml
│ ├── test_settings_mmi1x2_optimized1550_.yml
│ ├── test_settings_uni_cpw_straight_.yml
│ ├── test_settings_heater_straight_single_.yml
│ ├── test_settings_directional_coupler_balanced_.yml
│ ├── test_settings_eo_phase_shifter_.yml
│ ├── test_settings_eo_phase_shifter_high_speed_.yml
│ ├── test_settings_trail_cpw_.yml
│ ├── test_settings_double_linear_inverse_taper_.yml
│ ├── test_settings_L_turn_bend_.yml
│ ├── test_settings_U_bend_racetrack_.yml
│ ├── test_settings_mzm_unbalanced_.yml
│ └── test_settings_mzm_unbalanced_high_speed_.yml
└── test_components.py
├── .github
├── dependabot.yml
├── workflows
│ ├── release_drafter.yml
│ ├── test_code.yml
│ └── pages.yml
├── release-drafter.yml
└── write_cells_docs.py
├── Makefile
├── lnoi400
├── data
│ ├── ubend_racetrack.json
│ ├── edge_coupler_double_linear_taper.json
│ ├── mmi_1x2_optimized_1550.json
│ ├── mmi_2x2_optimized_1550.json
│ └── directional_coupler_balanced.json
├── config.py
├── layers.yaml
├── klayout
│ ├── d25
│ │ └── LNOI400.lyd25
│ ├── lnoi400.lyp
│ └── tech.lyt
├── __init__.py
├── spline.py
├── tech.py
├── models.py
└── cells.py
├── LICENSE
├── CHANGELOG.md
├── install_tech.py
├── README.md
├── .pre-commit-config.yaml
├── pyproject.toml
└── .gitignore
/docs/index.md:
--------------------------------------------------------------------------------
1 | ```{include} ../README.md
2 | ```
3 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ```{include} ../CHANGELOG.md
2 | ```
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/LXT_Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/LXT_Logo.png
--------------------------------------------------------------------------------
/docs/LXT_Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/docs/LXT_Icon.png
--------------------------------------------------------------------------------
/docs/LXT_Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/docs/LXT_Logo.png
--------------------------------------------------------------------------------
/tests/gds_ref/L_turn_bend.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/L_turn_bend.gds
--------------------------------------------------------------------------------
/tests/gds_ref/S_bend_vert.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/S_bend_vert.gds
--------------------------------------------------------------------------------
/tests/gds_ref/chip_frame.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/chip_frame.gds
--------------------------------------------------------------------------------
/tests/gds_ref/trail_cpw.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/trail_cpw.gds
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def pytest_configure():
5 | os.environ["JUPYTER_PLATFORM_DIRS"] = "1"
6 |
--------------------------------------------------------------------------------
/tests/gds_ref/bend_S_spline.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/bend_S_spline.gds
--------------------------------------------------------------------------------
/tests/gds_ref/CPW_pad_linear.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/CPW_pad_linear.gds
--------------------------------------------------------------------------------
/tests/gds_ref/U_bend_racetrack.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/U_bend_racetrack.gds
--------------------------------------------------------------------------------
/tests/gds_ref/eo_phase_shifter.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/eo_phase_shifter.gds
--------------------------------------------------------------------------------
/tests/gds_ref/heater_resistor.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/heater_resistor.gds
--------------------------------------------------------------------------------
/tests/gds_ref/mzm_unbalanced.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/mzm_unbalanced.gds
--------------------------------------------------------------------------------
/tests/gds_ref/straight_rwg1000.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/straight_rwg1000.gds
--------------------------------------------------------------------------------
/tests/gds_ref/straight_rwg3000.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/straight_rwg3000.gds
--------------------------------------------------------------------------------
/tests/gds_ref/uni_cpw_straight.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/uni_cpw_straight.gds
--------------------------------------------------------------------------------
/tests/gds_ref/mmi1x2_optimized1550.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/mmi1x2_optimized1550.gds
--------------------------------------------------------------------------------
/tests/gds_ref/mmi2x2_optimized1550.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/mmi2x2_optimized1550.gds
--------------------------------------------------------------------------------
/tests/gds_ref/mmi2x2optimized1550.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/mmi2x2optimized1550.gds
--------------------------------------------------------------------------------
/tests/gds_ref/heater_straight_single.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/heater_straight_single.gds
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | lnoi400
2 | ===================================
3 |
4 | Config
5 | ---------------------
6 |
7 | .. automodule:: lnoi400.config
8 |
--------------------------------------------------------------------------------
/tests/gds_ref/mzm_unbalanced_high_speed.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/mzm_unbalanced_high_speed.gds
--------------------------------------------------------------------------------
/tests/gds_ref/bend_S_spline_varying_width.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/bend_S_spline_varying_width.gds
--------------------------------------------------------------------------------
/tests/gds_ref/directional_coupler_balanced.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/directional_coupler_balanced.gds
--------------------------------------------------------------------------------
/tests/gds_ref/double_linear_inverse_taper.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/double_linear_inverse_taper.gds
--------------------------------------------------------------------------------
/tests/gds_ref/eo_phase_shifter_high_speed.gds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luxtelligence/lxt_pdk_gf/HEAD/tests/gds_ref/eo_phase_shifter_high_speed.gds
--------------------------------------------------------------------------------
/tests/test_components/test_settings_heater_resistor_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | length: 150
3 | name: heater_resistor_PNone_W0p9_O0
4 | settings:
5 | offset: 0
6 | width: 0.9
7 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_chip_frame_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: chip_frame_S10000_5000_EZW50_CNone
3 | settings:
4 | exclusion_zone_width: 50
5 | size:
6 | - 10000
7 | - 5000
8 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_S_bend_vert_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: S_bend_vert_VO25_HE100_DS5_CSxs_rwg1000
3 | settings:
4 | cross_section: xs_rwg1000
5 | dx_straight: 5
6 | h_extent: 100
7 | v_offset: 25
8 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_CPW_pad_linear_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: CPW_pad_linear_SW80_LS10_LT190_CSxs_uni_cpw
3 | settings:
4 | cross_section: xs_uni_cpw
5 | length_straight: 10
6 | length_tapered: 190
7 | start_width: 80
8 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_bend_S_spline_varying_width_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | length: 60.493
3 | name: bend_S_spline_varying_width_S58_14p5_CSNone_CSNone_N201_310bbd98
4 | settings:
5 | npoints: 201
6 | path_method: spline_null_curvature
7 | size:
8 | - 58
9 | - 14.5
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_straight_rwg1000_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | length: 10
3 | route_info_length: 10
4 | route_info_type: xs_rwg1000
5 | route_info_weight: 10
6 | route_info_xs_rwg1000_length: 10
7 | width: 1
8 | name: straight_rwg1000_L10
9 | settings:
10 | length: 10
11 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_straight_rwg3000_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | length: 10
3 | route_info_length: 10
4 | route_info_type: xs_rwg3000
5 | route_info_weight: 10
6 | route_info_xs_rwg3000_length: 10
7 | width: 3
8 | name: straight_rwg3000_L10
9 | settings:
10 | length: 10
11 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_bend_S_spline_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | length: 105.208
3 | name: bend_S_spline_S100_30_CSxs_rwg1000_N201_PMspline_clamped_path
4 | settings:
5 | cross_section: xs_rwg1000
6 | npoints: 201
7 | path_method: spline_clamped_path
8 | size:
9 | - 100
10 | - 30
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "/" # Location of package manifests
5 | schedule:
6 | interval: "daily"
7 |
8 | - package-ecosystem: github-actions
9 | directory: /
10 | schedule:
11 | interval: monthly
12 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_mmi2x2_optimized1550_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: mmi2x2optimized1550_WM5_LM76p5_WT1p5_LT25_PR0p7_CSxs_rwg1000
3 | settings:
4 | cross_section: xs_rwg1000
5 | length_mmi: 76.5
6 | length_taper: 25
7 | port_ratio: 0.7
8 | width_mmi: 5
9 | width_taper: 1.5
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_mmi2x2optimized1550_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: mmi2x2optimized1550_WM5_LM76p5_WT1p5_LT25_PR0p7_CSxs_rwg1000
3 | settings:
4 | cross_section: xs_rwg1000
5 | length_mmi: 76.5
6 | length_taper: 25
7 | port_ratio: 0.7
8 | width_mmi: 5
9 | width_taper: 1.5
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_mmi1x2_optimized1550_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: mmi1x2_optimized1550_WM6_LM26p75_WT1p5_LT25_PR0p55_CSxs_rwg1000
3 | settings:
4 | cross_section: xs_rwg1000
5 | length_mmi: 26.75
6 | length_taper: 25
7 | port_ratio: 0.55
8 | width_mmi: 6
9 | width_taper: 1.5
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_uni_cpw_straight_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: uni_cpw_straight_L1000_CSxs_uni_cpw_SW10_GW4_GPW250_BCP_ab5142b1
3 | settings:
4 | bondpad: CPW_pad_linear
5 | cross_section: xs_uni_cpw
6 | gap_width: 4
7 | ground_planes_width: 250
8 | length: 1000
9 | signal_width: 10
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_heater_straight_single_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: heater_straight_single_L150_W0p9_O0_PCWR3_PS100_100_PPNone_PVO10
3 | settings:
4 | length: 150
5 | offset: 0
6 | pad_size:
7 | - 100
8 | - 100
9 | pad_vert_offset: 10
10 | port_contact_width_ratio: 3
11 | width: 0.9
12 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_directional_coupler_balanced_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: directional_coupler_balanced_IWS30p6_SL58_CSL16p92_CWS0_e88e65ac
3 | settings:
4 | central_straight_length: 16.92
5 | coup_wg_width: 0.8
6 | coupl_wg_sep: 0.8
7 | cross_section_io: xs_rwg1000
8 | io_wg_sep: 30.6
9 | sbend_length: 58
10 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_eo_phase_shifter_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: eo_phase_shifter_RCWM2p5_TL100_ML7500_RCCW10_RGPW180_RG_617881fc
3 | settings:
4 | cpw_cell: uni_cpw_straight
5 | draw_cpw: true
6 | modulation_length: 7500
7 | rf_central_conductor_width: 10
8 | rf_gap: 4
9 | rf_ground_planes_width: 180
10 | rib_core_width_modulator: 2.5
11 | taper_length: 100
12 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_eo_phase_shifter_high_speed_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | additional_settings:
3 | cpw_cell: trail_cpw
4 | draw_cpw: true
5 | modulation_length: 7500
6 | rf_central_conductor_width: 21
7 | rf_gap: 4
8 | rf_ground_planes_width: 180
9 | rib_core_width_modulator: 2.5
10 | taper_length: 100
11 | name: eo_phase_shifter_high_speed
12 | settings: {}
13 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_trail_cpw_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: trail_cpw_L1000_SW21_GW4_T1p5_T44p7_T7_T1p5_T5_GPW180_R_121f14d9
3 | settings:
4 | bondpad: CPW_pad_linear
5 | cross_section: xs_uni_cpw
6 | gap_width: 4
7 | ground_planes_width: 180
8 | length: 1000
9 | rounding_radius: 0.5
10 | signal_width: 21
11 | tc: 5
12 | th: 1.5
13 | tl: 44.7
14 | tt: 1.5
15 | tw: 7
16 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_double_linear_inverse_taper_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: double_linear_inverse_taper_CSSxs_swg250_CSExs_rwg1000__e181987f
3 | settings:
4 | cross_section_end: xs_rwg1000
5 | cross_section_start: xs_swg250
6 | input_ext: 0
7 | lower_taper_end_width: 2.05
8 | lower_taper_length: 120
9 | slab_removal_width: 20
10 | upper_taper_length: 240
11 | upper_taper_start_width: 0.25
12 |
--------------------------------------------------------------------------------
/docs/_toc.yml:
--------------------------------------------------------------------------------
1 | # Table of contents
2 | # Learn more at https://jupyterbook.org/customize/toc.html
3 |
4 | format: jb-book
5 | root: index
6 | parts:
7 | # - caption: Tutorial
8 | # chapters:
9 | # - file: tutorial
10 | # sections:
11 | # - file: notebooks/demo
12 | - caption: Reference
13 | chapters:
14 | - file: cells
15 | - file: notebooks/circuit_layout.ipynb
16 | - file: notebooks/models.ipynb
17 | - file: changelog
18 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | pip install -e .[dev]
3 | pre-commit install
4 |
5 | dev:
6 | pip install -e .[dev,docs]
7 |
8 | test:
9 | pytest -s
10 |
11 | update-pre:
12 | pre-commit autoupdate --bleeding-edge
13 |
14 | git-rm-merged:
15 | git branch -D `git branch --merged | grep -v \* | xargs`
16 |
17 | build:
18 | rm -rf dist
19 | pip install build
20 | python -m build
21 |
22 | docs:
23 | python .github/write_cells_docs.py
24 | jb build docs
25 |
26 | .PHONY: drc doc docs
27 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_L_turn_bend_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | dy: 80
3 | length: 134.393
4 | min_bend_radius: 42.779
5 | radius: 80
6 | route_info_length: 134.393
7 | route_info_min_bend_radius: 42.779
8 | route_info_n_bend_90: 1
9 | route_info_type: xs_rwg1000
10 | route_info_weight: 134.393
11 | route_info_xs_rwg1000_length: 134.393
12 | width: 1
13 | name: L_turn_bend_R80_P1_WAFTrue_CSxs_rwg1000
14 | settings:
15 | cross_section: xs_rwg1000
16 | p: 1
17 | radius: 80
18 | with_arc_floorplan: true
19 |
--------------------------------------------------------------------------------
/lnoi400/data/ubend_racetrack.json:
--------------------------------------------------------------------------------
1 | {
2 | "doc": "FDTD simulation results of an Euler U-bend with d = 90.0. Parameters of a polynomial fit around the center wavelength.",
3 | "center_wavelength": 1.55,
4 | "pol_trans_abs": [
5 | 0.20293380908146152,
6 | 0.044257488576826404,
7 | -0.03142418048071505,
8 | -0.008804432384094858,
9 | 0.9993005019936548
10 | ],
11 | "pol_trans_phase": [
12 | 410.9488768318485,
13 | -560.8693164741372,
14 | 838.8503144233531,
15 | 870.0080469460073,
16 | 110.48703606742491
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_U_bend_racetrack_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | dy: 0
3 | length: 205.358
4 | min_bend_radius: 32.684
5 | radius: 45
6 | route_info_length: 205.358
7 | route_info_min_bend_radius: 32.684
8 | route_info_n_bend_90: 2
9 | route_info_type: xs_rwg3000
10 | route_info_weight: 205.358
11 | route_info_xs_rwg3000_length: 205.358
12 | width: 3
13 | name: U_bend_racetrack_VO90_P1_WAFTrue_CSxs_rwg3000
14 | settings:
15 | cross_section: xs_rwg3000
16 | p: 1
17 | v_offset: 90
18 | with_arc_floorplan: true
19 |
--------------------------------------------------------------------------------
/lnoi400/config.py:
--------------------------------------------------------------------------------
1 | __all__ = ["PATH"]
2 |
3 | import pathlib
4 |
5 | cwd = pathlib.Path.cwd()
6 | cwd_config = cwd / "config.yml"
7 | module = pathlib.Path(__file__).parent.absolute()
8 | repo = module.parent
9 |
10 |
11 | class Path:
12 | module = module
13 | repo = repo
14 | gds = module / "gds"
15 | klayout = module / "klayout"
16 |
17 | lyp = klayout / "tech" / "lnoi400.lyp"
18 | lyt = klayout / "tech" / "tech.lyt"
19 | lyp_yaml = module / "layers.yaml"
20 | tech = module / "klayout" / "tech"
21 |
22 |
23 | PATH = Path()
24 |
--------------------------------------------------------------------------------
/lnoi400/layers.yaml:
--------------------------------------------------------------------------------
1 | LayerViews:
2 | LN_RIDGE:
3 | layer: [2, 0]
4 | color: "#ca84f5"
5 | width: 1
6 | LN_SLAB:
7 | layer: [3, 0]
8 | color: "#875dd4"
9 | SLAB_NEGATIVE:
10 | layer: [3, 1]
11 | color: "#4c209e"
12 | LABELS:
13 | layer: [4, 0]
14 | color: "#5179b5"
15 | CHIP_CONTOUR:
16 | layer: [6, 0]
17 | color: "#ffc6b8"
18 | CHIP_EXCLUSION_ZONE:
19 | layer: [6, 1]
20 | color: "#00fe9c"
21 | TL:
22 | layer: [21, 0]
23 | color: "#8dc2f7"
24 | HT:
25 | layer: [21, 1]
26 | color: "#8dc2f7"
27 | hatch_pattern: coarsely dotted
28 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_mzm_unbalanced_.yml:
--------------------------------------------------------------------------------
1 | info: {}
2 | name: mzm_unbalanced_ML7500_LI100_LTAR75_RPSW80_RCCW10_RGPW18_0882c4f8
3 | settings:
4 | bias_tuning_section_length: 700
5 | cpw_cell: uni_cpw_straight
6 | heater_offset: 1.2
7 | heater_pad_size:
8 | - 75
9 | - 75
10 | heater_width: 1
11 | lbend_tune_arm_reff: 75
12 | length_imbalance: 100
13 | modulation_length: 7500
14 | rf_central_conductor_width: 10
15 | rf_gap: 4
16 | rf_ground_planes_width: 180
17 | rf_pad_length_straight: 10
18 | rf_pad_length_tapered: 300
19 | rf_pad_start_width: 80
20 | with_heater: false
21 |
--------------------------------------------------------------------------------
/tests/test_components/test_settings_mzm_unbalanced_high_speed_.yml:
--------------------------------------------------------------------------------
1 | info:
2 | additional_settings:
3 | bias_tuning_section_length: 700
4 | cpw_cell: trail_cpw
5 | heater_offset: 1.2
6 | heater_pad_size:
7 | - 75
8 | - 75
9 | heater_width: 1
10 | lbend_tune_arm_reff: 75
11 | length_imbalance: 100
12 | modulation_length: 7500
13 | rf_central_conductor_width: 21
14 | rf_gap: 4
15 | rf_ground_planes_width: 180
16 | rf_pad_length_straight: 10
17 | rf_pad_length_tapered: 300
18 | rf_pad_start_width: 80
19 | with_heater: false
20 | name: mzm_unbalanced_high_speed
21 | settings: {}
22 |
--------------------------------------------------------------------------------
/lnoi400/klayout/d25/LNOI400.lyd25:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | d25
6 |
7 |
8 |
9 | false
10 | false
11 | 0
12 |
13 | true
14 | d25_scripts
15 | tools_menu.d25.end
16 | dsl
17 | d25-dsl-xml
18 |
19 |
20 | slab = input(3, 0)
21 | ridge = input(2, 0)
22 | tl = input(21, 0)
23 | ht = input(21, 0)
24 |
25 |
26 |
27 | z(slab, zstart: 0.0, zstop: 0.2, name: 'slab: ln 3/0', )
28 | z(ridge, zstart: 0.2, zstop: 0.4, name: 'ridge: ln 2/0', )
29 | z(tl, zstart: 1.4, zstop: 2.3, name: 'tl: tl_metal 21/0', )
30 | z(ht, zstart: 1.4, zstop: 2.3, name: 'ht: tl_metal 21/0', )
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/lnoi400/data/edge_coupler_double_linear_taper.json:
--------------------------------------------------------------------------------
1 | {
2 | "doc": "FDTD simulation results of double-layer linear-linear edge coupler. Parameters of a polynomial fit around the center wavelength. The transmission and reflection are calculated with respect to the fundamental mode amplitude at the taper start.",
3 | "center_wavelength": 1.55,
4 | "pol_trans_abs": [
5 | -4.942469081113438,
6 | -1.1842306217518026,
7 | -0.17897835665607192,
8 | 0.07729299897695552,
9 | 0.9812198939816317
10 | ],
11 | "pol_trans_phase": [
12 | 667.647835700407,
13 | -1004.3084128839065,
14 | 1451.7976939382295,
15 | 113.69375168521131,
16 | -16.856582027159668
17 | ],
18 | "pol_refl_abs": [
19 | -0.3929244913129529,
20 | -0.02954138169696813,
21 | -0.0010202460489620237,
22 | -0.0007075034975085314,
23 | 0.009769294212456201
24 | ],
25 | "pol_refl_phase": [
26 | -78202.35268415872,
27 | -18229.854445217974,
28 | 219.3439106056484,
29 | 924.6478980535389,
30 | 107.9938160561272
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-2024 Luxtelligence
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/release_drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter and Labels
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [edited, opened, reopened, synchronize, unlabeled, labeled]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | update_release_draft:
15 | permissions:
16 | contents: write
17 | pull-requests: write
18 | runs-on: ubuntu-latest
19 | steps:
20 | # Drafts your next Release notes as Pull Requests are merged into "master"
21 | - uses: release-drafter/release-drafter@v6
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 | require_label:
25 | if: github.event.pull_request
26 | needs: update_release_draft
27 | runs-on: ubuntu-latest
28 | permissions:
29 | issues: write
30 | pull-requests: write
31 | steps:
32 | - uses: mheap/github-action-required-labels@v5
33 | with:
34 | mode: minimum
35 | count: 1
36 | labels: "breaking, bug, github_actions, documentation, dependencies, enhancement, feature, maintenance, security"
37 | add_comment: true
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The release naming convention follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4 |
5 | ## v1.3.0
6 |
7 | ### New
8 |
9 |
10 | - Added high-speed versions of mzm_unbalanced and eo_phase_shifter #107
11 | - Making project compatible with gdsfactoryplus #105
12 | - Support 2x2 MMI in MZM cell
13 |
14 | ### Bug fixes
15 |
16 | - Fix layerspec import
17 | - MZM caching bugfix
18 |
19 |
20 | ## v1.2.0
21 |
22 | ### New
23 |
24 | - Balanced directional coupler building block
25 | - Add wavelength dependence to phase shifter model
26 | - Add thermo-optical phase shifter cell
27 |
28 | ## v0.1.1
29 |
30 | ### Bug fixes
31 | - Fix typo for the heater layer in LayerStack
32 | - Fix optics layer names (ridge, slab)
33 | - Use center parameter in chip_frame
34 |
35 | ## v0.1.0
36 |
37 | ### New
38 | - Technology definition for lnoi400
39 | - Basic Component definitions: bends, edge coupler, mmis, uniform CPWs, phase and amplitude modulators
40 | - Circuit models based on [sax](https://github.com/flaport/sax)
41 | - Example notebooks for:
42 | - Layout with routing to the chip facets
43 | - Circuit simulation
44 |
--------------------------------------------------------------------------------
/lnoi400/data/mmi_1x2_optimized_1550.json:
--------------------------------------------------------------------------------
1 | {
2 | "doc": "FDTD simulation results of MMI1x2 optimized for transmission at 1550 nm. Parameters of a polynomial fit around the center wavelength.",
3 | "center_wavelength": 1.55,
4 | "pol_trans_abs": [13.818334434885296, 2.1023324380644817, -1.910178348586021, -0.05378794031031747, 0.6995352035610057],
5 | "pol_trans_phase": [139.76552232167927, -228.01120397917717, 347.32167119293695, -502.49319462985403, -86.03879775613684],
6 | "pol_refl_in_abs": [0.49070926690866096, 0.08171302240690632, 0.009950206637899795, 0.00904014488470984],
7 | "pol_refl_in_phase": [24911.37217400053, 1286.207105049023, -1045.3754869102559, -521.3905589401008, -60.7857085782603],
8 | "pol_refl_out_abs": [0.24578672735389936, -0.14323456225175157, -0.033708286995365135, 0.0329774209867788],
9 | "pol_refl_out_phase": [-129.30877551890666, -286.8073277469162, 462.55445774034405, -666.1843153945192, -111.3081869587087],
10 | "pol_refl_cross_abs": [0.7625767532937138, -0.25942526088440493, -0.014059704677509284, 0.040546918159599855],
11 | "pol_refl_cross_phase": [95.93068371448132, -275.01570876772894, 461.316583857209, -665.1311890960344, -108.13998322949305]
12 | }
13 |
--------------------------------------------------------------------------------
/install_tech.py:
--------------------------------------------------------------------------------
1 | """Symlink tech to klayout."""
2 | import os
3 | import pathlib
4 | import shutil
5 | import sys
6 |
7 |
8 | def remove_path_or_dir(dest: pathlib.Path):
9 | if dest.is_dir():
10 | os.unlink(dest)
11 | else:
12 | os.remove(dest)
13 |
14 |
15 | def make_link(src, dest, overwrite: bool = True) -> None:
16 | dest = pathlib.Path(dest)
17 | if dest.exists() and not overwrite:
18 | print(f"{dest} already exists")
19 | return
20 | if dest.exists() or dest.is_symlink():
21 | print(f"removing {dest} already installed")
22 | remove_path_or_dir(dest)
23 | try:
24 | os.symlink(src, dest, target_is_directory=True)
25 | except OSError:
26 | shutil.copy(src, dest)
27 | print("link made:")
28 | print(f"From: {src}")
29 | print(f"To: {dest}")
30 |
31 |
32 | if __name__ == "__main__":
33 | klayout_folder = "KLayout" if sys.platform == "win32" else ".klayout"
34 | cwd = pathlib.Path(__file__).resolve().parent
35 | home = pathlib.Path.home()
36 | src = cwd / "lnoi400" / "klayout"
37 | dest_folder = home / klayout_folder / "tech"
38 | dest_folder.mkdir(exist_ok=True, parents=True)
39 | dest = dest_folder / "lnoi400"
40 | make_link(src=src, dest=dest)
41 |
--------------------------------------------------------------------------------
/.github/workflows/test_code.yml:
--------------------------------------------------------------------------------
1 | name: Test pre-commit, code and docs
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | pre-commit:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Python
15 | uses: actions/setup-python@v5
16 | with:
17 | python-version: "3.12"
18 | cache: "pip"
19 | cache-dependency-path: pyproject.toml
20 | - name: Test pre-commit hooks
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install pre-commit
24 | pre-commit run -a
25 | test_code:
26 | runs-on: ${{ matrix.os }}
27 | strategy:
28 | max-parallel: 12
29 | matrix:
30 | python-version: ["3.12"]
31 | os: [ubuntu-latest]
32 | steps:
33 | - uses: actions/checkout@v4
34 | - name: Set up Python ${{ matrix.python-version }}
35 | uses: actions/setup-python@v5
36 | with:
37 | python-version: ${{ matrix.python-version }}
38 | cache: "pip"
39 | cache-dependency-path: pyproject.toml
40 | - name: Install dependencies
41 | run: |
42 | make dev
43 | - name: Test with pytest
44 | run: pytest
45 |
--------------------------------------------------------------------------------
/lnoi400/__init__.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | from gdsfactory.config import CONF
4 | from gdsfactory.cross_section import get_cross_sections
5 | from gdsfactory.get_factories import get_cells
6 | from gdsfactory.pdk import Pdk
7 |
8 | from lnoi400 import cells, config, tech
9 | from lnoi400.config import PATH
10 | from lnoi400.models import get_models
11 | from lnoi400.tech import LAYER, LAYER_STACK, LAYER_VIEWS
12 |
13 | _models = get_models()
14 | _cells = get_cells(cells)
15 | _cross_sections = get_cross_sections(tech)
16 |
17 | CONF.pdk = "lnoi400"
18 |
19 | _routing_strategies = dict(
20 | route_bundle_rwg1000=tech.route_bundle_rwg1000,
21 | )
22 |
23 |
24 | @lru_cache
25 | def get_pdk() -> Pdk:
26 | """Return LXT lnoi400 PDK."""
27 | return Pdk(
28 | name="lnoi400",
29 | cells=_cells,
30 | cross_sections=_cross_sections,
31 | layers=LAYER,
32 | layer_stack=LAYER_STACK,
33 | layer_views=LAYER_VIEWS,
34 | models=_models,
35 | routing_strategies=_routing_strategies,
36 | )
37 |
38 |
39 | def activate_pdk() -> None:
40 | pdk = get_pdk()
41 | pdk.activate()
42 |
43 |
44 | PDK = get_pdk()
45 |
46 | __all__ = [
47 | "LAYER",
48 | "LAYER_STACK",
49 | "LAYER_VIEWS",
50 | "PATH",
51 | "cells",
52 | "config",
53 | "tech",
54 | ]
55 | __version__ = "1.2.0"
56 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: build docs
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 | workflow_dispatch:
9 |
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | build-docs:
16 | runs-on: ubuntu-latest
17 | name: Sphinx docs to gh-pages
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: '3.12'
24 | cache: "pip"
25 | cache-dependency-path: pyproject.toml
26 | - name: Installing the library
27 | shell: bash -l {0}
28 | run: |
29 | make dev
30 | - name: make docs
31 | run: |
32 | make docs
33 | - name: Upload artifact
34 | uses: actions/upload-pages-artifact@v3
35 | with:
36 | path: "./docs/_build/html/"
37 | deploy-docs:
38 | needs: build-docs
39 | if: ${{ github.ref == 'refs/heads/main' }}
40 | permissions:
41 | pages: write
42 | id-token: write
43 |
44 | environment:
45 | name: github-pages
46 | url: ${{ steps.deployment.outputs.page_url }}
47 |
48 | runs-on: ubuntu-latest
49 | steps:
50 | - name: Deploy to GitHub Pages
51 | id: deployment
52 | uses: actions/deploy-pages@v4
53 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | change-template: '- $TITLE [#$NUMBER](https://github.com/Luxtelligence/lxt_pdk_gf/pull/$NUMBER)'
4 | template: |
5 | # What's Changed
6 | $CHANGES
7 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
8 | categories:
9 | - title: 'Breaking'
10 | label: 'breaking'
11 | - title: 'New'
12 | labels:
13 | - 'feature'
14 | - 'enhancement'
15 | - title: 'Bug Fixes'
16 | label: 'bug'
17 | - title: 'Maintenance'
18 | labels:
19 | - 'maintenance'
20 | - 'github_actions'
21 | - title: 'Documentation'
22 | label: 'documentation'
23 | - title: 'Other changes'
24 | - title: 'Dependency Updates'
25 | label: 'dependencies'
26 | collapse-after: 5
27 |
28 | version-resolver:
29 | major:
30 | labels:
31 | - 'breaking'
32 | - 'major'
33 | minor:
34 | labels:
35 | - 'feature'
36 | - 'minor'
37 | - 'enhancement'
38 | patch:
39 | labels:
40 | - 'bug'
41 | - 'maintenance'
42 | - 'github_actions'
43 | - 'documentation'
44 | - 'dependencies'
45 | - 'security'
46 | default: patch
47 |
48 | exclude-labels:
49 | - 'skip-changelog'
50 |
51 | autolabeler:
52 | - label: 'documentation'
53 | files:
54 | - '*.md'
55 | branch:
56 | - '/docs-.+/'
57 | - label: 'bug'
58 | branch:
59 | - '/fix-.+/'
60 | title:
61 | - '/fix/i'
62 | - label: 'enhancement'
63 | branch:
64 | - '/feature-.+/'
65 | - '/add-.+/'
66 | title:
67 | - '/^add\s/i'
68 |
--------------------------------------------------------------------------------
/lnoi400/data/mmi_2x2_optimized_1550.json:
--------------------------------------------------------------------------------
1 | {
2 | "doc": "FDTD simulation results of MMI2x2 optimized for transmission at 1550 nm. Parameters of a polynomial fit around the center wavelength.",
3 | "center_wavelength": 1.55,
4 | "pol_trans_bar_abs": [
5 | 35.620442496612924,
6 | -1.9683893445354477,
7 | -6.497661892319359,
8 | -0.08300290938564699,
9 | 0.700279285330558
10 | ],
11 | "pol_trans_bar_phase": [
12 | 232.3372658412472,
13 | -350.6700662288277,
14 | 550.899737437114,
15 | -800.910416528658,
16 | -135.84622464652068
17 | ],
18 | "pol_trans_cross_abs": [
19 | 60.90470647046822,
20 | 10.698445526578551,
21 | -6.862634742228134,
22 | -0.21069884062519814,
23 | 0.6976071480515665
24 | ],
25 | "pol_trans_cross_phase": [
26 | 222.95582756630876,
27 | -359.38196123854897,
28 | 550.1055585859954,
29 | -801.1073843998344,
30 | -131.1357234304027
31 | ],
32 | "pol_refl_bar_abs": [
33 | -277.1277903320858,
34 | -34.84218229593704,
35 | 25.048134615327616,
36 | 1.4125341832722962,
37 | -0.10042861157196822,
38 | -0.013677914528370612,
39 | 0.005929247166595322
40 | ],
41 | "pol_refl_bar_phase": [
42 | -28943.289326376,
43 | -2950.047245285256,
44 | 572.5906147591779,
45 | -329.3294107493721,
46 | -60.915793891574346
47 | ],
48 | "pol_refl_cross_abs": [
49 | -1732.7411777809464,
50 | -62.902536339907755,
51 | 69.33709004857391,
52 | 0.4278157971108136,
53 | -0.5020580303163353,
54 | 0.048508284937844905,
55 | 0.00695709456283943
56 | ],
57 | "pol_refl_cross_phase": [
58 | 2151.569475344031,
59 | -400.6756747108247,
60 | 763.1036896163519,
61 | -1238.205868251526,
62 | -203.18088225465507
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/tests/test_components.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | import pytest
4 | from gdsfactory.difftest import difftest
5 | from pytest_regressions.data_regression import DataRegressionFixture
6 |
7 | from lnoi400 import PDK
8 |
9 | cells = PDK.cells
10 |
11 | skip_test = {"import_gds"}
12 |
13 | component_names = set(cells.keys()) - set(skip_test)
14 | dirpath = pathlib.Path(__file__).absolute().parent / "gds_ref"
15 | dirpath.mkdir(exist_ok=True, parents=True)
16 |
17 | pcell_mapping = [
18 | ("cell", "cell"),
19 | ]
20 |
21 |
22 | @pytest.fixture(params=component_names, scope="function")
23 | def component_name(request) -> str:
24 | return request.param
25 |
26 |
27 | # @pytest.fixture(params=pcell_mapping, scope="function")
28 | # def name_mapping(request) -> str:
29 | # return request.param
30 |
31 |
32 | def test_gds(component_name: str) -> None:
33 | """Avoid regressions in GDS geometry shapes and layers."""
34 | component = cells[component_name]()
35 | difftest(
36 | component,
37 | test_name=component_name,
38 | dirpath=dirpath,
39 | ignore_sliver_differences=True,
40 | )
41 |
42 |
43 | # def test_alternative_implementation(
44 | # name_mapping: tuple,
45 | # ) -> None:
46 | # """Test against the cells distributed with a different PDK implementation."""
47 |
48 | # # TODO: Implement difftest with layers selection.
49 |
50 | # assert name_mapping[0] == name_mapping[1]
51 |
52 |
53 | def test_settings(
54 | component_name: str,
55 | data_regression: DataRegressionFixture,
56 | ) -> None:
57 | """Avoid regressions when exporting settings."""
58 | component = cells[component_name]()
59 | data_regression.check(component.to_dict())
60 |
61 |
62 | if __name__ == "__main__":
63 | print(component_names)
64 |
--------------------------------------------------------------------------------
/.github/write_cells_docs.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from lnoi400 import _cells as cells
4 | from lnoi400.config import PATH
5 |
6 | filepath = PATH.repo / "docs" / "cells.rst"
7 |
8 | skip = {}
9 |
10 | skip_plot: tuple[str, ...] = ("",)
11 | skip_settings: tuple[str, ...] = ()
12 |
13 |
14 | with open(filepath, "w+") as f:
15 | f.write(
16 | """
17 |
18 | Luxtelligence provides a library of components that have been fabricated in the reference material stack, and whose performance has been tested and validated. Here follows a list of the available parametric cells (gdsfactory.Component objects):
19 |
20 |
21 | Cells
22 | =============================
23 | """
24 | )
25 |
26 | for name in sorted(cells.keys()):
27 | if name in skip or name.startswith("_"):
28 | continue
29 | print(name)
30 | sig = inspect.signature(cells[name])
31 | kwargs = ", ".join(
32 | [
33 | f"{p}={repr(sig.parameters[p].default)}"
34 | for p in sig.parameters
35 | if isinstance(sig.parameters[p].default, int | float | str | tuple)
36 | and p not in skip_settings
37 | ]
38 | )
39 | if name in skip_plot:
40 | f.write(
41 | f"""
42 |
43 | {name}
44 | ----------------------------------------------------
45 |
46 | .. autofunction:: lnoi400.cells.{name}
47 |
48 | """
49 | )
50 | else:
51 | f.write(
52 | f"""
53 |
54 | {name}
55 | ----------------------------------------------------
56 |
57 | .. autofunction:: lnoi400.cells.{name}
58 |
59 | .. plot::
60 | :include-source:
61 |
62 | import lnoi400
63 |
64 | c = lnoi400.cells.{name}({kwargs})
65 | c.plot()
66 |
67 | """
68 | )
69 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Book settings
2 | # Learn more at https://jupyterbook.org/customize/config.html
3 |
4 | title: lnoi400
5 | author: Luxtelligence
6 | logo: LXT_Icon.png
7 |
8 | # Force re-execution of notebooks on each build.
9 | # See https://jupyterbook.org/content/execute.html
10 | execute:
11 | execute_notebooks: cache
12 | timeout: -1
13 | allow_errors: false
14 | # execute_notebooks: force
15 | # execute_notebooks: "off"
16 |
17 | latex:
18 | latex_engine: pdflatex # one of 'pdflatex', 'xelatex' (recommended for unicode), 'luatex', 'platex', 'uplatex'
19 | use_jupyterbook_latex: true # use sphinx-jupyterbook-latex for pdf builds as default
20 |
21 | # Add a bibtex file so that we can create citations
22 |
23 | html:
24 | home_page_in_navbar: true
25 | use_edit_page_button: true
26 | use_repository_button: true
27 | use_issues_button: true
28 | baseurl: https://github.com/Luxtelligence/lxt_pdk_gf
29 |
30 | # Information about where the book exists on the web
31 | repository:
32 | url: https://github.com/Luxtelligence/lxt_pdk_gf
33 | path_to_book: docs # Optional path to your book, relative to the repository root
34 | branch: main # Which branch of the repository should be used when creating links (optional)
35 |
36 | launch_buttons:
37 | notebook_interface: jupyterlab
38 | colab_url: "https://colab.research.google.com"
39 |
40 | sphinx:
41 | extra_extensions:
42 | - "sphinx.ext.autodoc"
43 | - "sphinx.ext.autodoc.typehints"
44 | - "sphinx.ext.autosummary"
45 | - "sphinx.ext.napoleon"
46 | - "sphinx.ext.viewcode"
47 | - "matplotlib.sphinxext.plot_directive"
48 | config:
49 | #autodoc_typehints: description
50 | autodoc_type_aliases:
51 | "ComponentSpec": "ComponentSpec"
52 | nb_execution_show_tb: True
53 | nb_execution_raise_on_error: true
54 | nb_custom_formats:
55 | .py:
56 | - jupytext.reads
57 | - fmt: py
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lnoi400
2 |
3 | 
4 |
5 | [Luxtelligence](https://luxtelligence.ai/) lnoi400 Process Design Kit (PDK). The Luxtelligence Process Design Kit contains a library of components that facilitate the design of photonic integrated circuits for Luxtelligence's Lithium Niobate on Insulator (LNOI) foundry service. The PDK includes both electrical and optical building blocks, that make use of Lithium Niobate's electro-optic effect and attractive optical properties. PDK building blocks consist both of a geometrical layout, defining the starting point for the microfabrication of the integrated circuit, and a circuit model, that approximates the real frequency-domain behaviour of the optical component.
6 |
7 | The lnoi400 PDK is released open-source, to let users easily evaluate a sample of the offering of Luxtelligence. Please [contact us](mailto:foundry@luxtelligence.ai) for information on advanced building blocks and variations on the standard PDK geometry.
8 |
9 | ## Installation
10 |
11 | Use python3.11 or python3.12. We recommend [VSCode](https://code.visualstudio.com/) as an IDE.
12 |
13 | If you don't have python installed on your system you can [download anaconda](https://www.anaconda.com/download/)
14 |
15 | Once you have python installed, open Anaconda Prompt as Administrator and then install the latest gdsfactory using pip.
16 |
17 | 
18 |
19 |
20 | ```
21 | git clone https://github.com/Luxtelligence/lxt_pdk_gf.git
22 | cd lxt_pdk_gf
23 | pip install -e . pre-commit
24 | pre-commit install
25 | python install_tech.py
26 | ```
27 | Then you need to restart Klayout to make sure the new technology installed appears.
28 |
29 | ## Examples
30 |
31 | After installing the PDK in your environment, you can validate that it is correctly working by running the Jupyter notebooks in the examples folder [docs/notebooks](https://github.com/Luxtelligence/lxt_pdk_gf/tree/main/docs/notebooks).
32 |
33 | ## Documentation
34 |
35 | - [gdsfactory docs](https://gdsfactory.github.io/gdsfactory/)
36 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.5.0
4 | hooks:
5 | - id: check-added-large-files
6 | # - id: check-case-conflict
7 | # - id: check-merge-conflict
8 | - id: check-symlinks
9 | - id: check-yaml
10 | - id: debug-statements
11 | - id: end-of-file-fixer
12 | - id: mixed-line-ending
13 | - id: name-tests-test
14 | args: ["--pytest-test-first"]
15 | - id: trailing-whitespace
16 |
17 | - repo: https://github.com/astral-sh/ruff-pre-commit
18 | rev: "v0.2.2"
19 | hooks:
20 | - id: ruff
21 | args: [ --fix, --exit-non-zero-on-fix ]
22 | - id: ruff-format
23 |
24 | - repo: https://github.com/shellcheck-py/shellcheck-py
25 | rev: 953faa6870f6663ac0121ab4a800f1ce76bca31f
26 | hooks:
27 | - id: shellcheck
28 |
29 | # - repo: https://github.com/pre-commit/mirrors-mypy
30 | # rev: "v1.0.1"
31 | # hooks:
32 | # - id: mypy
33 | # exclude: ^(docs/|example-plugin/|tests/fixtures)
34 | # additional_dependencies:
35 | # - "pydantic"
36 |
37 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
38 | rev: v2.12.0
39 | hooks:
40 | - id: pretty-format-toml
41 | args: [--autofix]
42 |
43 |
44 | - repo: https://github.com/aristanetworks/j2lint.git
45 | rev: 742a25ef5da996b9762f167ebae9bc8223e8382e
46 | hooks:
47 | - id: j2lint
48 | types: [file]
49 | files: \.(j2|yml|yaml)$
50 | args: [--extensions, "j2,yml,yaml", --ignore, jinja-statements-delimiter, jinja-statements-indentation, --]
51 | exclude: .github/.*
52 | - repo: https://github.com/codespell-project/codespell
53 | rev: v2.2.6
54 | hooks:
55 | - id: codespell
56 | additional_dependencies:
57 | - tomli
58 |
59 | - repo: https://github.com/kynan/nbstripout
60 | rev: 0.7.1
61 | hooks:
62 | - id: nbstripout
63 | files: ".ipynb"
64 | # - repo: https://github.com/pre-commit/pygrep-hooks
65 | # rev: 7b4409161486c6956bb3206ce96db5d56731b1b9 # Use the ref you want to point at
66 | # hooks:
67 | # - id: python-use-type-annotations
68 | # - repo: https://github.com/PyCQA/bandit
69 | # rev: fe1361fdcc274850d4099885a802f2c9f28aca08
70 | # hooks:
71 | # - id: bandit
72 | # args: [--exit-zero]
73 | # # ignore all tests, not just tests data
74 | # exclude: ^tests/
75 |
--------------------------------------------------------------------------------
/lnoi400/data/directional_coupler_balanced.json:
--------------------------------------------------------------------------------
1 | {
2 | "doc": "FTDT simulation results of the directional coupler for transmission at 1550 nm. Parameters of a polynomial fit around the center wavelength.",
3 | "center_wavelength": 1.55,
4 | "pol_trans_bar_abs": [
5 | -1047.4901613503614,
6 | -920.2404586375618,
7 | 18.20872763643584,
8 | 0.17783212818014305,
9 | -1.3669814442797343,
10 | 0.7319417383361545
11 | ],
12 | "pol_trans_bar_phase": [
13 | -7860750384729999.0,
14 | -244057639881537.3,
15 | 52285535413846.125,
16 | 1444832661654.5476,
17 | -128344019329.19559,
18 | -3041376403.0053744,
19 | 141271537.25054353,
20 | 2665026.3956523044,
21 | -64843.68232077865,
22 | -1143.9886065659468,
23 | 574.2474515350184,
24 | -807.6633182237304,
25 | -40.07757321847838
26 | ],
27 | "pol_trans_cross_abs": [
28 | -23206.140569161438,
29 | 29.959035285124333,
30 | 59.888458637039676,
31 | -0.21011574249867002,
32 | 1.3730165096112503,
33 | 0.6704987157500363
34 | ],
35 | "pol_trans_cross_phase": [
36 | 5620524229972341.0,
37 | 209633239380208.06,
38 | -38187074455895.62,
39 | -1235888619271.7837,
40 | 96067826723.8458,
41 | 2608200802.7288375,
42 | -109855657.03892201,
43 | -2367154.160317517,
44 | 56644.969588136344,
45 | 510.54742373701833,
46 | 554.3617377906189,
47 | -807.7739176751993,
48 | -44.783904814863675
49 | ],
50 | "pol_refl_bar_abs": [
51 | -51192.95288241643,
52 | 710.315828102365,
53 | 291.55222665022615,
54 | 4.283473971151275,
55 | -0.5849841568681478,
56 | 0.01115206457384756
57 | ],
58 | "pol_refl_bar_phase": [
59 | -2.632494240048258e+17,
60 | 1.0955720631730314e+16,
61 | 2488039605614699.5,
62 | -65047949255919.56,
63 | -8941831178486.834,
64 | 117960198682.58939,
65 | 14955286279.621767,
66 | -33012539.745406702,
67 | -10799689.249514855,
68 | -78972.42621926947,
69 | 2377.077797772454,
70 | -1619.9124756904775,
71 | -85.73309202928993
72 | ],
73 | "pol_refl_cross_abs": [
74 | -25780.842510073824,
75 | -974.5958262784009,
76 | 79.70833108972293,
77 | 1.327144588542236,
78 | 0.10504564138416553,
79 | 0.12893918663368492
80 | ],
81 | "pol_refl_cross_phase": [
82 | 976023151166609.6,
83 | -92939754117725.05,
84 | -12093976853664.701,
85 | 408458972394.40326,
86 | 48045169617.21425,
87 | -455904201.68035185,
88 | -85319103.65080932,
89 | -265668.0684952992,
90 | 80017.22091238415,
91 | 148.4129575313216,
92 | 1096.8615900287632,
93 | -1621.231477524193,
94 | -83.96399237932847
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
2 |
3 | [build-system]
4 | build-backend = "flit_core.buildapi"
5 | requires = ["flit_core >=3.2,<4"]
6 |
7 | [lint.pydocstyle]
8 | convention = "google"
9 |
10 | [project]
11 | authors = [
12 | {name = "Luxtelligence", email = "foundry@luxtelligence.ai"}
13 | ]
14 | classifiers = [
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Operating System :: OS Independent"
18 | ]
19 | dependencies = [
20 | "gdsfactory~=9.3",
21 | "gplugins[sax]>=1.4,<2"
22 | ]
23 | description = "Luxtelligence lnoi400 pdk"
24 | keywords = ["python"]
25 | license = {file = "LICENSE"}
26 | name = "lnoi400"
27 | readme = "README.md"
28 | requires-python = "~=3.12.0"
29 | version = "1.2.0"
30 |
31 | [project.optional-dependencies]
32 | dev = [
33 | "pre-commit",
34 | "pytest",
35 | "pytest-cov",
36 | "pytest_regressions"
37 | ]
38 | docs = [
39 | "jupytext",
40 | "matplotlib",
41 | "jupyter-book==1.0.4"
42 | ]
43 | plus = [
44 | "gdsfactoryplus"
45 | ]
46 |
47 | [tool.codespell]
48 | ignore-words-list = "te, te/tm, te, ba, fpr, fpr_spacing, ro, nd, donot, schem"
49 |
50 | [tool.gdsfactoryplus.drc]
51 | timeout = 300
52 |
53 | [tool.gdsfactoryplus.pdk]
54 | name = "lnoi400"
55 |
56 | [tool.mypy]
57 | python_version = "3.11"
58 | strict = true
59 |
60 | [tool.pylsp-mypy]
61 | enabled = true
62 | live_mode = true
63 | strict = true
64 |
65 | [tool.pytest.ini_options]
66 | # addopts = --tb=no
67 | addopts = '--tb=short'
68 | norecursedirs = ["extra/*.py"]
69 | python_files = ["lnoi400/*.py", "notebooks/*.ipynb", "tests/*.py"]
70 | testpaths = ["lnoi400/", "tests"]
71 |
72 | [tool.ruff]
73 | fix = true
74 | lint.ignore = [
75 | "E501", # line too long, handled by black
76 | "B008", # do not perform function calls in argument defaults
77 | "C901", # too complex
78 | "B905", # `zip()` without an explicit `strict=` parameter
79 | "C408" # C408 Unnecessary `dict` call (rewrite as a literal)
80 | ]
81 | lint.select = [
82 | "E", # pycodestyle errors
83 | "W", # pycodestyle warnings
84 | "F", # pyflakes
85 | "I", # isort
86 | "C", # flake8-comprehensions
87 | "B", # flake8-bugbear
88 | "UP"
89 | ]
90 |
91 | [tool.setuptools.package-data]
92 | mypkg = ["*.csv", "*.yaml"]
93 |
94 | [tool.setuptools.packages]
95 | find = {}
96 |
97 | [tool.tbump]
98 |
99 | [[tool.tbump.file]]
100 | src = "README.md"
101 |
102 | [[tool.tbump.file]]
103 | src = "pyproject.toml"
104 |
105 | [[tool.tbump.file]]
106 | src = "lnoi400/__init__.py"
107 |
108 | [tool.tbump.git]
109 | message_template = "Bump to {new_version}"
110 | tag_template = "v{new_version}"
111 |
112 | [tool.tbump.version]
113 | current = "0.0.1"
114 | # Example of a semver regexp.
115 | # Make sure this matches current_version before
116 | # using tbump
117 | regex = '''
118 | (?P\d+)
119 | \.
120 | (?P\d+)
121 | \.
122 | (?P\d+)
123 | '''
124 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows Explorer links
2 | *.lnk
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 | uv.lock
9 |
10 | extra/
11 | # C extensions
12 | *.so
13 | *.json
14 | ports/
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | builds/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | wheels/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 | cover/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 | db.sqlite3-journal
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | .pybuilder/
84 | target/
85 |
86 | # Jupyter Notebook
87 | # *.ipynb
88 | .ipynb_checkpoints
89 |
90 | # IPython
91 | profile_default/
92 | ipython_config.py
93 |
94 | # pyenv
95 | # For a library or package, you might want to ignore these files since the code is
96 | # intended to run in multiple environments; otherwise, check them in:
97 | # .python-version
98 |
99 | # pipenv
100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
103 | # install all needed dependencies.
104 | #Pipfile.lock
105 |
106 | # poetry
107 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108 | # This is especially recommended for binary packages to ensure reproducibility, and is more
109 | # commonly ignored for libraries.
110 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111 | #poetry.lock
112 |
113 | # pdm
114 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115 | #pdm.lock
116 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
117 | # in version control.
118 | # https://pdm.fming.dev/#use-with-ide
119 | .pdm.toml
120 |
121 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
122 | __pypackages__/
123 |
124 | # Celery stuff
125 | celerybeat-schedule
126 | celerybeat.pid
127 |
128 | # SageMath parsed files
129 | *.sage.py
130 |
131 | # Environments
132 | .env
133 | .venv
134 | env/
135 | venv/
136 | ENV/
137 | env.bak/
138 | venv.bak/
139 |
140 | # Spyder project settings
141 | .spyderproject
142 | .spyproject
143 |
144 | # Rope project settings
145 | .ropeproject
146 |
147 | # mkdocs documentation
148 | /site
149 |
150 | # mypy
151 | .mypy_cache/
152 | .dmypy.json
153 | dmypy.json
154 |
155 | # Pyre type checker
156 | .pyre/
157 |
158 | # pytype static type analyzer
159 | .pytype/
160 |
161 | # Cython debug symbols
162 | cython_debug/
163 |
164 | # PyCharm
165 |
166 | .idea/
167 |
168 | # VSC
169 |
170 | .vscode/
171 |
172 | # GDSII layouts
173 |
174 | *.oas
175 | lnoi/**/*.gds
176 |
177 | # Test with different PDK distributions
178 |
179 | test_pdk_releases.py
180 | mmi_test.gds
181 |
--------------------------------------------------------------------------------
/lnoi400/klayout/lnoi400.lyp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #7d57de
5 | #7d57de
6 | 0
7 | 0
8 |
9 |
10 | true
11 | true
12 | false
13 |
14 | false
15 | false
16 | 0
17 | LN_RIDGE
18 | 2/0@1
19 |
20 |
21 | #000080
22 | #000080
23 | 0
24 | 0
25 |
26 |
27 | true
28 | true
29 | false
30 |
31 | false
32 | false
33 | 0
34 | LN_SLAB
35 | 3/0@1
36 |
37 |
38 | #6750bf
39 | #6750bf
40 | 0
41 | 0
42 |
43 |
44 | true
45 | true
46 | false
47 |
48 | false
49 | false
50 | 0
51 | SLAB_NEGATIVE
52 | 3/1@1
53 |
54 |
55 | #5179b5
56 | #5179b5
57 | 0
58 | 0
59 |
60 |
61 | true
62 | true
63 | false
64 |
65 | false
66 | false
67 | 0
68 | LABELS
69 | 4/0@1
70 |
71 |
72 | #ffc6b8
73 | #ffc6b8
74 | 0
75 | 0
76 |
77 |
78 | true
79 | true
80 | false
81 |
82 | false
83 | false
84 | 0
85 | CHIP_CONTOUR
86 | 6/0@1
87 |
88 |
89 | #00fe9c
90 | #00fe9c
91 | 0
92 | 0
93 |
94 |
95 | true
96 | true
97 | false
98 |
99 | false
100 | false
101 | 0
102 | CHIP_EXCLUSION_ZONE
103 | 6/1@1
104 |
105 |
106 | #3503fc
107 | #3503fc
108 | 0
109 | 0
110 |
111 |
112 | true
113 | true
114 | false
115 |
116 | false
117 | false
118 | 0
119 | TL
120 | 21/0@1
121 |
122 |
123 | #3503fc
124 | #3503fc
125 | 0
126 | 0
127 | I3
128 |
129 | true
130 | true
131 | false
132 |
133 | false
134 | false
135 | 0
136 | HT
137 | 21/1@1
138 |
139 |
140 |
--------------------------------------------------------------------------------
/lnoi400/spline.py:
--------------------------------------------------------------------------------
1 | import gdsfactory as gf
2 | import matplotlib.pyplot as plt
3 | import numpy as np
4 | from gdsfactory.typings import Coordinate, CrossSectionSpec
5 |
6 |
7 | def spline_clamped_path(
8 | t: np.ndarray, start: Coordinate = (0.0, 0.0), end: Coordinate = (120.0, 25.0)
9 | ):
10 | """Returns a spline path with a null first derivative at the extrema."""
11 |
12 | xs = t
13 | ys = (t**2) * (3 - 2 * t)
14 |
15 | # Rescale to the start and end coordinates
16 |
17 | xs = start[0] + (end[0] - start[0]) * xs
18 | ys = start[1] + (end[1] - start[1]) * ys
19 |
20 | path = gf.Path(np.column_stack([xs, ys]))
21 | path.start_angle = path.end_angle = 0.0
22 |
23 | return path
24 |
25 |
26 | def spline_null_curvature(
27 | t: np.ndarray, start: Coordinate = (0.0, 0.0), end: Coordinate = (120.0, 25.0)
28 | ):
29 | """Returns a spline path with zero first and second derivatives at the extrema."""
30 |
31 | xs = t
32 | ys = (t**3) * (6 * t**2 - 15.0 * t + 10.0)
33 |
34 | xs = start[0] + (end[0] - start[0]) * xs
35 | ys = start[1] + (end[1] - start[1]) * ys
36 |
37 | path = gf.Path(np.column_stack([xs, ys]))
38 | path.start_angle = path.end_angle = 0.0
39 |
40 | return path
41 |
42 |
43 | @gf.cell
44 | def bend_S_spline(
45 | size: tuple[float, float] = (100.0, 30.0),
46 | cross_section: CrossSectionSpec = "xs_rwg1000",
47 | npoints: int = 201,
48 | path_method=spline_clamped_path,
49 | ) -> gf.Component:
50 | """A spline bend merging a vertical offset."""
51 |
52 | t = np.linspace(0, 1, npoints)
53 | xs = gf.get_cross_section(cross_section)
54 | path = path_method(t, start=(0.0, 0.0), end=size)
55 |
56 | c = path.extrude(xs)
57 |
58 | return c
59 |
60 |
61 | @gf.cell
62 | def bend_S_spline_varying_width(
63 | size: tuple[float, float] = (58, 14.5),
64 | cross_section1: CrossSectionSpec = None,
65 | cross_section2: CrossSectionSpec = None,
66 | npoints: int = 201,
67 | path_method=spline_null_curvature,
68 | ) -> gf.Component:
69 | """
70 | A spline bend merging a vertical offset.
71 | Can accept arbitrary cross sections. Not tested as a standalone PDK element. Used as a building
72 | block for cells with known behaviour.
73 | """
74 |
75 | if not cross_section1:
76 | s0 = gf.Section(
77 | width=0.2,
78 | offset=0,
79 | layer="LN_RIDGE",
80 | name="_default",
81 | port_names=("o1", "o2"),
82 | )
83 | s1 = gf.Section(
84 | width=10.0, offset=0, layer="LN_SLAB", name="slab", simplify=0.03
85 | )
86 | cross_section1 = gf.CrossSection(sections=[s0, s1])
87 |
88 | if not cross_section2:
89 | s0 = gf.Section(
90 | width=0.3,
91 | offset=0,
92 | layer="LN_RIDGE",
93 | name="_default",
94 | port_names=("o1", "o2"),
95 | )
96 | s1 = gf.Section(
97 | width=10.0, offset=0, layer="LN_SLAB", name="slab", simplify=0.03
98 | )
99 | cross_section2 = gf.CrossSection(sections=[s0, s1])
100 |
101 | t = np.linspace(0, 1, npoints)
102 | path = path_method(t, start=(0.0, 0.0), end=size)
103 |
104 | xtrans = gf.path.transition(
105 | cross_section1=cross_section1,
106 | cross_section2=cross_section2,
107 | width_type="linear",
108 | )
109 | return gf.path.extrude_transition(path, xtrans)
110 |
111 |
112 | if __name__ == "__main__":
113 | # Visualize differences between spline and bezier path
114 |
115 | t = np.linspace(0, 1, 600)
116 |
117 | apath = spline_null_curvature(t, end=(50.0, 15.0))
118 | bpath = spline_clamped_path(t, end=(50.0, 15.0))
119 | _, ka = apath.curvature()
120 | _, kb = bpath.curvature()
121 |
122 | plot_args_a = {
123 | "linewidth": 2.1,
124 | "label": "Zero curvature",
125 | }
126 |
127 | plot_args_b = {
128 | "linewidth": plot_args_a["linewidth"],
129 | "label": "Zero derivative",
130 | }
131 |
132 | ap = apath.points
133 | bp = bpath.points
134 | # ka = np.column_stack((ap[:-1, 0], curv_apath))
135 | # kb = np.column_stack((bp[:-1, 0], curv_bpath))
136 |
137 | fig, axs = plt.subplots(1, 2, figsize=(9, 3.5), tight_layout=True)
138 | axs[0].plot(ap[:, 0], ap[:, 1], **plot_args_a)
139 | axs[0].plot(bp[:, 0], bp[:, 1], **plot_args_b)
140 |
141 | axs[0].set_xlabel("x (um)")
142 | axs[0].set_ylabel("y (um)")
143 |
144 | axs[1].plot(t[0:-1], ka, **plot_args_a)
145 | axs[1].plot(t[0:-1], kb, **plot_args_b)
146 |
147 | axs[1].set_xlabel("x (um)")
148 | axs[1].set_ylabel("Curvature (arb.)")
149 |
150 | [axs[k].legend(loc="best") for k in range(2)]
151 | plt.show()
152 |
--------------------------------------------------------------------------------
/docs/cells.rst:
--------------------------------------------------------------------------------
1 |
2 |
3 | Luxtelligence provides a library of components that have been fabricated in the reference material stack, and whose performance has been tested and validated. Here follows a list of the available parametric cells (gdsfactory.Component objects):
4 |
5 |
6 | Cells
7 | =============================
8 |
9 |
10 | CPW_pad_linear
11 | ----------------------------------------------------
12 |
13 | .. autofunction:: lnoi400.cells.CPW_pad_linear
14 |
15 | .. plot::
16 | :include-source:
17 |
18 | import lnoi400
19 |
20 | c = lnoi400.cells.CPW_pad_linear(start_width=80.0, length_straight=10.0, length_tapered=190.0, cross_section='xs_uni_cpw')
21 | c.plot()
22 |
23 |
24 |
25 | L_turn_bend
26 | ----------------------------------------------------
27 |
28 | .. autofunction:: lnoi400.cells.L_turn_bend
29 |
30 | .. plot::
31 | :include-source:
32 |
33 | import lnoi400
34 |
35 | c = lnoi400.cells.L_turn_bend(radius=80.0, p=1.0, with_arc_floorplan=True, cross_section='xs_rwg1000')
36 | c.plot()
37 |
38 |
39 |
40 | S_bend_vert
41 | ----------------------------------------------------
42 |
43 | .. autofunction:: lnoi400.cells.S_bend_vert
44 |
45 | .. plot::
46 | :include-source:
47 |
48 | import lnoi400
49 |
50 | c = lnoi400.cells.S_bend_vert(v_offset=25.0, h_extent=100.0, dx_straight=5.0, cross_section='xs_rwg1000')
51 | c.plot()
52 |
53 |
54 |
55 | U_bend_racetrack
56 | ----------------------------------------------------
57 |
58 | .. autofunction:: lnoi400.cells.U_bend_racetrack
59 |
60 | .. plot::
61 | :include-source:
62 |
63 | import lnoi400
64 |
65 | c = lnoi400.cells.U_bend_racetrack(v_offset=90.0, p=1.0, with_arc_floorplan=True, cross_section='xs_rwg3000')
66 | c.plot()
67 |
68 |
69 |
70 | bend_S_spline
71 | ----------------------------------------------------
72 |
73 | .. autofunction:: lnoi400.cells.bend_S_spline
74 |
75 | .. plot::
76 | :include-source:
77 |
78 | import lnoi400
79 |
80 | c = lnoi400.cells.bend_S_spline(size=(100.0, 30.0), cross_section='xs_rwg1000', npoints=201)
81 | c.plot()
82 |
83 |
84 |
85 | chip_frame
86 | ----------------------------------------------------
87 |
88 | .. autofunction:: lnoi400.cells.chip_frame
89 |
90 | .. plot::
91 | :include-source:
92 |
93 | import lnoi400
94 |
95 | c = lnoi400.cells.chip_frame(size=(10000, 5000), exclusion_zone_width=50)
96 | c.plot()
97 |
98 |
99 |
100 | double_linear_inverse_taper
101 | ----------------------------------------------------
102 |
103 | .. autofunction:: lnoi400.cells.double_linear_inverse_taper
104 |
105 | .. plot::
106 | :include-source:
107 |
108 | import lnoi400
109 |
110 | c = lnoi400.cells.double_linear_inverse_taper(cross_section_start='xs_swg250', cross_section_end='xs_rwg1000', lower_taper_length=120.0, lower_taper_end_width=2.05, upper_taper_start_width=0.25, upper_taper_length=240.0, slab_removal_width=20.0, input_ext=0.0)
111 | c.plot()
112 |
113 |
114 |
115 | eo_phase_shifter
116 | ----------------------------------------------------
117 |
118 | .. autofunction:: lnoi400.cells.eo_phase_shifter
119 |
120 | .. plot::
121 | :include-source:
122 |
123 | import lnoi400
124 |
125 | c = lnoi400.cells.eo_phase_shifter(rib_core_width_modulator=2.5, taper_length=100.0, modulation_length=7500.0, rf_central_conductor_width=10.0, rf_ground_planes_width=180.0, rf_gap=4.0, draw_cpw=True)
126 | c.plot()
127 |
128 |
129 |
130 | mmi1x2_optimized1550
131 | ----------------------------------------------------
132 |
133 | .. autofunction:: lnoi400.cells.mmi1x2_optimized1550
134 |
135 | .. plot::
136 | :include-source:
137 |
138 | import lnoi400
139 |
140 | c = lnoi400.cells.mmi1x2_optimized1550(width_mmi=6.0, length_mmi=26.75, width_taper=1.5, length_taper=25.0, port_ratio=0.55, cross_section='xs_rwg1000')
141 | c.plot()
142 |
143 |
144 |
145 | mmi2x2optimized1550
146 | ----------------------------------------------------
147 |
148 | .. autofunction:: lnoi400.cells.mmi2x2optimized1550
149 |
150 | .. plot::
151 | :include-source:
152 |
153 | import lnoi400
154 |
155 | c = lnoi400.cells.mmi2x2optimized1550(width_mmi=5.0, length_mmi=76.5, width_taper=1.5, length_taper=25.0, port_ratio=0.7, cross_section='xs_rwg1000')
156 | c.plot()
157 |
158 |
159 |
160 | mzm_unbalanced
161 | ----------------------------------------------------
162 |
163 | .. autofunction:: lnoi400.cells.mzm_unbalanced
164 |
165 | .. plot::
166 | :include-source:
167 |
168 | import lnoi400
169 |
170 | c = lnoi400.cells.mzm_unbalanced(modulation_length=7500.0, lbend_tune_arm_reff=75.0, rf_pad_start_width=80.0, rf_central_conductor_width=10.0, rf_ground_planes_width=180.0, rf_gap=4.0, rf_pad_length_straight=10.0, rf_pad_length_tapered=190.0)
171 | c.plot()
172 |
173 |
174 |
175 | straight_rwg1000
176 | ----------------------------------------------------
177 |
178 | .. autofunction:: lnoi400.cells.straight_rwg1000
179 |
180 | .. plot::
181 | :include-source:
182 |
183 | import lnoi400
184 |
185 | c = lnoi400.cells.straight_rwg1000(length=10.0)
186 | c.plot()
187 |
188 |
189 |
190 | straight_rwg3000
191 | ----------------------------------------------------
192 |
193 | .. autofunction:: lnoi400.cells.straight_rwg3000
194 |
195 | .. plot::
196 | :include-source:
197 |
198 | import lnoi400
199 |
200 | c = lnoi400.cells.straight_rwg3000(length=10.0)
201 | c.plot()
202 |
203 |
204 |
205 | uni_cpw_straight
206 | ----------------------------------------------------
207 |
208 | .. autofunction:: lnoi400.cells.uni_cpw_straight
209 |
210 | .. plot::
211 | :include-source:
212 |
213 | import lnoi400
214 |
215 | c = lnoi400.cells.uni_cpw_straight(length=3000.0, cross_section='xs_uni_cpw', bondpad='CPW_pad_linear')
216 | c.plot()
217 |
--------------------------------------------------------------------------------
/lnoi400/klayout/tech.lyt:
--------------------------------------------------------------------------------
1 |
2 |
3 | LNOI400
4 |
5 |
6 | 0.001
7 |
8 |
9 |
10 | layers.lyp
11 | true
12 |
13 |
14 | 1
15 | true
16 | true
17 |
18 |
19 | true
20 | layer_map()
21 | true
22 | true
23 |
24 |
25 | true
26 | layer_map()
27 | 0.001
28 | true
29 | #1
30 | true
31 | #1
32 | false
33 | #1
34 | true
35 | OUTLINE
36 | true
37 | PLACEMENT_BLK
38 | true
39 | REGIONS
40 | true
41 |
42 | 0
43 | true
44 | .PIN
45 | 2
46 | true
47 | .PIN
48 | 2
49 | true
50 | .FILL
51 | 5
52 | true
53 | .OBS
54 | 3
55 | true
56 | .BLK
57 | 4
58 | true
59 | .LABEL
60 | 1
61 | true
62 | .LABEL
63 | 1
64 | true
65 |
66 | 0
67 | true
68 |
69 | 0
70 | VIA_
71 | true
72 | default
73 | false
74 | false
75 |
76 |
77 |
78 | false
79 | true
80 | true
81 | 64
82 | 0
83 | 1
84 | 0
85 | DATA
86 | 0
87 | 0
88 | BORDER
89 | layer_map()
90 | true
91 |
92 |
93 | 0.001
94 | 1
95 | 100
96 | 100
97 | 0
98 | 0
99 | 0
100 | false
101 | false
102 | false
103 | true
104 | layer_map()
105 |
106 |
107 | 0
108 | 0.001
109 | layer_map()
110 | true
111 | false
112 |
113 |
114 | 1
115 | 0.001
116 | layer_map()
117 | true
118 | false
119 | true
120 |
121 |
122 |
123 |
124 | GDS2
125 |
126 | true
127 | false
128 | false
129 | false
130 | false
131 | false
132 | 8000
133 | 32000
134 | LIB
135 |
136 |
137 | 2
138 | true
139 | true
140 | 1
141 | *
142 | false
143 |
144 |
145 | 0
146 |
147 |
148 | false
149 | false
150 |
151 |
152 | 0
153 |
154 | true
155 |
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/lnoi400/tech.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 |
3 | import gdsfactory as gf
4 | from gdsfactory.cross_section import (
5 | CrossSection,
6 | )
7 | from gdsfactory.technology import (
8 | LayerLevel,
9 | LayerMap,
10 | LayerStack,
11 | LogicalLayer,
12 | )
13 | from gdsfactory.typings import Layer, LayerSpec
14 |
15 | from lnoi400.config import PATH
16 |
17 | nm = 1e-3
18 | ridge_thickness = 200 * nm
19 | slab_thickness = 200 * nm
20 | box_thickness = 4700 * nm
21 | thickness_clad = 2000 * nm
22 | tl_separation = 1000 * nm
23 | tl_thickness = 900 * nm
24 | substrate_thickness = 10_000 * nm
25 |
26 |
27 | class LayerMapLNOI400(LayerMap):
28 | """Layer map for LXT lnoi400 technology."""
29 |
30 | LN_RIDGE: Layer = (2, 0)
31 | LN_SLAB: Layer = (3, 0)
32 | SLAB_NEGATIVE: Layer = (3, 1)
33 | LABELS: Layer = (4, 0)
34 | TL: Layer = (21, 0)
35 | HT: Layer = (21, 1)
36 | WAFER: Layer = (990, 0)
37 |
38 | # AUX
39 |
40 | CHIP_CONTOUR: Layer = (6, 0)
41 | CHIP_EXCLUSION_ZONE: Layer = (6, 1)
42 | DOC: Layer = (201, 0)
43 | ERROR: Layer = (50, 1)
44 |
45 | # common gdsfactory layers
46 |
47 | LABEL_INSTANCE: Layer = (66, 0)
48 | DEVREC: Layer = (68, 0)
49 | PORT: Layer = (99, 10)
50 | PORTE: Layer = (99, 11)
51 | TE: Layer = (203, 0)
52 | TM: Layer = (204, 0)
53 | TEXT: Layer = (66, 0)
54 |
55 |
56 | LAYER = LayerMapLNOI400
57 |
58 |
59 | def get_layer_stack() -> LayerStack:
60 | """Return lnoi400 LayerStack."""
61 |
62 | zmin_electrodes = slab_thickness + ridge_thickness + tl_separation
63 |
64 | lstack = LayerStack(
65 | layers=dict(
66 | substrate=LayerLevel(
67 | layer=LogicalLayer(layer=LAYER.WAFER),
68 | thickness=substrate_thickness,
69 | zmin=-substrate_thickness - box_thickness,
70 | material="si",
71 | orientation="100",
72 | mesh_order=101,
73 | ),
74 | box=LayerLevel(
75 | layer=LogicalLayer(layer=LAYER.WAFER),
76 | thickness=box_thickness,
77 | zmin=-box_thickness,
78 | material="sio2",
79 | mesh_order=100,
80 | ),
81 | slab=LayerLevel(
82 | layer=LogicalLayer(layer=LAYER.LN_SLAB),
83 | thickness=slab_thickness,
84 | zmin=0.0,
85 | sidewall_angle=13.0,
86 | material="ln",
87 | mesh_order=1,
88 | ),
89 | ridge=LayerLevel(
90 | layer=LogicalLayer(layer=LAYER.LN_RIDGE),
91 | thickness=ridge_thickness,
92 | zmin=slab_thickness,
93 | sidewall_angle=13.0,
94 | width_to_z=1,
95 | material="ln",
96 | mesh_order=2,
97 | ),
98 | clad=LayerLevel(
99 | layer=LogicalLayer(layer=LAYER.WAFER),
100 | zmin=0.0,
101 | material="sio2",
102 | thickness=thickness_clad,
103 | mesh_order=99,
104 | ),
105 | tl=LayerLevel(
106 | layer=LogicalLayer(layer=LAYER.TL),
107 | thickness=tl_thickness,
108 | zmin=zmin_electrodes,
109 | material="tl_metal",
110 | mesh_order=6,
111 | ),
112 | ht=LayerLevel(
113 | layer=LogicalLayer(layer=LAYER.HT),
114 | thickness=tl_thickness,
115 | zmin=zmin_electrodes,
116 | material="tl_metal",
117 | mesh_order=7,
118 | ),
119 | )
120 | )
121 |
122 | return lstack
123 |
124 |
125 | LAYER_STACK = get_layer_stack()
126 | LAYER_VIEWS = gf.technology.LayerViews(filepath=PATH.lyp_yaml)
127 |
128 | ############################
129 | # Cross-section functions
130 | ############################
131 |
132 | xsection = gf.xsection
133 |
134 |
135 | @xsection
136 | def xs_rwg1000(
137 | layer: LayerSpec = "LN_RIDGE",
138 | width: float = 1.0,
139 | radius: float = 60.0,
140 | ) -> CrossSection:
141 | """Routing rib waveguide cross section"""
142 | sections = (
143 | gf.Section(
144 | width=10.0,
145 | layer="LN_SLAB",
146 | name="slab",
147 | simplify=30 * nm,
148 | ),
149 | )
150 | return gf.cross_section.strip(
151 | width=width,
152 | layer=layer,
153 | sections=sections,
154 | radius=radius,
155 | )
156 |
157 |
158 | @xsection
159 | def xs_rwg2500(
160 | layer: LayerSpec = "LN_RIDGE",
161 | width: float = 2.5,
162 | ) -> CrossSection:
163 | sections = (
164 | gf.Section(
165 | width=11.5,
166 | layer="LN_SLAB",
167 | name="slab",
168 | simplify=30 * nm,
169 | ),
170 | )
171 | return gf.cross_section.strip(
172 | width=width,
173 | layer=layer,
174 | sections=sections,
175 | )
176 |
177 |
178 | @xsection
179 | def xs_rwg3000(
180 | layer: LayerSpec = "LN_RIDGE",
181 | width: float = 3.0,
182 | ) -> CrossSection:
183 | """Multimode rib waveguide cross section"""
184 | sections = (
185 | gf.Section(
186 | width=12.0,
187 | layer="LN_SLAB",
188 | name="slab",
189 | simplify=30 * nm,
190 | ),
191 | )
192 | return gf.cross_section.strip(
193 | width=width,
194 | layer=layer,
195 | sections=sections,
196 | )
197 |
198 |
199 | @xsection
200 | def xs_swg250(
201 | layer: LayerSpec = "LN_SLAB",
202 | width: float = 0.25,
203 | ) -> CrossSection:
204 | return gf.cross_section.strip(
205 | width=width,
206 | layer=layer,
207 | )
208 |
209 |
210 | @xsection
211 | def xs_ht_wire(
212 | width: float = 0.9,
213 | offset: float = 0.0,
214 | ) -> CrossSection:
215 | """Generate cross-section of a heater wire."""
216 |
217 | return gf.cross_section.cross_section(
218 | width=width,
219 | offset=offset,
220 | layer=LAYER.HT,
221 | port_names=gf.cross_section.port_names_electrical,
222 | port_types=gf.cross_section.port_types_electrical,
223 | )
224 |
225 |
226 | @xsection
227 | def xs_uni_cpw(
228 | central_conductor_width: float = 15.0,
229 | ground_planes_width: float = 250.0,
230 | gap: float = 5.0,
231 | ) -> CrossSection:
232 | """Generate cross-section of a uniform coplanar waveguide."""
233 |
234 | offset = 0.5 * (central_conductor_width + ground_planes_width) + gap
235 |
236 | g1 = gf.Section(
237 | width=ground_planes_width,
238 | offset=-offset,
239 | layer=LAYER.TL,
240 | simplify=50 * nm,
241 | name="ground_bottom",
242 | )
243 |
244 | g2 = gf.Section(
245 | width=ground_planes_width,
246 | offset=offset,
247 | layer=LAYER.TL,
248 | simplify=50 * nm,
249 | name="ground_top",
250 | )
251 |
252 | s = gf.Section(
253 | width=central_conductor_width,
254 | offset=0.0,
255 | layer=LAYER.TL,
256 | simplify=50 * nm,
257 | name="signal",
258 | )
259 |
260 | xs_cpw = gf.cross_section.cross_section(
261 | width=central_conductor_width,
262 | offset=0.0,
263 | layer=LAYER.TL,
264 | sections=(g1, s, g2),
265 | port_names=gf.cross_section.port_names_electrical,
266 | port_types=gf.cross_section.port_types_electrical,
267 | )
268 |
269 | return xs_cpw
270 |
271 |
272 | ############################
273 | # Routing functions
274 | ############################
275 |
276 |
277 | route_bundle_rwg1000 = partial(
278 | gf.routing.route_bundle,
279 | cross_section="xs_rwg1000",
280 | straight="straight_rwg1000",
281 | bend=gf.components.bend_euler,
282 | radius=60.0,
283 | separation=7.5,
284 | start_straight_length=5.0,
285 | end_straight_length=5.0,
286 | min_straight_taper=100.0,
287 | on_collision="show_error",
288 | )
289 |
290 | if __name__ == "__main__":
291 | pass
292 |
--------------------------------------------------------------------------------
/docs/notebooks/models.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Circuit models\n",
8 | "\n",
9 | "We provide circuit models for the PDK elements, implemented using the [sax](https://flaport.github.io/sax/) circuit simulator. A dictionary with the available models can be obtained by running:\n",
10 | "\n",
11 | "`models = lnoi400.get_models()`\n",
12 | "\n",
13 | "These models are useful for constructing the scattering matrix of a building block, or of a hierarchical circuit. They are obtained by experimental characterization of the building blocks and with FDTD simulations,\n",
14 | "that provide the full wavelength-dependent behaviour. Since the lnoi400 PDK is conceived for optical C-band operation, the model results should not be trusted below 1500 or above 1600 nm.\n",
15 | "\n",
16 | "### Examples of circuit simulation using sax"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": null,
22 | "metadata": {},
23 | "outputs": [],
24 | "source": [
25 | "import numpy as np\n",
26 | "from functools import partial\n",
27 | "import matplotlib.pyplot as plt\n",
28 | "import gdsfactory as gf\n",
29 | "import sax\n",
30 | "import gplugins.sax as gs\n",
31 | "import lnoi400"
32 | ]
33 | },
34 | {
35 | "cell_type": "markdown",
36 | "metadata": {},
37 | "source": [
38 | "#### Circuit simulation of a splitter tree"
39 | ]
40 | },
41 | {
42 | "cell_type": "markdown",
43 | "metadata": {},
44 | "source": [
45 | "First, we display the circuit model of the 1x2 MMI shipped with the PDK."
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": null,
51 | "metadata": {},
52 | "outputs": [],
53 | "source": [
54 | "splitter = gf.get_component(\"mmi1x2_optimized1550\")\n",
55 | "splitter.plot()"
56 | ]
57 | },
58 | {
59 | "cell_type": "code",
60 | "execution_count": null,
61 | "metadata": {},
62 | "outputs": [],
63 | "source": [
64 | "pcell_models = lnoi400.get_models()\n",
65 | "mmi_model = pcell_models[\"mmi1x2_optimized1550\"]\n",
66 | "_ = gs.plot_model(\n",
67 | " mmi_model,\n",
68 | " wavelength_start=1.5,\n",
69 | " wavelength_stop=1.6,\n",
70 | " port1=\"o1\",\n",
71 | " ports2=(\"o2\", \"o3\"),\n",
72 | ")\n"
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {},
78 | "source": [
79 | "We then build a simple, two-level-deep splitter tree, creating a new gdsfactory hierarchical component. "
80 | ]
81 | },
82 | {
83 | "cell_type": "code",
84 | "execution_count": null,
85 | "metadata": {},
86 | "outputs": [],
87 | "source": [
88 | "@gf.cell\n",
89 | "def splitter_chain(\n",
90 | " splitter = gf.get_component(\"mmi1x2_optimized1550\"),\n",
91 | " column_offset = (250.0, 200.0),\n",
92 | " routing_reff = 90.0\n",
93 | ") -> gf.Component:\n",
94 | "\n",
95 | " c = gf.Component()\n",
96 | " s0 = c << splitter\n",
97 | " s01 = c << splitter\n",
98 | " s02 = c << splitter\n",
99 | " s01.dmove(\n",
100 | " s01.ports[\"o1\"].dcenter,\n",
101 | " s0.ports[\"o2\"].dcenter + np.array(column_offset)\n",
102 | " )\n",
103 | " s02.dmove(\n",
104 | " s02.ports[\"o1\"].dcenter,\n",
105 | " s0.ports[\"o3\"].dcenter + np.array([column_offset[0], - column_offset[1]])\n",
106 | " )\n",
107 | "\n",
108 | " # Bend spec\n",
109 | "\n",
110 | " routing_bend = gf.get_component('L_turn_bend', radius=routing_reff)\n",
111 | "\n",
112 | " # Routing between splitters\n",
113 | "\n",
114 | " for ports_to_route in [\n",
115 | " (s0.ports[\"o2\"], s01.ports[\"o1\"]),\n",
116 | " (s0.ports[\"o3\"], s02.ports[\"o1\"]),\n",
117 | " ]:\n",
118 | "\n",
119 | " gf.routing.route_single(\n",
120 | " c,\n",
121 | " ports_to_route[0],\n",
122 | " ports_to_route[1],\n",
123 | " start_straight_length=5.0,\n",
124 | " end_straight_length=5.0,\n",
125 | " cross_section=\"xs_rwg1000\",\n",
126 | " bend=routing_bend,\n",
127 | " straight=\"straight_rwg1000\",\n",
128 | " )\n",
129 | "\n",
130 | " # Expose the I/O ports\n",
131 | "\n",
132 | " c.add_port(name=\"in\", port=s0.ports[\"o1\"])\n",
133 | " c.add_port(name=\"out_00\", port=s01.ports[\"o2\"])\n",
134 | " c.add_port(name=\"out_01\", port=s01.ports[\"o3\"])\n",
135 | " c.add_port(name=\"out_10\", port=s02.ports[\"o2\"])\n",
136 | " c.add_port(name=\"out_11\", port=s02.ports[\"o3\"])\n",
137 | "\n",
138 | " return c\n",
139 | "\n",
140 | "chain = splitter_chain()\n",
141 | "chain"
142 | ]
143 | },
144 | {
145 | "cell_type": "markdown",
146 | "metadata": {},
147 | "source": [
148 | "Let's compile the circuit simulation using sax."
149 | ]
150 | },
151 | {
152 | "cell_type": "code",
153 | "execution_count": null,
154 | "metadata": {},
155 | "outputs": [],
156 | "source": [
157 | "nl = chain.get_netlist()\n",
158 | "\n",
159 | "models = {\n",
160 | " # The Euler bend should be sufficiently low-loss to be approximated with a straight waveguide\n",
161 | " # (if the frequency is not too low)\n",
162 | " \"L_turn_bend\": pcell_models[\"straight_rwg1000\"],\n",
163 | " \"straight_rwg1000\": pcell_models[\"straight_rwg1000\"],\n",
164 | " \"mmi1x2_optimized1550\": pcell_models[\"mmi1x2_optimized1550\"],\n",
165 | "}\n",
166 | "circuit, _ = sax.circuit(netlist=nl, models=models)\n",
167 | "\n",
168 | "_ = gs.plot_model(\n",
169 | " circuit,\n",
170 | " wavelength_start=1.5,\n",
171 | " wavelength_stop=1.6,\n",
172 | " port1=\"in\",\n",
173 | " ports2=(\"out_00\", \"out_11\"),\n",
174 | ")"
175 | ]
176 | },
177 | {
178 | "cell_type": "markdown",
179 | "metadata": {},
180 | "source": [
181 | "#### Simulation of a Mach-Zehnder interferometer with a thermo-optical phase shifter"
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {},
187 | "source": [
188 | "First we take a look at the cell layout."
189 | ]
190 | },
191 | {
192 | "cell_type": "code",
193 | "execution_count": null,
194 | "metadata": {},
195 | "outputs": [],
196 | "source": [
197 | "mzm_specs = dict(\n",
198 | " modulation_length=1500.0,\n",
199 | " with_heater=True,\n",
200 | " bias_tuning_section_length=1000.0,\n",
201 | ")\n",
202 | "mzm = gf.get_component(\n",
203 | " \"mzm_unbalanced\",\n",
204 | " **mzm_specs,\n",
205 | " )\n",
206 | "mzm.plot()"
207 | ]
208 | },
209 | {
210 | "cell_type": "markdown",
211 | "metadata": {},
212 | "source": [
213 | "Then, we retrieve the circuit model and evaluate it for different wavelengths and voltages."
214 | ]
215 | },
216 | {
217 | "cell_type": "code",
218 | "execution_count": null,
219 | "metadata": {},
220 | "outputs": [],
221 | "source": [
222 | "mzm_specs = dict(\n",
223 | " modulation_length=1500.0,\n",
224 | " heater_length=1000.0,\n",
225 | ")\n",
226 | "mzm_model = partial(\n",
227 | " pcell_models[\"mzm_unbalanced\"],\n",
228 | " **mzm_specs,\n",
229 | ")\n",
230 | "\n",
231 | "fig = plt.figure(figsize=(7.5, 5))\n",
232 | "\n",
233 | "wls = [1.4, 1.5, 1.6]\n",
234 | "V_scan = np.linspace(-3, 3, 99)\n",
235 | "for wl in wls:\n",
236 | " P_out = [np.abs(mzm_model(\n",
237 | " wl=wl,\n",
238 | " V_ht=V,\n",
239 | " )[\"o2\", \"o1\"])\n",
240 | " for V in V_scan]\n",
241 | " plt.semilogy(V_scan, P_out, label=f'{wl} um')\n",
242 | "\n",
243 | "plt.legend(loc='best')\n",
244 | "plt.xlabel(\"Voltage (V)\")\n",
245 | "_ = plt.ylabel(\"MZM transmission\")"
246 | ]
247 | },
248 | {
249 | "cell_type": "code",
250 | "execution_count": null,
251 | "metadata": {},
252 | "outputs": [],
253 | "source": []
254 | }
255 | ],
256 | "metadata": {
257 | "kernelspec": {
258 | "display_name": "gf-pdk-dev",
259 | "language": "python",
260 | "name": "python3"
261 | },
262 | "language_info": {
263 | "codemirror_mode": {
264 | "name": "ipython",
265 | "version": 3
266 | },
267 | "file_extension": ".py",
268 | "mimetype": "text/x-python",
269 | "name": "python",
270 | "nbconvert_exporter": "python",
271 | "pygments_lexer": "ipython3",
272 | "version": "3.12.9"
273 | }
274 | },
275 | "nbformat": 4,
276 | "nbformat_minor": 2
277 | }
278 |
--------------------------------------------------------------------------------
/docs/notebooks/circuit_layout.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Circuit layout\n",
8 | "\n",
9 | "Here we provide an example of PIC layout with the lnoi400 PDK. We start by choosing a die floorplan compatible with a submission for an LXT MPW run, then place some edge couplers for I/O at the right locations on the chip frame. Finally we create a circuit cell with an evanescently-coupled ring resonator and connect it with the input and output edge couplers."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "from functools import partial\n",
19 | "from pathlib import Path\n",
20 | "import numpy as np\n",
21 | "import lnoi400\n",
22 | "import gdsfactory as gf"
23 | ]
24 | },
25 | {
26 | "cell_type": "markdown",
27 | "metadata": {},
28 | "source": [
29 | "### Choose the chip format and display the outline"
30 | ]
31 | },
32 | {
33 | "cell_type": "code",
34 | "execution_count": null,
35 | "metadata": {},
36 | "outputs": [],
37 | "source": [
38 | "@gf.cell\n",
39 | "def chip_frame():\n",
40 | " c = gf.get_component(\"chip_frame\", size=(10_000, 5000), center=(0, 0))\n",
41 | " return c\n",
42 | "\n",
43 | "chip_layout = chip_frame()\n",
44 | "chip_layout"
45 | ]
46 | },
47 | {
48 | "cell_type": "markdown",
49 | "metadata": {},
50 | "source": [
51 | "### Get the circuit building blocks"
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": null,
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "input_ext = 10.0\n",
61 | "double_taper = gf.get_component(\"double_linear_inverse_taper\",\n",
62 | " input_ext=input_ext,\n",
63 | " )\n",
64 | "\n",
65 | "coupler_gap = 0.6\n",
66 | "ring_radius = 100.0\n",
67 | "ring_width = 2.0\n",
68 | "wg_width = 1.0\n",
69 | "\n",
70 | "ring = gf.components.ring(\n",
71 | " layer=\"LN_RIDGE\",\n",
72 | " radius=ring_radius,\n",
73 | " width=ring_width,\n",
74 | " angle_resolution=0.15,\n",
75 | ")\n",
76 | "\n",
77 | "dc_wg = gf.components.straight(\n",
78 | " length = ring_radius * 2,\n",
79 | " cross_section=\"xs_rwg1000\",\n",
80 | ")\n",
81 | "\n",
82 | "@gf.cell\n",
83 | "def ring_with_coupler(\n",
84 | " ring=ring,\n",
85 | " bus=dc_wg,\n",
86 | " gap=coupler_gap,\n",
87 | ") -> gf.Component:\n",
88 | "\n",
89 | " c = gf.Component()\n",
90 | " ring_ref = c << ring\n",
91 | " coupler_ref = c << bus\n",
92 | " coupler_ref.drotate(90)\n",
93 | " coupler_ref.dcenter = [\n",
94 | " ring_ref.dxmax + gap + 0.5 * wg_width, 0.0\n",
95 | " ]\n",
96 | " c.add_ports(coupler_ref.ports)\n",
97 | " c.flatten()\n",
98 | " return c\n",
99 | "\n",
100 | "coupled_ring = ring_with_coupler()\n",
101 | "coupled_ring"
102 | ]
103 | },
104 | {
105 | "cell_type": "markdown",
106 | "metadata": {},
107 | "source": [
108 | "### Circuit assembly\n",
109 | "\n",
110 | "Positioning of the I/O couplers"
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": null,
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "x_in = chip_layout.dxmin + 1000.0\n",
120 | "in_loc = np.array([x_in, chip_layout.dymax])\n",
121 | "out_loc = np.array([x_in + 2.5 * ring_radius, chip_layout.dymin])\n",
122 | "\n",
123 | "ec_in = gf.Component()\n",
124 | "ec_ref = ec_in << double_taper\n",
125 | "ec_ref.drotate(-90.0)\n",
126 | "ec_ref.dmove(\n",
127 | " ec_ref.ports[\"o1\"].dcenter, in_loc + [0.0, 0.5 * input_ext]\n",
128 | ")\n",
129 | "ec_in.add_ports(ec_ref.ports)\n",
130 | "\n",
131 | "ec_out = gf.Component()\n",
132 | "ec_ref = ec_out << double_taper\n",
133 | "ec_ref.drotate(90.0)\n",
134 | "ec_ref.dmove(\n",
135 | " ec_ref.ports[\"o1\"].dcenter, out_loc - [0.0, 0.5 * input_ext]\n",
136 | ")\n",
137 | "ec_out.add_ports(ec_ref.ports)\n",
138 | "\n",
139 | "ecs = {\n",
140 | " \"in\": ec_in,\n",
141 | " \"out\": ec_out,\n",
142 | "}"
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "metadata": {},
148 | "source": [
149 | "Connecting the ring with I/O"
150 | ]
151 | },
152 | {
153 | "cell_type": "code",
154 | "execution_count": null,
155 | "metadata": {},
156 | "outputs": [],
157 | "source": [
158 | "routing_roc = 75.0\n",
159 | "\n",
160 | "@gf.cell\n",
161 | "def ring_pass_circuit(\n",
162 | " coupled_ring = coupled_ring,\n",
163 | " ecs = ecs,\n",
164 | ") -> gf.Component:\n",
165 | "\n",
166 | " c = gf.Component()\n",
167 | " ring_ref = c << coupled_ring\n",
168 | " ring_ref.dmovex(- ring_ref.ports[\"o1\"].dcenter[0] + ecs[\"out\"].ports[\"o1\"].dcenter[0])\n",
169 | "\n",
170 | " # Bend spec\n",
171 | "\n",
172 | " routing_bend = partial(\n",
173 | " gf.components.bend_euler,\n",
174 | " radius=routing_roc,\n",
175 | " with_arc_floorplan=True,\n",
176 | " )\n",
177 | "\n",
178 | " # Routing to I/O\n",
179 | "\n",
180 | " [c << ec for ec in ecs.values()]\n",
181 | "\n",
182 | " gf.routing.route_single(\n",
183 | " c,\n",
184 | " ring_ref.ports[\"o2\"],\n",
185 | " ecs[\"in\"].ports[\"o2\"],\n",
186 | " start_straight_length=5.0,\n",
187 | " end_straight_length=5.0,\n",
188 | " cross_section=\"xs_rwg1000\",\n",
189 | " bend=routing_bend,\n",
190 | " straight=\"straight_rwg1000\",\n",
191 | " )\n",
192 | "\n",
193 | " gf.routing.route_single(\n",
194 | " c,\n",
195 | " ring_ref.ports[\"o1\"],\n",
196 | " ecs[\"out\"].ports[\"o2\"],\n",
197 | " start_straight_length=5.0,\n",
198 | " end_straight_length=5.0,\n",
199 | " cross_section=\"xs_rwg1000\",\n",
200 | " bend=routing_bend,\n",
201 | " straight=\"straight_rwg1000\",\n",
202 | " )\n",
203 | "\n",
204 | " c.flatten()\n",
205 | " c.add_port(name=\"o1\", port=ecs[\"in\"].ports[\"o1\"])\n",
206 | " c.add_port(name=\"o2\", port=ecs[\"out\"].ports[\"o1\"])\n",
207 | "\n",
208 | " return c\n",
209 | "\n",
210 | "circuit = ring_pass_circuit()\n",
211 | "circuit"
212 | ]
213 | },
214 | {
215 | "cell_type": "markdown",
216 | "metadata": {},
217 | "source": [
218 | "Assemble on the die outline"
219 | ]
220 | },
221 | {
222 | "cell_type": "code",
223 | "execution_count": null,
224 | "metadata": {},
225 | "outputs": [],
226 | "source": [
227 | "@gf.cell\n",
228 | "def die_assembled(\n",
229 | " chip_layout = chip_layout,\n",
230 | " circuit = circuit,\n",
231 | ") -> gf.Component:\n",
232 | " c = gf.Component()\n",
233 | " c << chip_layout\n",
234 | " c << circuit\n",
235 | " c.add_ports(circuit.ports)\n",
236 | " return c\n",
237 | "\n",
238 | "die = die_assembled()\n",
239 | "die.plot()\n",
240 | "die.show()\n",
241 | "_ = die.write_gds(gdsdir=Path.cwd())"
242 | ]
243 | },
244 | {
245 | "cell_type": "markdown",
246 | "metadata": {},
247 | "source": [
248 | "Recap the port positions for testing"
249 | ]
250 | },
251 | {
252 | "cell_type": "code",
253 | "execution_count": null,
254 | "metadata": {},
255 | "outputs": [],
256 | "source": [
257 | "die.pprint_ports()"
258 | ]
259 | },
260 | {
261 | "cell_type": "markdown",
262 | "metadata": {},
263 | "source": [
264 | "### Clear the gdsfactory cache"
265 | ]
266 | },
267 | {
268 | "cell_type": "code",
269 | "execution_count": null,
270 | "metadata": {},
271 | "outputs": [],
272 | "source": [
273 | "gf.clear_cache()"
274 | ]
275 | },
276 | {
277 | "cell_type": "code",
278 | "execution_count": null,
279 | "metadata": {},
280 | "outputs": [],
281 | "source": []
282 | }
283 | ],
284 | "metadata": {
285 | "kernelspec": {
286 | "display_name": "base",
287 | "language": "python",
288 | "name": "python3"
289 | },
290 | "language_info": {
291 | "codemirror_mode": {
292 | "name": "ipython",
293 | "version": 3
294 | },
295 | "file_extension": ".py",
296 | "mimetype": "text/x-python",
297 | "name": "python",
298 | "nbconvert_exporter": "python",
299 | "pygments_lexer": "ipython3",
300 | "version": "3.11.9"
301 | }
302 | },
303 | "nbformat": 4,
304 | "nbformat_minor": 2
305 | }
306 |
--------------------------------------------------------------------------------
/lnoi400/models.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import json
3 | from collections.abc import Callable
4 | from functools import partial
5 | from pathlib import Path
6 |
7 | import jax.numpy as jnp
8 | import numpy as np
9 | import sax
10 | from gplugins.sax.models import phase_shifter as _phase_shifter
11 | from gplugins.sax.models import straight as __straight
12 | from numpy.polynomial import Polynomial
13 | from numpy.typing import NDArray
14 | from sax.utils import reciprocal
15 |
16 | import lnoi400
17 |
18 | nm = 1e-3
19 |
20 | FloatArray = NDArray[jnp.floating]
21 | Float = float | FloatArray
22 |
23 | ####################
24 | # Utility functions
25 | ####################
26 |
27 |
28 | def get_json_data(
29 | data_tag: str,
30 | ) -> dict:
31 | """Load data from a json structure."""
32 |
33 | path = Path(lnoi400.__file__).parent / "data" / f"{data_tag}.json"
34 | with open(path) as f:
35 | data_dict = json.load(f)
36 | return data_dict
37 |
38 |
39 | def poly_eval_from_json(
40 | wl: np.ndarray,
41 | data_tag: str,
42 | key: str,
43 | ) -> np.ndarray:
44 | """Evaluate a polynomial model for frequency-dependent response
45 | stored in json format."""
46 |
47 | cell_data = get_json_data(data_tag)
48 | if "center_wavelength" in cell_data.keys():
49 | wl0 = cell_data["center_wavelength"]
50 | else:
51 | wl0 = 0.0
52 | poly_coef = cell_data[key]
53 | poly_coef.reverse()
54 | poly_model = Polynomial(poly_coef)
55 | return poly_model(wl - wl0)
56 |
57 |
58 | ################
59 | # Straights
60 | ################
61 |
62 |
63 | def _straight(
64 | *,
65 | wl: Float = 1.55,
66 | length: Float = 10.0,
67 | cross_section: str = "xs_rwg1000",
68 | ) -> sax.SDict:
69 | if not isinstance(cross_section, str):
70 | raise TypeError(
71 | f"""The cross_section parameter should be a string,
72 | received {type(cross_section)} instead."""
73 | )
74 |
75 | if cross_section == "xs_rwg1000":
76 | return __straight(
77 | wl=wl,
78 | length=length,
79 | loss_dB_cm=0.2,
80 | wl0=1.55,
81 | neff=1.8,
82 | ng=2.22,
83 | )
84 |
85 | if cross_section == "xs_swg250":
86 | return __straight(
87 | wl=wl,
88 | length=length,
89 | loss_dB_cm=1.0,
90 | wl0=1.55,
91 | neff=1.44,
92 | ng=1.7,
93 | )
94 |
95 | else:
96 | raise ValueError(
97 | f"""A model for the specified waveguide
98 | cross section {cross_section} is not defined."""
99 | )
100 |
101 |
102 | straight_rwg1000 = partial(_straight, cross_section="xs_rwg1000")
103 | straight_swg250 = partial(_straight, cross_section="xs_swg250")
104 |
105 | ################
106 | # Bends
107 | ################
108 |
109 |
110 | def _2port_poly_model(
111 | *,
112 | wl: Float = 1.55,
113 | data_tag: str,
114 | trans_abs_key: str = "",
115 | trans_phase_key: str = "",
116 | refl_abs_key: str = "",
117 | refl_phase_key: str = "",
118 | ) -> sax.SDict:
119 | s_par = {}
120 | locs = locals()
121 | for key in ["trans_abs_key", "trans_phase_key", "refl_abs_key", "refl_phase_key"]:
122 | if locs[key]:
123 | s_par[key] = poly_eval_from_json(wl, data_tag, locs[key])
124 | else:
125 | s_par[key] = np.zeros_like(wl)
126 |
127 | trans = s_par["trans_abs_key"] * jnp.exp(1j * s_par["trans_phase_key"])
128 | refl = s_par["refl_abs_key"] * jnp.exp(1j * s_par["refl_phase_key"])
129 |
130 | return sax.reciprocal(
131 | {
132 | ("o2", "o1"): trans,
133 | ("o1", "o1"): refl,
134 | ("o2", "o2"): refl,
135 | }
136 | )
137 |
138 |
139 | U_bend_racetrack = partial(
140 | _2port_poly_model,
141 | data_tag="ubend_racetrack",
142 | trans_abs_key="pol_trans_abs",
143 | trans_phase_key="pol_trans_phase",
144 | )
145 |
146 |
147 | ################
148 | # Edge couplers
149 | ################
150 |
151 |
152 | double_linear_inverse_taper = partial(
153 | _2port_poly_model,
154 | data_tag="edge_coupler_double_linear_taper",
155 | trans_abs_key="pol_trans_abs",
156 | trans_phase_key="pol_trans_phase",
157 | refl_abs_key="pol_refl_abs",
158 | refl_phase_key="pol_refl_phase",
159 | )
160 |
161 |
162 | ################
163 | # MMIs
164 | ################
165 |
166 |
167 | def _1in_2out_symmetric_poly_model(
168 | *,
169 | wl: Float = 1.55,
170 | data_tag: str,
171 | trans_abs_key: str = "",
172 | trans_phase_key: str = "",
173 | rin_abs_key: str = "",
174 | rin_phase_key: str = "",
175 | rout_abs_key: str = "",
176 | rout_phase_key: str = "",
177 | rcross_abs_key: str = "",
178 | rcross_phase_key: str = "",
179 | ) -> sax.SDict:
180 | s_par = {}
181 | locs = locals()
182 | for key in [
183 | "trans_abs_key",
184 | "trans_phase_key",
185 | "rin_abs_key",
186 | "rin_phase_key",
187 | "rout_abs_key",
188 | "rout_phase_key",
189 | "rcross_abs_key",
190 | "rcross_phase_key",
191 | ]:
192 | if locs[key]:
193 | s_par[key] = poly_eval_from_json(wl, data_tag, locs[key])
194 | else:
195 | s_par[key] = np.zeros_like(wl)
196 |
197 | trans = s_par["trans_abs_key"] * jnp.exp(1j * s_par["trans_phase_key"])
198 | rin = s_par["rin_abs_key"] * jnp.exp(1j * s_par["rin_phase_key"])
199 | rout = s_par["rout_abs_key"] * jnp.exp(1j * s_par["rout_phase_key"])
200 | rcross = s_par["rcross_abs_key"] * jnp.exp(1j * s_par["rcross_phase_key"])
201 |
202 | return sax.reciprocal(
203 | {
204 | ("o2", "o1"): trans,
205 | ("o3", "o1"): trans,
206 | ("o1", "o1"): rin,
207 | ("o2", "o2"): rout,
208 | ("o3", "o3"): rout,
209 | ("o2", "o3"): rcross,
210 | }
211 | )
212 |
213 |
214 | def _2in_2out_symmetric_poly_model(
215 | *,
216 | wl: Float = 1.55,
217 | data_tag: str,
218 | trans_bar_abs_key: str = "",
219 | trans_bar_phase_key: str = "",
220 | trans_cross_abs_key: str = "",
221 | trans_cross_phase_key: str = "",
222 | refl_self_abs_key: str = "",
223 | refl_self_phase_key: str = "",
224 | refl_cross_abs_key: str = "",
225 | refl_cross_phase_key: str = "",
226 | ) -> sax.SDict:
227 | s_par = {}
228 | locs = locals()
229 | for key in [
230 | "trans_bar_abs_key",
231 | "trans_bar_phase_key",
232 | "trans_cross_abs_key",
233 | "trans_cross_phase_key",
234 | "refl_self_abs_key",
235 | "refl_self_phase_key",
236 | "refl_cross_abs_key",
237 | "refl_cross_phase_key",
238 | ]:
239 | if locs[key]:
240 | s_par[key] = poly_eval_from_json(wl, data_tag, locs[key])
241 | else:
242 | s_par[key] = np.zeros_like(wl)
243 |
244 | bar = s_par["trans_bar_abs_key"] * jnp.exp(1j * s_par["trans_bar_phase_key"])
245 | cross = s_par["trans_cross_abs_key"] * jnp.exp(1j * s_par["trans_cross_phase_key"])
246 | refl_self = s_par["refl_self_abs_key"] * jnp.exp(1j * s_par["refl_self_phase_key"])
247 | refl_cross = s_par["refl_cross_abs_key"] * jnp.exp(
248 | 1j * s_par["refl_cross_phase_key"]
249 | )
250 |
251 | sdict = {
252 | ("o1", "o4"): bar,
253 | ("o2", "o3"): bar,
254 | ("o1", "o3"): cross,
255 | ("o2", "o4"): cross,
256 | ("o1", "o2"): refl_cross,
257 | ("o3", "o4"): refl_cross,
258 | }
259 |
260 | for n in range(1, 5):
261 | port = f"o{n}"
262 | sdict[(port, port)] = refl_self
263 |
264 | return sax.reciprocal(sdict)
265 |
266 |
267 | mmi1x2_optimized1550 = partial(
268 | _1in_2out_symmetric_poly_model,
269 | data_tag="mmi_1x2_optimized_1550",
270 | trans_abs_key="pol_trans_abs",
271 | trans_phase_key="pol_trans_phase",
272 | rin_abs_key="pol_refl_in_abs",
273 | rin_phase_key="pol_refl_in_phase",
274 | rout_abs_key="pol_refl_out_abs",
275 | rout_phase_key="pol_refl_out_phase",
276 | rcross_abs_key="pol_refl_cross_abs",
277 | rcross_phase_key="pol_refl_cross_phase",
278 | )
279 |
280 | mmi2x2_optimized1550 = partial(
281 | _2in_2out_symmetric_poly_model,
282 | data_tag="mmi_2x2_optimized_1550",
283 | trans_bar_abs_key="pol_trans_bar_abs",
284 | trans_bar_phase_key="pol_trans_bar_phase",
285 | trans_cross_abs_key="pol_trans_cross_abs",
286 | trans_cross_phase_key="pol_trans_cross_phase",
287 | refl_self_abs_key="pol_refl_bar_abs",
288 | refl_self_phase_key="pol_refl_bar_phase",
289 | refl_cross_abs_key="pol_refl_cross_abs",
290 | refl_cross_phase_key="pol_refl_cross_phase",
291 | )
292 |
293 | #####################
294 | # Directional coupler
295 | #####################
296 |
297 | directional_coupler_balanced = partial(
298 | _2in_2out_symmetric_poly_model,
299 | data_tag="directional_coupler_balanced",
300 | trans_bar_abs_key="pol_trans_bar_abs",
301 | trans_bar_phase_key="pol_trans_bar_phase",
302 | trans_cross_abs_key="pol_trans_cross_abs",
303 | trans_cross_phase_key="pol_trans_cross_phase",
304 | refl_self_abs_key="pol_refl_bar_abs",
305 | refl_self_phase_key="pol_refl_bar_phase",
306 | refl_cross_abs_key="pol_refl_cross_abs",
307 | refl_cross_phase_key="pol_refl_cross_phase",
308 | )
309 |
310 | ################
311 | # Modulators
312 | ################
313 |
314 |
315 | def eo_phase_shifter(
316 | wl: Float = 1.55,
317 | wl_0: float = 1.55,
318 | length: float = 7500.0,
319 | neff_0: float = 1.85,
320 | ng_0: float = 2.21,
321 | loss: float = 2e-5,
322 | V_pi: float = np.nan,
323 | V_dc: float = 0.0,
324 | ) -> sax.SDict:
325 | # Default V_pi
326 | if np.isnan(V_pi):
327 | V_pi = 2 * 3.3e4 * wl / length / wl_0
328 | v = V_dc / V_pi
329 |
330 | # Effective index at the operation frequency
331 | neff = neff_0 - (ng_0 - neff_0) * (wl - wl_0) / wl_0
332 |
333 | ps = _phase_shifter(
334 | wl=wl,
335 | neff=neff,
336 | voltage=v,
337 | length=length,
338 | loss=loss,
339 | )
340 |
341 | return ps
342 |
343 |
344 | def to_phase_shifter(
345 | wl: Float = 1.55,
346 | wl_0: float = 1.55,
347 | neff_0: float = 1.8,
348 | ng_0: float = 2.22,
349 | loss: float = 2e-5,
350 | heater_length: float = 700.0,
351 | heater_width: float = 1.0,
352 | P_pi: float = np.nan,
353 | R: float = np.nan,
354 | V_dc: float = 0.0,
355 | ):
356 | """Model for a thermal phase shifter.
357 |
358 | Args:
359 | wl: wavelength in um.
360 | wl_0: center wavelength in um.
361 | neff_0: effective index at center wavelength.
362 | ng_0: group index at center wavelength.
363 | loss: propagation loss (dB/um)
364 | heater_length: in um.
365 | heater_width: in um.
366 | P_pi: dissipated power for a pi phase shift (W).
367 | R: resistance (Ohm).
368 | V_dc: static voltage applied to the resistor (V).
369 | """
370 | if np.isnan(R):
371 | R = 25 * heater_length / 700.0 / heater_width
372 | if np.isnan(P_pi):
373 | P_pi = 0.075 * heater_width * wl / wl_0
374 |
375 | # Effective index at the operation frequency
376 | neff = neff_0 - (ng_0 - neff_0) * (wl - wl_0) / wl_0
377 |
378 | P = V_dc**2 / R
379 | deltaphi = P * jnp.pi / P_pi
380 | phase = 2 * jnp.pi * neff * heater_length / wl + deltaphi
381 | amplitude = jnp.asarray(10 ** (-loss * heater_length / 20), dtype=complex)
382 | transmission = amplitude * jnp.exp(1j * phase)
383 | return reciprocal(
384 | {
385 | ("o1", "o2"): transmission,
386 | }
387 | )
388 |
389 |
390 | def mzm_unbalanced(
391 | wl: Float = 1.55,
392 | length_imbalance: float = 100.0,
393 | modulation_length: float = 1000.0,
394 | V_pi: float = np.nan,
395 | P_pi: float = np.nan,
396 | V_dc: float = 0.0,
397 | V_ht: float = 0.0,
398 | **kwargs,
399 | ) -> sax.SDict:
400 | """Model of a Mach-Zehnder modulator with EO and TO phase tuning mechanisms.
401 |
402 | Args:
403 | wl: wavelength in um.
404 | length_imbalance: length difference between the MZ branches, in um.
405 | modulation_length: length of the EO modulation section, in um.
406 | V_pi: voltage dropped on the EO phase modulation section for a pi phase shift (in V).
407 | P_pi: power dissipated in the TO element for a pi phase shift (in W).
408 | V_dc: voltage applied to the EO shifter (in V).
409 | V_ht: voltage applied to the TO shifter (in V).
410 | kwargs: to_phase_shifter keyword arguments.
411 | """
412 | mzm, _ = sax.circuit(
413 | netlist={
414 | "instances": {
415 | "coupler": "mmi",
416 | "top_shifter": "ps_top",
417 | "bot_shifter": "ps_bot",
418 | "dl": "wg_straight",
419 | "top_tops": "tops",
420 | "bot_tops": "dummy_tops",
421 | "splitter": "mmi",
422 | },
423 | "connections": {
424 | "coupler,o2": "top_shifter,o1",
425 | "coupler,o3": "bot_shifter,o1",
426 | "bot_shifter,o2": "dl,o1",
427 | "dl,o2": "bot_tops,o1",
428 | "top_shifter,o2": "top_tops,o1",
429 | "splitter,o2": "top_tops,o2",
430 | "splitter,o3": "bot_tops,o2",
431 | },
432 | "ports": {
433 | "o1": "coupler,o1",
434 | "o2": "splitter,o1",
435 | },
436 | },
437 | models={
438 | "mmi": partial(
439 | mmi1x2_optimized1550,
440 | wl=wl,
441 | ),
442 | "wg_straight": partial(
443 | _straight,
444 | wl=wl,
445 | length=length_imbalance,
446 | cross_section="xs_rwg1000",
447 | ),
448 | "ps_top": partial(
449 | eo_phase_shifter,
450 | wl=wl,
451 | length=modulation_length,
452 | V_dc=V_dc,
453 | V_pi=V_pi,
454 | ),
455 | "ps_bot": partial(
456 | eo_phase_shifter,
457 | wl=wl,
458 | length=modulation_length,
459 | V_dc=-V_dc,
460 | V_pi=V_pi,
461 | ),
462 | "tops": partial(
463 | to_phase_shifter,
464 | wl=wl,
465 | P_pi=P_pi,
466 | V_dc=V_ht,
467 | **kwargs,
468 | ),
469 | "dummy_tops": partial(
470 | to_phase_shifter,
471 | wl=wl,
472 | P_pi=P_pi,
473 | V_dc=0.0,
474 | **kwargs,
475 | ),
476 | },
477 | backend="default",
478 | )
479 | return mzm()
480 |
481 |
482 | ################
483 | # Models Dict
484 | ################
485 |
486 |
487 | def get_models() -> dict[str, Callable[..., sax.SDict]]:
488 | models = {}
489 | for name, func in list(globals().items()):
490 | if name[0] != "_":
491 | if not callable(func):
492 | continue
493 | _func = func
494 | while isinstance(_func, partial):
495 | _func = _func.func
496 | try:
497 | sig = inspect.signature(_func)
498 | except ValueError:
499 | continue
500 | if sig.return_annotation == sax.SDict:
501 | models[name] = func
502 | return models
503 |
504 |
505 | if __name__ == "__main__":
506 | pass
507 |
--------------------------------------------------------------------------------
/lnoi400/cells.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 |
3 | import gdsfactory as gf
4 | import numpy as np
5 | from gdsfactory.routing import route_quad
6 | from gdsfactory.typings import ComponentSpec, CrossSectionSpec
7 |
8 | from lnoi400.spline import (
9 | bend_S_spline,
10 | bend_S_spline_varying_width,
11 | spline_clamped_path,
12 | )
13 | from lnoi400.tech import LAYER, xs_uni_cpw
14 |
15 | ################
16 | # Straights
17 | ################
18 |
19 |
20 | @gf.cell
21 | def _straight(
22 | length: float = 10.0,
23 | cross_section: CrossSectionSpec = "xs_rwg1000",
24 | **kwargs,
25 | ) -> gf.Component:
26 | return gf.components.straight(
27 | length=length,
28 | cross_section=cross_section,
29 | **kwargs,
30 | )
31 |
32 |
33 | @gf.cell
34 | def straight_rwg1000(length: float = 10.0, **kwargs) -> gf.Component:
35 | """Straight single-mode waveguide."""
36 | if "cross_section" not in kwargs:
37 | kwargs["cross_section"] = "xs_rwg1000"
38 | return _straight(
39 | length=length,
40 | **kwargs,
41 | )
42 |
43 |
44 | @gf.cell
45 | def straight_rwg3000(length: float = 10.0, **kwargs) -> gf.Component:
46 | """Straight multimode waveguide."""
47 | if "cross_section" not in kwargs:
48 | kwargs["cross_section"] = "xs_rwg3000"
49 | return _straight(
50 | length=length,
51 | **kwargs,
52 | )
53 |
54 |
55 | ##########
56 | # Bends
57 | ##########
58 |
59 |
60 | @gf.cell
61 | def L_turn_bend(
62 | radius: float = 80.0,
63 | p: float = 1.0,
64 | with_arc_floorplan: bool = True,
65 | cross_section: CrossSectionSpec = "xs_rwg1000",
66 | **kwargs,
67 | ) -> gf.Component:
68 | """
69 | A 90-degrees bend following an Euler path, with linearly-varying curvature
70 | (increasing and decreasing).
71 | """
72 |
73 | npoints = int(np.round(200 * radius / 80.0))
74 | angle = 90.0
75 |
76 | return gf.components.bend_euler(
77 | radius=radius,
78 | angle=angle,
79 | p=p,
80 | with_arc_floorplan=with_arc_floorplan,
81 | npoints=npoints,
82 | cross_section=cross_section,
83 | **kwargs,
84 | )
85 |
86 |
87 | # TODO: inquire about meaning of bend_points_distance in relation with Euler bends
88 |
89 |
90 | @gf.cell
91 | def U_bend_racetrack(
92 | v_offset: float = 90.0,
93 | p: float = 1.0,
94 | with_arc_floorplan: bool = True,
95 | cross_section: CrossSectionSpec = "xs_rwg3000",
96 | **kwargs,
97 | ) -> gf.Component:
98 | """A U-bend with fixed cross-section and dimensions, suitable for building a low-loss racetrack resonator."""
99 |
100 | radius = 0.5 * v_offset
101 |
102 | npoints = int(np.round(600 * radius / 90.0))
103 | angle = 180.0
104 |
105 | return gf.components.bend_euler(
106 | radius=radius,
107 | angle=angle,
108 | p=p,
109 | with_arc_floorplan=with_arc_floorplan,
110 | npoints=npoints,
111 | cross_section=cross_section,
112 | **kwargs,
113 | )
114 |
115 |
116 | @gf.cell
117 | def S_bend_vert(
118 | v_offset: float = 25.0,
119 | h_extent: float = 100.0,
120 | dx_straight: float = 5.0,
121 | cross_section: CrossSectionSpec = "xs_rwg1000",
122 | ) -> gf.Component:
123 | """A spline bend that bridges a vertical displacement."""
124 |
125 | if np.abs(v_offset) < 10.0:
126 | raise ValueError(
127 | f"The vertical distance bridged by the S-bend ({v_offset}) is too small."
128 | )
129 |
130 | if np.abs(h_extent / v_offset) < 3.5 or h_extent < 90.0:
131 | raise ValueError(
132 | f"The bend would be too tight. Increase h_extent from its current value of {h_extent}."
133 | )
134 |
135 | S_bend = gf.components.extend_ports(
136 | bend_S_spline(
137 | size=(h_extent, v_offset),
138 | cross_section=cross_section,
139 | npoints=int(np.round(2.5 * h_extent)),
140 | path_method=spline_clamped_path,
141 | ),
142 | length=dx_straight,
143 | cross_section=cross_section,
144 | )
145 |
146 | bend_cell = gf.Component()
147 | bend_ref = bend_cell << S_bend
148 | bend_ref.dmove(bend_ref.ports["o1"].dcenter, (0.0, 0.0))
149 | bend_cell.add_port(name="o1", port=bend_ref.ports["o1"])
150 | bend_cell.add_port(name="o2", port=bend_ref.ports["o2"])
151 | bend_cell.flatten()
152 |
153 | return bend_cell
154 |
155 |
156 | ################
157 | # MMIs
158 | ################
159 |
160 |
161 | @gf.cell
162 | def mmi1x2_optimized1550(
163 | width_mmi: float = 6.0,
164 | length_mmi: float = 26.75,
165 | width_taper: float = 1.5,
166 | length_taper: float = 25.0,
167 | port_ratio: float = 0.55,
168 | cross_section: CrossSectionSpec = "xs_rwg1000",
169 | **kwargs,
170 | ) -> gf.Component:
171 | """MMI1x2 with layout optimized for maximum transmission at 1550 nm."""
172 |
173 | gap_mmi = (
174 | port_ratio * width_mmi - width_taper
175 | ) # The port ratio is defined as the ratio between the waveguides separation and the MMI width.
176 |
177 | return gf.components.mmi1x2(
178 | width_mmi=width_mmi,
179 | length_mmi=length_mmi,
180 | gap_mmi=gap_mmi,
181 | length_taper=length_taper,
182 | width_taper=width_taper,
183 | cross_section=cross_section,
184 | **kwargs,
185 | )
186 |
187 |
188 | @gf.cell
189 | def mmi2x2optimized1550(
190 | width_mmi: float = 5.0,
191 | length_mmi: float = 76.5,
192 | width_taper: float = 1.5,
193 | length_taper: float = 25.0,
194 | port_ratio: float = 0.7,
195 | cross_section: CrossSectionSpec = "xs_rwg1000",
196 | **kwargs,
197 | ) -> gf.Component:
198 | """MMI2x2 with layout optimized for maximum transmission at 1550 nm."""
199 |
200 | gap_mmi = (
201 | port_ratio * width_mmi - width_taper
202 | ) # The port ratio is defined as the ratio between the waveguides separation and the MMI width.
203 |
204 | return gf.components.mmi2x2(
205 | width_mmi=width_mmi,
206 | length_mmi=length_mmi,
207 | gap_mmi=gap_mmi,
208 | length_taper=length_taper,
209 | width_taper=width_taper,
210 | cross_section=cross_section,
211 | **kwargs,
212 | )
213 |
214 |
215 | mmi2x2_optimized1550 = mmi2x2optimized1550
216 |
217 | #####################
218 | # Directional coupler
219 | #####################
220 |
221 |
222 | @gf.cell
223 | def directional_coupler_balanced(
224 | io_wg_sep: float = 30.6,
225 | sbend_length: float = 58,
226 | central_straight_length: float = 16.92,
227 | coupl_wg_sep: float = 0.8,
228 | coup_wg_width: float = 0.8,
229 | cross_section_io: CrossSectionSpec = "xs_rwg1000",
230 | ) -> gf.Component:
231 | """Returns a 50-50 directional coupler. Default parameters give a 50/50 splitting at 1550 nm.
232 |
233 | Args:
234 | io_wg_sep: Separation of the two straights at the input/output, top-to-top.
235 | sbend_length: length of the s-bend part.
236 | central_straight_length: length of the coupling region.
237 | coupl_wg_sep: Distance between two waveguides in the coupling region (side to side).
238 | cross_section_io: cross section spec at the i/o (must be defined in tech.py).
239 | coup_wg_width: waveguide width at the coupling section.
240 | """
241 |
242 | s0 = gf.Section(
243 | width=coup_wg_width,
244 | offset=0,
245 | layer="LN_RIDGE",
246 | name="_default",
247 | port_names=("o1", "o2"),
248 | )
249 | s1 = gf.Section(width=10.0, offset=0, layer="LN_SLAB", name="slab", simplify=0.03)
250 | cross_section_coupling = gf.CrossSection(sections=[s0, s1])
251 |
252 | cross_section_io = gf.get_cross_section(cross_section_io)
253 |
254 | s_height = (
255 | io_wg_sep - coupl_wg_sep - coup_wg_width
256 | ) / 2 # take into account the width of the waveguide
257 | size = (sbend_length, s_height)
258 |
259 | # s-bend settings
260 | settings_s_bend = {
261 | "size": size,
262 | "cross_section1": cross_section_coupling,
263 | "cross_section2": cross_section_io,
264 | "npoints": 201,
265 | }
266 | dc = gf.Component()
267 | # top right branch
268 | c_tr = dc << bend_S_spline_varying_width(**settings_s_bend)
269 | c_tr.dmove(
270 | c_tr.ports["o1"].dcenter,
271 | (central_straight_length / 2, 0.5 * (coupl_wg_sep + coup_wg_width)),
272 | )
273 |
274 | # bottom right branch
275 | c_br = dc << bend_S_spline_varying_width(**settings_s_bend)
276 | c_br.dmirror_y()
277 | c_br.dmove(
278 | c_br.ports["o1"].dcenter,
279 | (central_straight_length / 2, -0.5 * (coupl_wg_sep + coup_wg_width)),
280 | )
281 |
282 | # central waveguides
283 | straight_center_up = dc << gf.components.straight(
284 | length=central_straight_length, cross_section=cross_section_coupling
285 | )
286 | straight_center_up.connect("o2", c_tr.ports["o1"])
287 | straight_center_down = dc << gf.components.straight(
288 | length=central_straight_length, cross_section=cross_section_coupling
289 | )
290 | straight_center_down.connect("o2", c_br.ports["o1"])
291 |
292 | # top left branch
293 | c_tl = dc << bend_S_spline_varying_width(**settings_s_bend)
294 | c_tl.dmirror_x()
295 | c_tl.dmove(c_tl.ports["o1"].dcenter, straight_center_up.ports["o1"].dcenter)
296 |
297 | # bottom left branch
298 | c_bl = dc << bend_S_spline_varying_width(**settings_s_bend)
299 | c_bl.dmirror_x()
300 | c_bl.dmirror_y()
301 | c_bl.dmove(c_bl.ports["o1"].dcenter, straight_center_down.ports["o1"].dcenter)
302 |
303 | # Expose the ports
304 | exposed_ports = [
305 | ("o1", c_bl.ports["o2"]),
306 | ("o2", c_tl.ports["o2"]),
307 | ("o3", c_tr.ports["o2"]),
308 | ("o4", c_br.ports["o2"]),
309 | ]
310 |
311 | [dc.add_port(name=name, port=port) for name, port in exposed_ports]
312 | return dc
313 |
314 |
315 | ################
316 | # Edge couplers
317 | ################
318 |
319 |
320 | @gf.cell
321 | def double_linear_inverse_taper(
322 | cross_section_start: CrossSectionSpec = "xs_swg250",
323 | cross_section_end: CrossSectionSpec = "xs_rwg1000",
324 | lower_taper_length: float = 120.0,
325 | lower_taper_end_width: float = 2.05,
326 | upper_taper_start_width: float = 0.25,
327 | upper_taper_length: float = 240.0,
328 | slab_removal_width: float = 20.0,
329 | input_ext: float = 0.0,
330 | ) -> gf.Component:
331 | """Inverse taper with two layers, starting from a wire waveguide at the facet
332 | and transitioning to a rib waveguide. The tapering profile is linear in both layers."""
333 |
334 | lower_taper_start_width = gf.get_cross_section(cross_section_start).width
335 | upper_taper_end_width = gf.get_cross_section(cross_section_end).width
336 |
337 | xs_taper_lower_end = partial(
338 | gf.cross_section.strip,
339 | width=lower_taper_start_width
340 | + (lower_taper_end_width - lower_taper_start_width)
341 | * (1 + upper_taper_length / lower_taper_length),
342 | layer="LN_SLAB",
343 | )
344 |
345 | xs_taper_upper_start = partial(
346 | gf.cross_section.strip, layer=LAYER.LN_RIDGE, width=upper_taper_start_width
347 | )
348 |
349 | xs_taper_upper_end = partial(xs_taper_upper_start, width=upper_taper_end_width)
350 |
351 | taper_lower = gf.components.taper_cross_section(
352 | cross_section1=cross_section_start,
353 | cross_section2=xs_taper_lower_end,
354 | length=lower_taper_length + upper_taper_length,
355 | linear=True,
356 | )
357 |
358 | taper_upper = gf.components.taper_cross_section(
359 | cross_section1=xs_taper_upper_start,
360 | cross_section2=xs_taper_upper_end,
361 | length=upper_taper_length,
362 | linear=True,
363 | )
364 |
365 | if input_ext:
366 | straight_ext = gf.components.straight(
367 | cross_section=cross_section_start,
368 | length=input_ext,
369 | )
370 |
371 | # Place the two tapers on the different layers
372 |
373 | double_taper = gf.Component()
374 | if input_ext:
375 | sref = double_taper << straight_ext
376 | sref.dmovex(-input_ext)
377 | ltref = double_taper << taper_lower
378 | utref = double_taper << taper_upper
379 | utref.dmovex(lower_taper_length)
380 |
381 | # Define the input and output optical ports
382 |
383 | double_taper.add_port(
384 | port=sref.ports["o1"]
385 | ) if input_ext else double_taper.add_port(port=ltref.ports["o1"])
386 | double_taper.add_port(port=utref.ports["o2"])
387 |
388 | # Place the tone inversion box for the slab etch
389 |
390 | if slab_removal_width:
391 | bn = gf.components.rectangle(
392 | size=(
393 | double_taper.ports["o2"].dcenter[0]
394 | - double_taper.ports["o1"].dcenter[0],
395 | slab_removal_width,
396 | ),
397 | centered=True,
398 | layer=LAYER.SLAB_NEGATIVE,
399 | )
400 | bnref = double_taper << bn
401 | bnref.dmovex(
402 | origin=bnref.dxmin,
403 | destination=-input_ext,
404 | )
405 | double_taper.flatten()
406 |
407 | return double_taper
408 |
409 |
410 | ###################
411 | # GSG bonding pad
412 | ###################
413 |
414 |
415 | @gf.cell
416 | def CPW_pad_linear(
417 | start_width: float = 80.0,
418 | length_straight: float = 10.0,
419 | length_tapered: float = 190.0,
420 | cross_section: CrossSectionSpec = "xs_uni_cpw",
421 | ) -> gf.Component:
422 | """RF access line for high-frequency GSG probes. The probe pad maintains a
423 | fixed gap/central conductor ratio across its length, to achieve a good
424 | impedance matching."""
425 |
426 | xs_cpw = gf.get_cross_section(cross_section)
427 |
428 | # Extract the CPW cross sectional parameters
429 |
430 | sections = xs_cpw.sections
431 | signal_section = [s for s in sections if s.name == "signal"][0]
432 | ground_section = [s for s in sections if s.name == "ground_top"][0]
433 | end_width = signal_section.width
434 | ground_planes_width = ground_section.width
435 | end_gap = ground_section.offset - 0.5 * (end_width + ground_planes_width)
436 | aspect_ratio = end_width / (end_width + 2 * end_gap)
437 |
438 | # Pad elements generation
439 |
440 | pad = gf.Component()
441 |
442 | start_gap = 0.5 * (aspect_ratio ** (-1) - 1) * start_width
443 |
444 | central_conductor_shape = [
445 | (0.0, start_width / 2.0),
446 | (length_straight, start_width / 2.0),
447 | (length_straight + length_tapered, end_width / 2.0),
448 | (length_straight + length_tapered, -end_width / 2.0),
449 | (length_straight, -start_width / 2.0),
450 | (0.0, -start_width / 2.0),
451 | ]
452 |
453 | ground_plane_shape = [
454 | (0.0, start_width / 2.0 + start_gap),
455 | (length_straight, start_width / 2.0 + start_gap),
456 | (length_straight + length_tapered, end_width / 2.0 + end_gap),
457 | (
458 | length_straight + length_tapered,
459 | end_width / 2.0 + end_gap + ground_planes_width,
460 | ),
461 | (0.0, end_width / 2.0 + end_gap + ground_planes_width),
462 | ]
463 |
464 | bottom_ground_shape = [(p[0], -p[1]) for p in ground_plane_shape]
465 |
466 | pad.add_polygon(central_conductor_shape, layer="TL")
467 | pad.add_polygon(ground_plane_shape, layer="TL")
468 | pad.add_polygon(bottom_ground_shape, layer="TL")
469 |
470 | # Ports definition
471 |
472 | pad.add_port(
473 | name="e1",
474 | center=(length_straight, 0.0),
475 | width=start_width,
476 | orientation=180.0,
477 | port_type="electrical",
478 | layer="TL",
479 | )
480 |
481 | pad.add_port(
482 | name="e2",
483 | center=(length_straight + length_tapered, 0.0),
484 | width=end_width,
485 | orientation=0.0,
486 | port_type="electrical",
487 | layer="TL",
488 | )
489 |
490 | return pad
491 |
492 |
493 | ####################
494 | # Transmission lines
495 | ####################
496 |
497 |
498 | @gf.cell()
499 | def uni_cpw_straight(
500 | length: float = 1000.0,
501 | cross_section: CrossSectionSpec = "xs_uni_cpw",
502 | signal_width: float = 10.0,
503 | gap_width: float = 4.0,
504 | ground_planes_width: float = 250.0,
505 | bondpad: ComponentSpec = "CPW_pad_linear",
506 | ) -> gf.Component:
507 | """A CPW transmission line for microwaves, with a uniform cross section."""
508 |
509 | cpw_xs = gf.get_cross_section(
510 | cross_section,
511 | central_conductor_width=signal_width,
512 | gap=gap_width,
513 | ground_planes_width=ground_planes_width,
514 | )
515 | cpw = gf.Component()
516 | bp = gf.get_component(bondpad, cross_section=cpw_xs)
517 |
518 | tl = cpw << gf.components.straight(length=length, cross_section=cpw_xs)
519 | bp1 = cpw << bp
520 | bp2 = cpw << bp
521 |
522 | bp1.connect("e2", tl.ports["e1"])
523 | bp2.dmirror()
524 | bp2.connect("e2", tl.ports["e2"])
525 |
526 | cpw.add_ports(tl.ports)
527 | cpw.add_port(
528 | name="bp1",
529 | port=bp1.ports["e1"],
530 | )
531 | cpw.add_port(
532 | name="bp2",
533 | port=bp2.ports["e1"],
534 | )
535 | cpw.flatten()
536 |
537 | return cpw
538 |
539 |
540 | @gf.cell()
541 | def trail_cpw(
542 | length: float = 1000.0,
543 | signal_width: float = 21,
544 | gap_width: float = 4,
545 | th: float = 1.5,
546 | tl: float = 44.7,
547 | tw: float = 7.0,
548 | tt: float = 1.5,
549 | tc: float = 5.0,
550 | ground_planes_width: float = 180.0,
551 | rounding_radius: float = 0.5,
552 | bondpad: ComponentSpec = "CPW_pad_linear",
553 | cross_section: CrossSectionSpec = xs_uni_cpw,
554 | ) -> gf.Component:
555 | """A CPW transmission line with periodic T-rails on all electrodes."""
556 |
557 | num_cells = np.floor(length / (tl + tc))
558 | gap_width_corrected = gap_width + 2 * th + 2 * tt # total gap width with T-rails
559 |
560 | # redefine cross section to include T-rails
561 | xs_cpw_trail = partial(
562 | cross_section,
563 | central_conductor_width=signal_width,
564 | gap=gap_width_corrected,
565 | ground_planes_width=ground_planes_width,
566 | )
567 |
568 | cpw = gf.Component()
569 | bp = gf.get_component(bondpad, cross_section=xs_cpw_trail)
570 | strght = cpw << gf.components.straight(length=length, cross_section=xs_cpw_trail)
571 | bp1 = cpw << bp
572 | bp2 = cpw << bp
573 | bp1.connect("e2", strght.ports["e1"])
574 | bp2.dmirror()
575 | bp2.connect("e2", strght.ports["e2"])
576 | cpw.add_ports(strght.ports)
577 |
578 | cpw.add_port(
579 | name="bp1",
580 | port=bp1.ports["e1"],
581 | )
582 | cpw.add_port(
583 | name="bp2",
584 | port=bp2.ports["e1"],
585 | )
586 |
587 | # Initiate T-rail polygon element. Create a bit more to ensure round corners close to electrodes
588 | trailpol = gf.kdb.DPolygon(
589 | [
590 | (tl, signal_width / 2),
591 | (tl, signal_width / 2 - tt),
592 | (0, signal_width / 2 - tt),
593 | (0, signal_width / 2),
594 | (tl / 2 - tw / 2, signal_width / 2),
595 | (tl / 2 - tw / 2, signal_width / 2 + th),
596 | (0, signal_width / 2 + th),
597 | (0, signal_width / 2 + th + tt),
598 | (tl, signal_width / 2 + th + tt),
599 | (tl, signal_width / 2 + th),
600 | (tl / 2 + tw / 2, signal_width / 2 + th),
601 | (tl / 2 + tw / 2, signal_width / 2),
602 | ]
603 | )
604 |
605 | # Create T-rail component
606 | trailcomp = gf.Component()
607 | _ = trailcomp.add_polygon(trailpol, layer=cross_section().layer)
608 |
609 | # Apply roc to the T-rail corners
610 | trailround = gf.Component()
611 | rinner = rounding_radius * 1000 # The circle radius of inner corners (in nm).
612 | router = rounding_radius * 1000 # The circle radius of outer corners (in nm).
613 | n = 30 # The number of points per full circle.
614 |
615 | for layer, polygons in trailcomp.get_polygons().items():
616 | for p in polygons:
617 | p_round = p.round_corners(rinner, router, n)
618 | trailround.add_polygon(p_round, layer=layer)
619 |
620 | # Create T-rail unit cell
621 | trail_uc = gf.Component()
622 | inc_t1 = trail_uc << trailround
623 | inc_t2 = trail_uc << trailround
624 | inc_t2.dmovey(gap_width_corrected - th)
625 | inc_t3 = trail_uc << trailround
626 | inc_t3.dmovey(-signal_width - th)
627 | inc_t4 = trail_uc << trailround
628 | inc_t4.dmovey(-signal_width - gap_width_corrected)
629 |
630 | # Place T-rails symmetrically w/r to bondpads
631 |
632 | dl_tr = 0.5 * (length - num_cells * tl - (num_cells - 1) * tc)
633 |
634 | [ref.dmovex(dl_tr) for ref in (inc_t1, inc_t2, inc_t3, inc_t4)]
635 |
636 | # Duplicate cell
637 | cpw.add_ref(
638 | trail_uc,
639 | columns=num_cells,
640 | rows=1,
641 | column_pitch=tl + tc,
642 | )
643 |
644 | cpw.flatten()
645 |
646 | return cpw
647 |
648 |
649 | ###################
650 | # Thermal shifters
651 | ###################
652 |
653 |
654 | @gf.cell
655 | def heater_resistor(
656 | path: gf.path.Path | None = None,
657 | width: float = 0.9,
658 | offset: float = 0.0,
659 | ) -> gf.Component:
660 | """A resistive wire used as a low-frequency phase shifter, exploiting
661 | the thermo-optical effect."""
662 |
663 | if not path:
664 | path = gf.path.straight(length=150.0)
665 |
666 | xs = gf.get_cross_section("xs_ht_wire", width=width, offset=offset)
667 | c = path.extrude(xs)
668 |
669 | return c
670 |
671 |
672 | @gf.cell
673 | def heater_straight_single(
674 | length: float = 150.0,
675 | width: float = 0.9,
676 | offset: float = 0.0,
677 | port_contact_width_ratio: float = 3.0,
678 | pad_size: tuple[float, float] = (100.0, 100.0),
679 | pad_pitch: float | None = None,
680 | pad_vert_offset: float = 10.0,
681 | ) -> gf.Component:
682 | """A straight resistive wire used as a low-frequency phase shifter,
683 | exploiting the thermo-optical effect. The heater is terminated by wide pads
684 | for probing or bonding."""
685 |
686 | if pad_vert_offset <= 0:
687 | raise ValueError(
688 | "pad_vert_offset must be a positive number,"
689 | + f"received {pad_vert_offset}."
690 | )
691 |
692 | if port_contact_width_ratio <= 0:
693 | raise ValueError(
694 | "port_contact_width_ratio must be a positive number,"
695 | + f"received {port_contact_width_ratio}."
696 | )
697 |
698 | if not pad_pitch:
699 | pad_pitch = length
700 |
701 | c = gf.Component()
702 | bondpads = gf.components.pad_array(
703 | pad=gf.components.pad,
704 | size=pad_size,
705 | column_pitch=pad_pitch,
706 | row_pitch=pad_pitch,
707 | columns=2,
708 | port_orientation=-90.0,
709 | layer=LAYER.HT,
710 | )
711 | bps = c << bondpads
712 |
713 | ht = heater_resistor(
714 | path=gf.path.straight(length),
715 | width=width,
716 | offset=offset,
717 | )
718 |
719 | # Place the ports along the edge of the wire
720 | for p in ht.ports:
721 | if p.orientation == 0.0:
722 | p.dcenter = (p.dcenter[0] - 0.5 * p.dwidth, p.dcenter[1] + 0.5 * width)
723 | if p.orientation == 180.0:
724 | p.dcenter = (p.dcenter[0] + 0.5 * p.dwidth, p.dcenter[1] + 0.5 * width)
725 | p.orientation = 90.0
726 |
727 | ht_ref = c << ht
728 |
729 | bps.dcenter = ht_ref.dcenter
730 | bps.dymin = ht_ref.dymax + pad_vert_offset
731 |
732 | port_contact_width = port_contact_width_ratio * width
733 | ht.ports["e1"].dx += 0.5 * (port_contact_width - width)
734 | ht.ports["e2"].dx -= 0.5 * (port_contact_width - width)
735 |
736 | routing_params = {
737 | "width2": port_contact_width,
738 | "layer": LAYER.HT,
739 | }
740 |
741 | # Connect pads and heater wire
742 | _ = route_quad(
743 | c,
744 | port1=bps.ports["e11"],
745 | port2=ht.ports["e1"],
746 | **routing_params,
747 | )
748 |
749 | _ = route_quad(
750 | c,
751 | port1=bps.ports["e12"],
752 | port2=ht.ports["e2"],
753 | **routing_params,
754 | )
755 |
756 | c.add_port(
757 | name="ht_start",
758 | port=ht.ports["e1"],
759 | )
760 |
761 | c.add_port(
762 | name="ht_end",
763 | port=ht.ports["e2"],
764 | )
765 |
766 | c.add_port(
767 | name="e1",
768 | port=bps.ports["e11"],
769 | )
770 | c.add_port(
771 | name="e2",
772 | port=bps.ports["e12"],
773 | )
774 |
775 | c.flatten()
776 |
777 | return c
778 |
779 |
780 | ###############
781 | # Modulators
782 | ###############
783 |
784 |
785 | @gf.cell
786 | def eo_phase_shifter(
787 | rib_core_width_modulator: float = 2.5,
788 | taper_length: float = 100.0,
789 | modulation_length: float = 7500.0,
790 | rf_central_conductor_width: float = 10.0,
791 | rf_ground_planes_width: float = 180.0,
792 | rf_gap: float = 4.0,
793 | cpw_cell: ComponentSpec = uni_cpw_straight,
794 | draw_cpw: bool = True,
795 | ) -> gf.Component:
796 | """Phase shifter based on the Pockels effect. The waveguide is located
797 | within the gap of a CPW transmission line."""
798 | ps = gf.Component()
799 | xs_modulator = gf.get_cross_section("xs_rwg1000", width=rib_core_width_modulator)
800 | wg_taper = gf.components.taper_cross_section(
801 | cross_section1="xs_rwg1000", cross_section2=xs_modulator, length=taper_length
802 | )
803 | wg_phase_modulation = gf.components.straight(
804 | length=modulation_length - 2 * taper_length, cross_section=xs_modulator
805 | )
806 |
807 | taper_1 = ps << wg_taper
808 | wg_pm = ps << wg_phase_modulation
809 | taper_2 = ps << wg_taper
810 | taper_2.dmirror_x()
811 | wg_pm.connect("o1", taper_1.ports["o2"])
812 | taper_2.dmirror_x()
813 | taper_2.connect("o2", wg_pm.ports["o2"])
814 |
815 | for name, port in [
816 | ("o1", taper_1.ports["o1"]),
817 | ("o2", taper_2.ports["o1"]),
818 | ]:
819 | ps.add_port(name=name, port=port)
820 |
821 | # Add the transmission line
822 |
823 | if draw_cpw:
824 | xs_cpw = gf.partial(
825 | xs_uni_cpw,
826 | central_conductor_width=rf_central_conductor_width,
827 | ground_planes_width=rf_ground_planes_width,
828 | gap=rf_gap,
829 | )
830 | tl = ps << cpw_cell(
831 | length=modulation_length,
832 | cross_section=xs_cpw,
833 | gap_width=rf_gap,
834 | signal_width=rf_central_conductor_width,
835 | ground_planes_width=rf_ground_planes_width,
836 | )
837 |
838 | gap_eff = rf_gap + 2 * np.sum(
839 | [tl.cell.settings[key] for key in ("tt", "th") if key in tl.cell.settings]
840 | )
841 |
842 | tl.dmove(
843 | tl.ports["e1"].dcenter,
844 | (0.0, -0.5 * rf_central_conductor_width - 0.5 * gap_eff),
845 | )
846 |
847 | for name, port in [
848 | ("e1", tl.ports["bp1"]),
849 | ("e2", tl.ports["bp2"]),
850 | ]:
851 | ps.add_port(name=name, port=port)
852 |
853 | ps.flatten()
854 |
855 | return ps
856 |
857 |
858 | @gf.cell
859 | def eo_phase_shifter_high_speed(**kwargs) -> gf.Component:
860 | """High-speed phase shifter based on the Pockels effect. The waveguide is located
861 | within the gap of a CPW transmission line.
862 | Note: The base variant (eo_phase_shifter) uses a default central conductor width of 10.0,
863 | while this high-speed variant explicitly passes 21.0 for rf_central_conductor_width to achieve the desired high-speed properties.
864 | Pass the parameter set of eo_phase_shifter to modify.
865 | """
866 | kwargs.setdefault("rf_central_conductor_width", 21.0)
867 | kwargs.setdefault("cpw_cell", trail_cpw)
868 | ps = eo_phase_shifter(**kwargs)
869 | ps.info["additional_settings"] = dict(ps.settings)
870 | return ps
871 |
872 |
873 | @gf.cell
874 | def _mzm_interferometer(
875 | splitter: ComponentSpec = "mmi1x2_optimized1550",
876 | taper_length: float = 100.0,
877 | rib_core_width_modulator: float = 2.5,
878 | modulation_length: float = 7500.0,
879 | length_imbalance: float = 100.0,
880 | bias_tuning_section_length: float = 750.0,
881 | sbend_large_size: tuple[float, float] = (200.0, 50.0),
882 | sbend_small_size: tuple[float, float] = (200.0, -45.0),
883 | sbend_small_straight_extend: float = 5.0,
884 | lbend_tune_arm_reff: float = 75.0,
885 | lbend_combiner_reff: float = 80.0,
886 | ) -> gf.Component:
887 | interferometer = gf.Component()
888 |
889 | sbend_large = S_bend_vert(
890 | v_offset=sbend_large_size[1], h_extent=sbend_large_size[0], dx_straight=5.0
891 | )
892 |
893 | sbend_small = S_bend_vert(
894 | v_offset=sbend_small_size[1],
895 | h_extent=sbend_small_size[0],
896 | dx_straight=sbend_small_straight_extend,
897 | )
898 |
899 | def branch_top():
900 | bt = gf.Component()
901 | sbend_1 = bt << sbend_large
902 | sbend_2 = bt << sbend_small
903 | pm = bt << eo_phase_shifter(
904 | rib_core_width_modulator=rib_core_width_modulator,
905 | modulation_length=modulation_length,
906 | taper_length=taper_length,
907 | draw_cpw=False,
908 | )
909 | sbend_3 = bt << sbend_small
910 | sbend_2.connect("o1", sbend_1.ports["o2"])
911 | pm.connect("o1", sbend_2.ports["o2"])
912 | sbend_3.dmirror_x()
913 | sbend_3.connect("o1", pm.ports["o2"])
914 |
915 | for name, port in [
916 | ("o1", sbend_1.ports["o1"]),
917 | ("o2", sbend_3.ports["o2"]),
918 | ("taper_start", pm.ports["o1"]),
919 | ]:
920 | bt.add_port(name=name, port=port)
921 | bt.flatten()
922 |
923 | return bt
924 |
925 | def branch_tune_short(straight_unbalance: float = 0.0):
926 | arm = gf.Component()
927 | lbend = L_turn_bend(radius=lbend_tune_arm_reff)
928 | straight_y = gf.components.straight(
929 | length=20.0 + straight_unbalance, cross_section="xs_rwg1000"
930 | )
931 | straight_x = gf.components.straight(
932 | length=bias_tuning_section_length, cross_section="xs_rwg1000"
933 | )
934 | symbol_to_component = {
935 | "b": (lbend, "o1", "o2"),
936 | "L": (straight_y, "o1", "o2"),
937 | "B": (lbend, "o2", "o1"),
938 | "_": (straight_x, "o1", "o2"),
939 | }
940 | sequence = "bLB_!b!L"
941 | arm = gf.components.component_sequence(
942 | sequence=sequence,
943 | ports_map={"phase_tuning_segment_start": ("_1", "o1")},
944 | symbol_to_component=symbol_to_component,
945 | )
946 |
947 | arm.add_port(port=arm.ports["phase_tuning_segment_start"])
948 | arm.flatten()
949 | return arm
950 |
951 | def branch_tune_long(straight_unbalance):
952 | return partial(branch_tune_short, straight_unbalance=straight_unbalance)()
953 |
954 | splt = gf.get_component(splitter)
955 |
956 | # Uniformly handle the cases of a 1x2 or 2x2 MMI
957 |
958 | if len(splt.ports) == 4:
959 | out_top = splt.ports["o3"]
960 | out_bottom = splt.ports["o4"]
961 | elif len(splt.ports) == 3:
962 | out_top = splt.ports["o2"]
963 | out_bottom = splt.ports["o3"]
964 | else:
965 | raise ValueError(f"Splitter cell {splitter} not supported.")
966 |
967 | def combiner_section():
968 | comb_section = gf.Component()
969 | lbend_combiner = L_turn_bend(radius=lbend_combiner_reff)
970 | lbend_top = comb_section << lbend_combiner
971 | lbend_bottom = comb_section << lbend_combiner
972 | lbend_bottom.dmirror_y()
973 | combiner = comb_section << splt
974 | lbend_top.connect("o1", out_top)
975 | lbend_bottom.connect("o1", out_bottom)
976 |
977 | # comb_section.flatten()
978 |
979 | exposed_ports = [
980 | ("o2", lbend_top.ports["o2"]),
981 | ("o1", combiner.ports["o1"]),
982 | ("o3", lbend_bottom.ports["o2"]),
983 | ]
984 |
985 | if "2x2" in splitter:
986 | exposed_ports.append(
987 | ("in2", combiner.ports["o2"]),
988 | )
989 |
990 | for name, port in exposed_ports:
991 | comb_section.add_port(name=name, port=port)
992 |
993 | return comb_section
994 |
995 | splt_ref = interferometer << splt
996 | bt = interferometer << branch_top()
997 | bb = interferometer << branch_top()
998 | bs = interferometer << branch_tune_short()
999 | bl = interferometer << branch_tune_long(abs(0.5 * length_imbalance))
1000 | cs = interferometer << combiner_section()
1001 | bb.dmirror_y()
1002 | bt.connect("o1", out_top)
1003 | bb.connect("o1", out_bottom)
1004 | if length_imbalance >= 0:
1005 | bs.dmirror_y()
1006 | bs.connect("o1", bb.ports["o2"])
1007 | bl.connect("o1", bt.ports["o2"])
1008 | else:
1009 | bs.connect("o1", bt.ports["o2"])
1010 | bl.dmirror_y()
1011 | bl.connect("o1", bb.ports["o2"])
1012 | cs.dmirror_x()
1013 | [
1014 | cs.connect("o2", bl.ports["o2"])
1015 | if length_imbalance >= 0
1016 | else cs.connect("o2", bs.ports["o2"])
1017 | ]
1018 |
1019 | exposed_ports = [
1020 | ("o1", splt_ref.ports["o1"]),
1021 | ("upper_taper_start", bt.ports["taper_start"]),
1022 | ("short_bias_branch_start", bs.ports["phase_tuning_segment_start"]),
1023 | ("long_bias_branch_start", bl.ports["phase_tuning_segment_start"]),
1024 | ("o2", cs.ports["o1"]),
1025 | ]
1026 |
1027 | if "2x2" in splitter:
1028 | exposed_ports.extend(
1029 | [
1030 | ("out2", cs.ports["in2"]),
1031 | ("in2", splt_ref.ports["o2"]),
1032 | ]
1033 | )
1034 |
1035 | for name, port in exposed_ports:
1036 | interferometer.add_port(name=name, port=port)
1037 | interferometer.flatten()
1038 |
1039 | return interferometer
1040 |
1041 |
1042 | @gf.cell
1043 | def mzm_unbalanced(
1044 | modulation_length: float = 7500.0,
1045 | length_imbalance: float = 100.0,
1046 | lbend_tune_arm_reff: float = 75.0,
1047 | rf_pad_start_width: float = 80.0,
1048 | rf_central_conductor_width: float = 10.0,
1049 | rf_ground_planes_width: float = 180.0,
1050 | rf_gap: float = 4.0,
1051 | rf_pad_length_straight: float = 10.0,
1052 | rf_pad_length_tapered: float = 300.0,
1053 | bias_tuning_section_length: float = 700.0,
1054 | cpw_cell: ComponentSpec = uni_cpw_straight,
1055 | with_heater: bool = False,
1056 | heater_offset: float = 1.2,
1057 | heater_width: float = 1.0,
1058 | heater_pad_size: tuple[float, float] = (75.0, 75.0),
1059 | **kwargs,
1060 | ) -> gf.Component:
1061 | """Mach-Zehnder modulator based on the Pockels effect with an applied RF electric field.
1062 | The modulator works in a differential push-pull configuration driven by a single GSG line."""
1063 |
1064 | mzm = gf.Component()
1065 |
1066 | # Transmission line subcell
1067 |
1068 | xs_cpw = gf.partial(
1069 | xs_uni_cpw,
1070 | central_conductor_width=rf_central_conductor_width,
1071 | ground_planes_width=rf_ground_planes_width,
1072 | gap=rf_gap,
1073 | )
1074 |
1075 | rf_line = mzm << cpw_cell(
1076 | bondpad={
1077 | "component": "CPW_pad_linear",
1078 | "settings": {
1079 | "start_width": rf_pad_start_width,
1080 | "length_straight": rf_pad_length_straight,
1081 | "length_tapered": rf_pad_length_tapered,
1082 | },
1083 | },
1084 | length=modulation_length,
1085 | signal_width=rf_central_conductor_width,
1086 | cross_section=xs_cpw,
1087 | ground_planes_width=rf_ground_planes_width,
1088 | gap_width=rf_gap,
1089 | )
1090 |
1091 | rf_line.dmove(rf_line.ports["e1"].dcenter, (0.0, 0.0))
1092 |
1093 | # Interferometer subcell
1094 |
1095 | if "splitter" not in kwargs.keys():
1096 | kwargs["splitter"] = "mmi1x2_optimized1550"
1097 | splitter = kwargs["splitter"]
1098 |
1099 | splitter = gf.get_component(splitter)
1100 |
1101 | sbend_large_AR = 3.6
1102 |
1103 | gap_eff = rf_gap + 2 * np.sum(
1104 | [
1105 | rf_line.cell.settings[key]
1106 | for key in ("tt", "th")
1107 | if key in rf_line.cell.settings
1108 | ]
1109 | )
1110 |
1111 | GS_separation = rf_pad_start_width * gap_eff / rf_central_conductor_width
1112 |
1113 | sbend_large_v_offset = (
1114 | 0.5 * rf_pad_start_width
1115 | + 0.5 * GS_separation
1116 | - 0.5 * splitter.settings["port_ratio"] * splitter.settings["width_mmi"]
1117 | )
1118 |
1119 | sbend_small_straight_length = rf_pad_length_straight * 0.5
1120 |
1121 | lbend_combiner_reff = (
1122 | 0.5 * rf_pad_start_width
1123 | + lbend_tune_arm_reff
1124 | + 0.5 * GS_separation
1125 | - 0.5 * splitter.settings["port_ratio"] * splitter.settings["width_mmi"]
1126 | )
1127 |
1128 | interferometer = (
1129 | mzm
1130 | << partial(
1131 | _mzm_interferometer,
1132 | modulation_length=modulation_length,
1133 | length_imbalance=length_imbalance,
1134 | sbend_large_size=(
1135 | sbend_large_AR * sbend_large_v_offset,
1136 | sbend_large_v_offset,
1137 | ),
1138 | sbend_small_size=(
1139 | rf_pad_length_straight
1140 | + rf_pad_length_tapered
1141 | - 2 * sbend_small_straight_length,
1142 | -0.5
1143 | * (
1144 | rf_pad_start_width
1145 | - rf_central_conductor_width
1146 | + GS_separation
1147 | - gap_eff
1148 | ),
1149 | ),
1150 | sbend_small_straight_extend=sbend_small_straight_length,
1151 | lbend_tune_arm_reff=lbend_tune_arm_reff,
1152 | lbend_combiner_reff=lbend_combiner_reff,
1153 | bias_tuning_section_length=bias_tuning_section_length,
1154 | **kwargs,
1155 | )()
1156 | )
1157 |
1158 | interferometer.dmove(
1159 | interferometer.ports["upper_taper_start"].dcenter,
1160 | (0.0, 0.5 * (rf_central_conductor_width + gap_eff)),
1161 | )
1162 |
1163 | # Add heater for phase tuning
1164 |
1165 | if with_heater:
1166 | ht_ref = mzm << heater_straight_single(
1167 | length=bias_tuning_section_length,
1168 | width=heater_width,
1169 | offset=heater_offset,
1170 | pad_size=heater_pad_size,
1171 | )
1172 |
1173 | if length_imbalance < 0.0:
1174 | heater_disp = [0, 0.5 * heater_width + heater_offset]
1175 | else:
1176 | ht_ref.dmirror_y()
1177 | heater_disp = [0, -0.5 * heater_width - heater_offset]
1178 |
1179 | ht_ref.dmove(
1180 | origin=ht_ref.ports["ht_start"].dcenter,
1181 | destination=(
1182 | np.array(interferometer.ports["long_bias_branch_start"].dcenter)
1183 | + heater_disp
1184 | ),
1185 | )
1186 |
1187 | # Expose the ports
1188 |
1189 | exposed_ports = [
1190 | ("e1", rf_line.ports["bp1"]),
1191 | ("e2", rf_line.ports["bp2"]),
1192 | ]
1193 |
1194 | if "1x2" in kwargs["splitter"]:
1195 | exposed_ports.extend(
1196 | [
1197 | ("o1", interferometer.ports["o1"]),
1198 | ("o2", interferometer.ports["o2"]),
1199 | ]
1200 | )
1201 | elif "2x2" in kwargs["splitter"]:
1202 | exposed_ports.extend(
1203 | [
1204 | ("o1", interferometer.ports["o1"]),
1205 | ("o2", interferometer.ports["in2"]),
1206 | ("o3", interferometer.ports["out2"]),
1207 | ("o4", interferometer.ports["o2"]),
1208 | ]
1209 | )
1210 |
1211 | if with_heater:
1212 | exposed_ports += [
1213 | ("e3", ht_ref.ports["e1"]),
1214 | (
1215 | "e4",
1216 | ht_ref.ports["e2"],
1217 | ),
1218 | ]
1219 |
1220 | [mzm.add_port(name=name, port=port) for name, port in exposed_ports]
1221 | return mzm
1222 |
1223 |
1224 | @gf.cell
1225 | def mzm_unbalanced_high_speed(**kwargs) -> gf.Component:
1226 | """High-speed Mach-Zehnder modulator based on the Pockels effect with an applied RF electric field.
1227 | The modulator works in a differential push-pull configuration driven by a single GSG line.
1228 | Note: The base variant (mzm_unbalanced) uses a default central conductor width of 10.0,
1229 | while this high-speed variant explicitly passes 21.0 for rf_central_conductor_width to achieve the desired high-speed properties.
1230 | Pass the parameter set of mzm_unbalanced to modify.
1231 | """
1232 | kwargs.setdefault("rf_central_conductor_width", 21.0)
1233 | kwargs.setdefault("cpw_cell", trail_cpw)
1234 | mzm = mzm_unbalanced(**kwargs)
1235 | mzm.info["additional_settings"] = dict(mzm.settings)
1236 | return mzm
1237 |
1238 |
1239 | ##################
1240 | # Chip floorplan
1241 | ##################
1242 |
1243 |
1244 | @gf.cell
1245 | def chip_frame(
1246 | size: tuple[float, float] = (10_000, 5000),
1247 | exclusion_zone_width: float = 50,
1248 | center: tuple[float, float] = None,
1249 | ) -> gf.Component:
1250 | """Provide the chip extent and the exclusion zone around the chip frame.
1251 | In the exclusion zone, only the edge couplers routing to the chip facet should be placed.
1252 | Allowed chip dimensions (in either direction): 5000 um, 10000 um, 20000 um."""
1253 |
1254 | # Check that the chip dimensions have the admissible values.
1255 |
1256 | snapped_size = []
1257 |
1258 | if size[0] <= 5050 and size[1] <= 5050:
1259 | raise (ValueError(f"The chip frame size {size} is not supported."))
1260 |
1261 | if size[0] > 20200 or size[1] > 20200:
1262 | raise (ValueError(f"The chip frame size {size} is not supported."))
1263 |
1264 | else:
1265 | for s in size:
1266 | if abs(s - 5000.0) <= 50.0:
1267 | snapped_size.append(4950.0)
1268 | elif abs(s - 10000.0) <= 100.0:
1269 | snapped_size.append(10000)
1270 | elif abs(s - 20000.0) <= 200:
1271 | snapped_size.append(20100)
1272 | else:
1273 | raise (ValueError(f"The chip frame size {size} is not supported."))
1274 |
1275 | # Chip frame elements
1276 |
1277 | inner_box = gf.components.rectangle(
1278 | size=tuple(snapped_size),
1279 | layer=LAYER.CHIP_CONTOUR,
1280 | centered=True,
1281 | )
1282 |
1283 | outer_box = gf.components.rectangle(
1284 | size=tuple(s + 2 * exclusion_zone_width for s in snapped_size),
1285 | layer=LAYER.CHIP_EXCLUSION_ZONE,
1286 | centered=True,
1287 | )
1288 |
1289 | c = gf.Component()
1290 | ib = c << inner_box
1291 | ob = c << outer_box
1292 |
1293 | if center:
1294 | ib.dmove(origin=(0.0, 0.0), destination=center)
1295 | ob.dmove(origin=(0.0, 0.0), destination=center)
1296 |
1297 | c.flatten()
1298 |
1299 | return c
1300 |
1301 |
1302 | if __name__ == "__main__":
1303 | pass
1304 |
--------------------------------------------------------------------------------