├── 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 | ![Luxtelligence](LXT_Logo.png) 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 | ![anaconda prompt](https://i.imgur.com/eKk2bbs.png) 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 | --------------------------------------------------------------------------------