diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4fc48015..75bf4897 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.0 +current_version = 0.3.1 commit = True tag = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9de9c16..10fdf39c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,3 +48,9 @@ repos: hooks: - id: j2lint args: [--ignore, jinja-statements-delimiter, jinja-statements-indentation, --] + + - repo: https://github.com/markdownlint/markdownlint.git + rev: v0.12.0 + hooks: + - id: markdownlint + args: [--rules, '~MD007,~MD012,~MD013,~MD026,~MD029,~MD033,~MD034'] diff --git a/README.md b/README.md index 26ac8b55..7e17e5be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# gplugins 0.3.0 +# gplugins 0.3.1 [![docs](https://github.com/gdsfactory/gplugins/actions/workflows/pages.yml/badge.svg)](https://gdsfactory.github.io/gplugins/) [![PyPI](https://img.shields.io/pypi/v/gplugins)](https://pypi.org/project/gplugins/) @@ -18,24 +18,31 @@ gdsfactory plugins: - `klayout` for fill, dataprep and testing. - `ray` for distributed computing and optimization. - `sax` S-parameter circuit solver. -- `schematic`: for bokeh schematic editor and `path_length_analysis` +- `schematic`: for bokeh schematic editor and `path_length_analysis`. - `meep` for FDTD. - `mpb` for MPB mode solver. -- `web`: for gdsfactory webapp +- `elmer` for electrostatic (capacitive) simulations. +- `palace` for electrostatic (capacitive) simulations. +- `web` for gdsfactory webapp. ## Installation -You can install all plugins with: +You can install most plugins with: ``` pip install "gplugins[database,devsim,femwell,gmsh,schematic,meow,meshwell,ray,sax,tidy3d]" --upgrade ``` -Or Install only the plugins you need `pip install gplugins[schematic,femwell,meow,sax,tidy3d]` from the available plugins: +Or install only the plugins you need with for example `pip install gplugins[schematic,femwell,meow,sax,tidy3d]` from the available plugins. -Separate installation (not using pip): +### Non-pip plugins + +The following plugins require special installation without pip: - For Meep and MPB you need to use `conda` or `mamba` on MacOS, Linux or [Windows WSL (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/install) with `conda install pymeep=*=mpi_mpich_* -c conda-forge -y` +- For Elmer, refer to [Elmer FEM – Installation](https://www.elmerfem.org/blog/binaries/) for installation or compilation instructions each platform. Gplugins assumes `ElmerSolver`, `ElmerSolver_mpi`, and `ElmerGrid` are available in your PATH environment variable. +- For Palace, refer to [Palace – Installation](https://awslabs.github.io/palace/stable/install/) for compilation instructions using Spack or Singularity. Gplugins assumes `palace` is available in your PATH environment variable. + ## Getting started diff --git a/docs/_config.yml b/docs/_config.yml index a9ece8da..31592134 100755 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -21,6 +21,7 @@ execute: - "*ray_optimiser*" - "*03_numerical_implantation*" - "*02_model_extraction*" + - "*palace*" # - "*20_schematic_driven_layout*" # - "*001_meep_sparameters*" # - "*00_tidy3d.ipynb" @@ -48,6 +49,9 @@ launch_buttons: notebook_interface: jupyterlab colab_url: "https://colab.research.google.com" +bibtex_bibfiles: + - bibliography.bib + sphinx: extra_extensions: - "sphinx.ext.autodoc" @@ -57,6 +61,7 @@ sphinx: - "sphinx.ext.viewcode" - "matplotlib.sphinxext.plot_directive" - "sphinxcontrib.autodoc_pydantic" + - "sphinxcontrib.bibtex" config: #autodoc_typehints: description autodoc_type_aliases: @@ -66,5 +71,3 @@ sphinx: .py: - jupytext.reads - fmt: py - bibtex_reference_style: author_year - bibtex_bibfiles: "bibliography.bib" diff --git a/docs/_toc.yml b/docs/_toc.yml index 07123bb6..37876b1f 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -23,6 +23,7 @@ parts: - file: notebooks/tcad_02_analytical_process - file: notebooks/tcad_03_numerical_implantation - file: notebooks/elmer_01_electrostatic + - file: notebooks/palace_01_electrostatic - file: plugins_mode_solver sections: - file: notebooks/femwell_01_modes diff --git a/docs/api_design.rst b/docs/api_design.rst index ef43dce3..4596e824 100644 --- a/docs/api_design.rst +++ b/docs/api_design.rst @@ -195,3 +195,26 @@ Circuit solver send_to_interconnect run_wavelength_sweep plot_wavelength_sweep + +************** +Electrostatics +************** + +.. currentmodule:: gplugins.elmer + +.. rubric:: Elmer + +.. autosummary:: + :toctree: _autosummary/ + + run_capacitive_simulation_elmer + + +.. currentmodule:: gplugins.palace + +.. rubric:: Palace + +.. autosummary:: + :toctree: _autosummary/ + + run_capacitive_simulation_palace diff --git a/docs/bibliography.bib b/docs/bibliography.bib index 1bdf9312..3f22ae91 100644 --- a/docs/bibliography.bib +++ b/docs/bibliography.bib @@ -1,31 +1,31 @@ - -@article{smolic_capacitance_2021, - title = {Capacitance matrix revisited}, - volume = {92}, - issn = {1937-6472}, - url = {http://www.jpier.org/PIERB/pier.php?paper=21011501}, - doi = {10.2528/PIERB21011501}, - pages = {1--18}, - journaltitle = {Progress In Electromagnetics Research B}, - shortjournal = {{PIER} B}, - author = {Smolić, Ivica and Klajn, Bruno}, - urldate = {2023-08-17}, - date = {2021}, - langid = {english}, -} - @article{marxer_long-distance_2023, - title = {Long-Distance Transmon Coupler with cz-Gate Fidelity above 99.8 \%}, - volume = {4}, - issn = {2691-3399}, - url = {https://link.aps.org/doi/10.1103/PRXQuantum.4.010314}, - doi = {10.1103/PRXQuantum.4.010314}, - pages = {010314}, - number = {1}, - journaltitle = {{PRX} Quantum}, - shortjournal = {{PRX} Quantum}, - author = {Marxer, Fabian and Vepsäläinen, Antti and Jolin, Shan W. and Tuorila, Jani and Landra, Alessandro and Ockeloen-Korppi, Caspar and Liu, Wei and Ahonen, Olli and Auer, Adrian and Belzane, Lucien and Bergholm, Ville and Chan, Chun Fai and Chan, Kok Wai and Hiltunen, Tuukka and Hotari, Juho and Hyyppä, Eric and Ikonen, Joni and Janzso, David and Koistinen, Miikka and Kotilahti, Janne and Li, Tianyi and Luus, Jyrgen and Papic, Miha and Partanen, Matti and Räbinä, Jukka and Rosti, Jari and Savytskyi, Mykhailo and Seppälä, Marko and Sevriuk, Vasilii and Takala, Eelis and Tarasinski, Brian and Thapa, Manish J. and Tosto, Francesca and Vorobeva, Natalia and Yu, Liuqi and Tan, Kuan Yen and Hassel, Juha and Möttönen, Mikko and Heinsoo, Johannes}, - urldate = {2023-08-17}, - date = {2023-02-06}, - langid = {english}, + title = {Long-Distance Transmon Coupler with cz-Gate Fidelity above 99.8 \%}, + author = {Marxer, Fabian and Veps\"{a}l\"{a}inen, Antti and Jolin, Shan W. and Tuorila, Jani and Landra, Alessandro and Ockeloen-Korppi, Caspar and Liu, Wei and Ahonen, Olli and Auer, Adrian and Belzane, Lucien and Bergholm, Ville and Chan, Chun Fai and Chan, Kok Wai and Hiltunen, Tuukka and Hotari, Juho and Hyypp\"{a}, Eric and Ikonen, Joni and Janzso, David and Koistinen, Miikka and Kotilahti, Janne and Li, Tianyi and Luus, Jyrgen and Papic, Miha and Partanen, Matti and R\"{a}bin\"{a}, Jukka and Rosti, Jari and Savytskyi, Mykhailo and Sepp\"{a}l\"{a}, Marko and Sevriuk, Vasilii and Takala, Eelis and Tarasinski, Brian and Thapa, Manish J. and Tosto, Francesca and Vorobeva, Natalia and Yu, Liuqi and Tan, Kuan Yen and Hassel, Juha and M\"{o}tt\"{o}nen, Mikko and Heinsoo, Johannes}, + year = {2023}, + journal = {{PRX} Quantum}, + volume = {4}, + number = {1}, + pages = {010314}, + doi = {10.1103/PRXQuantum.4.010314}, + issn = {2691-3399}, + url = {https://link.aps.org/doi/10.1103/PRXQuantum.4.010314}, + urldate = {2023-08-17}, + shortjournal = {{PRX} Quantum}, + date = {2023-02-06}, + langid = {english} +} +@article{smolic_capacitance_2021, + title = {Capacitance matrix revisited}, + author = {Smoli\'{c}, Ivica and Klajn, Bruno}, + year = {2021}, + journal = {Progress In Electromagnetics Research B}, + volume = {92}, + pages = {1--18}, + doi = {10.2528/PIERB21011501}, + issn = {1937-6472}, + url = {http://www.jpier.org/PIERB/pier.php?paper=21011501}, + urldate = {2023-08-17}, + shortjournal = {{PIER} B}, + date = {2021}, + langid = {english} } diff --git a/docs/changelog.md b/docs/changelog.md index 7b221ffe..668901bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,8 @@ # [CHANGELOG](https://keepachangelog.com/en/1.0.0/) -## [Unreleased](https://github.com/gdsfactory/gplugins/compare/v0.2.0...main) +## [Unreleased](https://github.com/gdsfactory/gplugins/compare/v0.3.0...main) -## [0.3.0](https://github.com/gdsfactory/gplugins/compare/v0.2.0...v0.2.0) +## [0.3.0](https://github.com/gdsfactory/gplugins/compare/v0.3.0...v0.2.0) - improve meshing - add verification diff --git a/docs/notebooks/elmer_01_electrostatic.py b/docs/notebooks/elmer_01_electrostatic.py index d5588d3e..fed975e3 100644 --- a/docs/notebooks/elmer_01_electrostatic.py +++ b/docs/notebooks/elmer_01_electrostatic.py @@ -32,24 +32,24 @@ # #!/bin/bash # singularity exec /elmer.sif ElmerSolver_mpi $@ # ``` +# +# ## Geometry, layer config and materials -# %% +# %% tags=["hide-input"] -import inspect import os -from collections.abc import Sequence from math import inf from pathlib import Path import gdsfactory as gf -import numpy as np import pyvista as pv -from gdsfactory.component import Component -from gdsfactory.components.interdigital_capacitor import interdigital_capacitor +from gdsfactory.components.interdigital_capacitor_enclosed import ( + interdigital_capacitor_enclosed, +) from gdsfactory.generic_tech import LAYER, get_generic_pdk from gdsfactory.technology import LayerStack from gdsfactory.technology.layer_stack import LayerLevel -from gdsfactory.typings import LayerSpec +from IPython.display import display from gplugins.elmer import run_capacitive_simulation_elmer @@ -57,8 +57,6 @@ PDK = get_generic_pdk() PDK.activate() -# LAYER_STACK = PDK.layer_stack - # %% [markdown] # We employ an example LayerStack used in superconducting circuits similar to {cite:p}`marxer_long-distance_2023`. @@ -87,99 +85,6 @@ "vacuum": {"relative_permittivity": 1}, } -# %% - -INTERDIGITAL_DEFAULTS = { - k: v.default - for k, v in inspect.signature(interdigital_capacitor).parameters.items() -} - - -@gf.cell -def interdigital_capacitor_enclosed( - enclosure_box: Sequence[Sequence[float | int]] = [[-200, -200], [200, 200]], - fingers: int = INTERDIGITAL_DEFAULTS["fingers"], - finger_length: float | int = INTERDIGITAL_DEFAULTS["finger_length"], - finger_gap: float | int = INTERDIGITAL_DEFAULTS["finger_gap"], - thickness: float | int = INTERDIGITAL_DEFAULTS["thickness"], - cpw_dimensions: Sequence[float | int] = (10, 6), - gap_to_ground: float | int = 5, - metal_layer: LayerSpec = INTERDIGITAL_DEFAULTS["layer"], - gap_layer: LayerSpec = "DEEPTRENCH", -) -> Component: - """Generates an interdigital capacitor surrounded by a ground plane and coplanar waveguides with ports on both ends. - See for :func:`~interdigital_capacitor` for details. - - Note: - ``finger_length=0`` effectively provides a plate capacitor. - - Args: - enclosure_box: Bounding box dimensions for a ground metal enclosure. - fingers: total fingers of the capacitor. - finger_length: length of the probing fingers. - finger_gap: length of gap between the fingers. - thickness: Thickness of fingers and section before the fingers. - gap_to_ground: Size of gap from capacitor to ground metal. - cpw_dimensions: Dimensions for the trace width and gap width of connecting coplanar waveguides. - metal_layer: layer for metalization. - gap_layer: layer for trenching. - """ - c = Component() - cap = interdigital_capacitor( - fingers, finger_length, finger_gap, thickness, metal_layer - ).ref_center() - c.add(cap) - - gap = Component() - for port in cap.get_ports_list(): - port2 = port.copy() - direction = -1 if port.orientation > 0 else 1 - port2.move((30 * direction, 0)) - port2 = port2.flip() - - cpw_a, cpw_b = cpw_dimensions - s1 = gf.Section(width=cpw_b, offset=(cpw_a + cpw_b) / 2, layer=gap_layer) - s2 = gf.Section(width=cpw_b, offset=-(cpw_a + cpw_b) / 2, layer=gap_layer) - x = gf.CrossSection( - width=cpw_a, - offset=0, - layer=metal_layer, - port_names=("in", "out"), - sections=[s1, s2], - ) - route = gf.routing.get_route( - port, - port2, - cross_section=x, - ) - c.add(route.references) - - term = c << gf.components.bbox( - [[0, 0], [cpw_b, cpw_a + 2 * cpw_b]], layer=gap_layer - ) - if direction < 0: - term.movex(-cpw_b) - term.move( - destination=route.ports[-1].move_copy(-1 * np.array([0, cpw_a / 2 + cpw_b])) - ) - - c.add_port(route.ports[-1]) - c.auto_rename_ports() - - gap.add_polygon(cap.get_polygon_enclosure(), layer=gap_layer) - gap = gap.offset(gap_to_ground, layer=gap_layer) - gap = gf.geometry.boolean(A=gap, B=c, operation="A-B", layer=gap_layer) - - ground = gf.components.bbox(bbox=enclosure_box, layer=metal_layer) - ground = gf.geometry.boolean( - A=ground, B=[c, gap], operation="A-B", layer=metal_layer - ) - - c << ground - - return c.flatten() - - # %% simulation_box = [[-200, -200], [200, 200]] c = gf.Component("capacitance_elmer") @@ -191,14 +96,30 @@ def interdigital_capacitor_enclosed( c << substrate c.flatten() +# %% [markdown] +# ## Running the simulation +# ```{eval-rst} +# We use the function :func:`~run_capacitive_simulation_elmer`. This runs the simulation and returns an instance of :class:`~ElectrostaticResults` containing the capacitance matrix and a path to the mesh and the field solution. +# ``` + # %% +help(run_capacitive_simulation_elmer) + +# %% [markdown] +# ```{eval-rst} +# .. note:: +# The meshing parameters and element order shown here are very lax. As such, the computed capacitances are not very accurate. +# ``` + +# %% tags=["hide-output"] + results = run_capacitive_simulation_elmer( c, layer_stack=layer_stack, material_spec=material_spec, - n_processes=4, + n_processes=1, element_order=1, - simulation_folder=Path(os.getcwd()) / "tmp", + simulation_folder=Path(os.getcwd()) / "temporary", mesh_parameters=dict( background_tag="vacuum", background_padding=(0,) * 5 + (700,), @@ -228,15 +149,18 @@ def interdigital_capacitor_enclosed( }, ), ) -print(results) +display(results) + # %% if results.field_file_location: + pv.start_xvfb() + pv.set_jupyter_backend("panel") field = pv.read(results.field_file_location) - slice = field.slice_orthogonal(z=layer_stack.layers["bw"].zmin * 1e-6) + field_slice = field.slice_orthogonal(z=layer_stack.layers["bw"].zmin * 1e-6) p = pv.Plotter() - p.add_mesh(slice, scalars="electric field", cmap="turbo") + p.add_mesh(field_slice, scalars="electric field", cmap="turbo") p.show_grid() p.camera_position = "xy" p.enable_parallel_projection() @@ -245,6 +169,8 @@ def interdigital_capacitor_enclosed( # %% [markdown] # ## Bibliography +# # ```{bibliography} # :style: unsrt +# :filter: docname in docnames # ``` diff --git a/docs/notebooks/palace_01_electrostatic.py b/docs/notebooks/palace_01_electrostatic.py new file mode 100644 index 00000000..6075dba2 --- /dev/null +++ b/docs/notebooks/palace_01_electrostatic.py @@ -0,0 +1,172 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_filter: -all +# custom_cell_magics: kql +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.15.0 +# kernelspec: +# display_name: base +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Electrostatic simulations with Palace +# Here, we show how Palace may be used to perform electrostatic simulations. For a given geometry, one needs to specify the terminals where to apply potential, similar to {doc}`./elmer_01_electrostatic.py`. +# This effectively solves the mutual capacitance matrix for the terminals and the capacitance to ground. +# For details on the physics, see {cite:p}`smolic_capacitance_2021`. +# +# ## Installation +# See [Palace – Installation](https://awslabs.github.io/palace/stable/install/) for installation or compilation instructions. Gplugins assumes `palace` is available in your PATH environment variable. +# +# Alternatively, [Singularity / Apptainer](https://apptainer.org/) containers may be used. Instructions for building and an example definition file are found at [Palace – Build using Singularity/Apptainer](https://awslabs.github.io/palace/dev/install/#Build-using-Singularity/Apptainer). +# Afterwards, an easy install method is to add a script to `~/.local/bin` (or elsewhere in `PATH`) calling the Singularity container. For example, one may create a `palace` file containing +# ```console +# #!/bin/bash +# singularity exec ~/palace.sif /opt/palace/bin/palace "$@" +# ``` +# +# ## Geometry, layer config and materials + +# %% tags=["hide-input"] + +import os +from math import inf +from pathlib import Path + +import gdsfactory as gf +import pyvista as pv +from gdsfactory.components.interdigital_capacitor_enclosed import ( + interdigital_capacitor_enclosed, +) +from gdsfactory.generic_tech import LAYER, get_generic_pdk +from gdsfactory.technology import LayerStack +from gdsfactory.technology.layer_stack import LayerLevel +from IPython.display import display + +from gplugins.palace import run_capacitive_simulation_palace +from gplugins.typings import RFMaterialSpec + +gf.config.rich_output() +PDK = get_generic_pdk() +PDK.activate() + +# %% [markdown] +# We employ an example LayerStack used in superconducting circuits similar to {cite:p}`marxer_long-distance_2023`. + +# %% +layer_stack = LayerStack( + layers=dict( + substrate=LayerLevel( + layer=LAYER.WAFER, + thickness=500, + zmin=0, + material="Si", + mesh_order=99, + ), + bw=LayerLevel( + layer=LAYER.WG, + thickness=200e-3, + zmin=500, + material="Nb", + mesh_order=2, + ), + ) +) +material_spec: RFMaterialSpec = { + "Si": {"relative_permittivity": 11.45}, + "Nb": {"relative_permittivity": inf}, + "vacuum": {"relative_permittivity": 1}, +} + +# %% +simulation_box = [[-200, -200], [200, 200]] +c = gf.Component("capacitance_palace") +cap = c << interdigital_capacitor_enclosed( + metal_layer=LAYER.WG, gap_layer=LAYER.DEEPTRENCH, enclosure_box=simulation_box +) +c.add_ports(cap.ports) +substrate = gf.components.bbox(bbox=simulation_box, layer=LAYER.WAFER) +c << substrate +c.flatten() + +# %% [markdown] +# ## Running the simulation +# ```{eval-rst} +# We use the function :func:`~run_capacitive_simulation_palace`. This runs the simulation and returns an instance of :class:`~ElectrostaticResults` containing the capacitance matrix and a path to the mesh and the field solutions. +# ``` + +# %% +help(run_capacitive_simulation_palace) + +# %% [markdown] +# ```{eval-rst} +# .. note:: +# The meshing parameters and element order shown here are very lax. As such, the computed capacitances are not very accurate. +# ``` + +# %% +results = run_capacitive_simulation_palace( + c, + layer_stack=layer_stack, + material_spec=material_spec, + n_processes=1, + element_order=1, + simulation_folder=Path(os.getcwd()) / "temporary", + mesh_parameters=dict( + background_tag="vacuum", + background_padding=(0,) * 5 + (700,), + portnames=c.ports, + default_characteristic_length=200, + layer_portname_delimiter=(delimiter := "__"), + resolutions={ + "bw": { + "resolution": 15, + }, + "substrate": { + "resolution": 40, + }, + "vacuum": { + "resolution": 40, + }, + **{ + f"bw{delimiter}{port}": { + "resolution": 20, + "DistMax": 30, + "DistMin": 10, + "SizeMax": 14, + "SizeMin": 3, + } + for port in c.ports + }, + }, + ), +) +display(results) + +# %% +if results.field_file_location: + pv.start_xvfb() + pv.set_jupyter_backend("panel") + field = pv.read(results.field_file_location) + slice = field.slice_orthogonal(z=layer_stack.layers["bw"].zmin * 1e-6) + + p = pv.Plotter() + p.add_mesh(slice, scalars="E", cmap="turbo") + p.show_grid() + p.camera_position = "xy" + p.enable_parallel_projection() + p.show() + + +# %% [markdown] +# ## Bibliography +# +# ```{bibliography} +# :style: unsrt +# :filter: docname in docnames +# ``` diff --git a/docs/notebooks/sax_02_model_extraction.py b/docs/notebooks/sax_02_model_extraction.py index 793fd388..4a87ff4c 100644 --- a/docs/notebooks/sax_02_model_extraction.py +++ b/docs/notebooks/sax_02_model_extraction.py @@ -63,7 +63,7 @@ def trainable_straight_rib(parameters): # Next we can instantiate the `Model` proper. Here, we use the children class `FemwellWaveguideModel`. Its `outputs_from_inputs` method returns the effective index from the input geometry, and its `sdict` function uses the input geometry, length, and loss to return the S-parameters for the corresponding straight waveguide: # + -from gplugins.sax.femwell_waveguide_model import FemwellWaveguideModel +from gplugins.sax.integrations.femwell_waveguide_model import FemwellWaveguideModel rib_waveguide_model = FemwellWaveguideModel( trainable_component=trainable_straight_rib, diff --git a/docs/plugins_fdtd.md b/docs/plugins_fdtd.md index 23c038db..4051abbe 100644 --- a/docs/plugins_fdtd.md +++ b/docs/plugins_fdtd.md @@ -8,9 +8,9 @@ FDTD simulations compute the [Sparameters](https://en.wikipedia.org/wiki/Scatter gdsfactory provides you a similar python API to drive 3 different FDTD simulators: - - MEEP - - tidy3d - - Lumerical Ansys FDTD +- MEEP +- tidy3d +- Lumerical Ansys FDTD Gdsfactory follows the Sparameters syntax `o1@0,o2@0` where `o1` is the input port `@0` mode 0 (usually fundamental TE mode) and `o2@0` refers to output port `o2` mode 0. diff --git a/docs/workflow.md b/docs/workflow.md index 5e31e13f..537ddba1 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -1,4 +1,6 @@ # Workflow Photonics +You can learn about photonics in this [photonics bootcamp](https://byucamacholab.github.io/Photonics-Bootcamp/index.html) + ```{tableofcontents} ``` diff --git a/gplugins/__init__.py b/gplugins/__init__.py index 3adf2b76..d1b87c70 100644 --- a/gplugins/__init__.py +++ b/gplugins/__init__.py @@ -1,6 +1,6 @@ """gplugins - gdsfactory plugins""" -__version__ = "0.3.0" +__version__ = "0.3.1" from gplugins.utils import plot, port_symmetries from gplugins.utils.get_effective_indices import get_effective_indices diff --git a/gplugins/async_utils.py b/gplugins/async_utils.py new file mode 100644 index 00000000..f28fff48 --- /dev/null +++ b/gplugins/async_utils.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import asyncio +import io +import sys +from collections.abc import Awaitable, Coroutine +from contextlib import nullcontext +from pathlib import Path +from typing import Any, TypeVar + +T = TypeVar("T") + + +async def handle_return( + in_stream: asyncio.streams.StreamReader, + out_stream: io.TextIOWrapper | None = None, + log_file: Path | None = None, + append: bool = False, +) -> None: + """Reads through a :class:`StreamReader` and tees content to ``out_stream`` and ``log_file``.""" + with open( + log_file, "a" if append else "w", encoding="utf-8", buffering=1 + ) if log_file else nullcontext(None) as f: + while True: + # Without this sleep, the program won't exit + await asyncio.sleep(0) + data = await in_stream.readline() + if data: + line = data.decode("utf-8").rstrip() + if out_stream: + print(line, file=out_stream) + if f: + f.write(line + "\n") + else: + break + + +async def execute_and_stream_output( + command: list[str] | str, + *args, + shell: bool = True, + append: bool = False, + log_file_dir: Path | None = None, + log_file_str: str | None = None, + **kwargs, +) -> asyncio.subprocess.Process: + """Run a command asynchronously and stream *stdout* and *stderr* to main and a log file + in ``log_file_dir / log_file_str``. Uses ``shell=True`` as default unlike ``subprocess.Popen``. Returns an asyncio process. + + Args: + command: Command(s) to run. Sequences will be unpacked. + shell: Whether to use shell or exec. + append: Whether to use append to log file instead of writing. + log_file_dir: Directory for log files. + log_file_str: Log file name. Will be expanded to ``f'{log_file_str}_out.log'`` and ``f'{log_file_str}_err.log'``. + + ``*args`` and ``**kwargs`` are passed to :func:`~create_subprocess_shell` or :func:`create_subprocess_exec`, + which in turn passes them to :class:`subprocess.Popen`. + """ + if log_file_dir is None: + log_file_dir = Path.cwd() + if log_file_str is None: + log_file_str = command if isinstance(command, str) else "_".join(command) + + subprocess_factory = ( + asyncio.create_subprocess_shell if shell else asyncio.create_subprocess_exec + ) + proc = await subprocess_factory( + *([command] if isinstance(command, str) else command), + *args, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + **kwargs, + ) + asyncio.create_task( + handle_return( + proc.stdout, + out_stream=sys.stdout, + log_file=log_file_dir / f"{log_file_str}_out.log", + append=append, + ) + ) + asyncio.create_task( + handle_return( + proc.stderr, + out_stream=sys.stderr, + log_file=log_file_dir / f"{log_file_str}_err.log", + append=append, + ) + ) + + # This needs to also handle the "wait_to_finish" flag + await proc.wait() + return proc + + +def run_async_with_event_loop(coroutine: Coroutine[Any, Any, T] | Awaitable[T]) -> T: + """Run a coroutine within an asyncio event loop, either by adding it to the + existing running event loop or by creating a new event loop. Returns the result. + + Args: + coroutine: The coroutine (async function) to be executed. + + Note: + If an asyncio event loop is already running, `nest_asyncio `_ + is used to create a new loop. the given coroutine will be added to the running event loop. + If no event loop is running, a new event loop will be created. + + Example: + async def main(): + # Your main coroutine code here + pass + + run_async_with_event_loop(main()) + """ + + try: + loop = asyncio.get_running_loop() + try: + import nest_asyncio # pylint: disable=import-outside-toplevel + + nest_asyncio.apply() + except ModuleNotFoundError as e: + raise UserWarning( + "You need to install `nest-asyncio` to run in existing event loops like IPython" + ) from e + return loop.run_until_complete(coroutine) + except RuntimeError as e: + if "no running event loop" not in str(e): + raise e + return asyncio.run(coroutine) diff --git a/gplugins/elmer/get_capacitance.py b/gplugins/elmer/get_capacitance.py index cb24c7cd..ad3aefbb 100644 --- a/gplugins/elmer/get_capacitance.py +++ b/gplugins/elmer/get_capacitance.py @@ -3,7 +3,6 @@ import inspect import itertools import shutil -import subprocess from collections.abc import Iterable, Mapping, Sequence from math import inf from pathlib import Path @@ -12,14 +11,13 @@ import gdsfactory as gf import gmsh - -# from gdsfactory.components import interdigital_capacitor_enclosed from gdsfactory.generic_tech import LAYER_STACK from gdsfactory.technology import LayerStack from jinja2 import Environment, FileSystemLoader from numpy import isfinite from pandas import read_csv +from gplugins.async_utils import execute_and_stream_output, run_async_with_event_loop from gplugins.typings import ElectrostaticResults, RFMaterialSpec ELECTROSTATIC_SIF = "electrostatic.sif" @@ -63,19 +61,20 @@ def _elmergrid(simulation_folder: Path, name: str, n_processes: int = 1): elmergrid = shutil.which("ElmerGrid") if elmergrid is None: raise RuntimeError( - "ElmerGrid not found. Make sure it is available in your PATH." + "`ElmerGrid` not found. Make sure it is available in your PATH." ) - with open(simulation_folder / f"{name}_ElmerGrid.log", "w", encoding="utf-8") as fp: - subprocess.run( + run_async_with_event_loop( + execute_and_stream_output( [elmergrid, "14", "2", name, "-autoclean"], - cwd=simulation_folder, shell=False, - stdout=fp, - stderr=fp, - check=True, + log_file_dir=simulation_folder, + log_file_str=Path(name).stem + "_ElmerGrid", + cwd=simulation_folder, ) - if n_processes > 1: - subprocess.run( + ) + if n_processes > 1: + run_async_with_event_loop( + execute_and_stream_output( [ elmergrid, "2", @@ -86,40 +85,37 @@ def _elmergrid(simulation_folder: Path, name: str, n_processes: int = 1): "4", "-removeunused", ], - cwd=simulation_folder, shell=False, - stdout=fp, - stderr=fp, - check=True, + append=True, + log_file_dir=simulation_folder, + log_file_str=Path(name).stem + "_ElmerGrid", + cwd=simulation_folder, ) + ) def _elmersolver(simulation_folder: Path, name: str, n_processes: int = 1): """Run simulations with ElmerFEM.""" - elmersolver = ( - shutil.which("ElmerSolver") - if (no_mpi := n_processes == 1) - else shutil.which("ElmerSolver_mpi") + elmersolver_name = ( + "ElmerSolver" if (no_mpi := n_processes == 1) else "ElmerSolver_mpi" ) + elmersolver = shutil.which(elmersolver_name) if elmersolver is None: raise RuntimeError( - ("ElmerSolver" if n_processes == 1 else "ElmerSolver_mpi") - + " not found. Make sure it is available in your PATH." + f"`{elmersolver_name}` not found. Make sure it is available in your PATH." ) sif_file = str(simulation_folder / f"{Path(name).stem}.sif") - with open( - simulation_folder / f"{name}_ElmerSolver.log", "w", encoding="utf-8" - ) as fp: - subprocess.run( + run_async_with_event_loop( + execute_and_stream_output( [elmersolver, sif_file] if no_mpi else ["mpiexec", "-np", str(n_processes), elmersolver, sif_file], - cwd=simulation_folder, shell=False, - stdout=fp, - stderr=fp, - check=True, + log_file_dir=simulation_folder, + log_file_str=Path(name).stem + "_ElmerSolver", + cwd=simulation_folder, ) + ) def _read_elmer_results( @@ -194,7 +190,7 @@ def run_capacitive_simulation_elmer( mesh_file: Path to a ready mesh to use. Useful for reusing one mesh file. By default a mesh is generated according to ``mesh_parameters``. - .. _Elmer https://github.com/ElmerCSC/elmerfem + .. _Elmer: https://github.com/ElmerCSC/elmerfem """ if layer_stack is None: diff --git a/gplugins/elmer/tests/test_elmer.py b/gplugins/elmer/tests/test_elmer.py index 84fbe7e4..b40d0817 100644 --- a/gplugins/elmer/tests/test_elmer.py +++ b/gplugins/elmer/tests/test_elmer.py @@ -1,16 +1,12 @@ -import inspect -from collections.abc import Sequence from math import inf import gdsfactory as gf -import numpy as np import pytest from gdsfactory.component import Component -from gdsfactory.components.interdigital_capacitor import interdigital_capacitor +from gdsfactory.components import interdigital_capacitor_enclosed from gdsfactory.generic_tech import LAYER from gdsfactory.technology import LayerStack from gdsfactory.technology.layer_stack import LayerLevel -from gdsfactory.typings import LayerSpec from gplugins.elmer import run_capacitive_simulation_elmer @@ -38,96 +34,6 @@ "vacuum": {"relative_permittivity": 1}, } -INTERDIGITAL_DEFAULTS = { - k: v.default - for k, v in inspect.signature(interdigital_capacitor).parameters.items() -} - - -@gf.cell -def interdigital_capacitor_enclosed( - enclosure_box: Sequence[Sequence[float | int]] = [[-200, -200], [200, 200]], - fingers: int = INTERDIGITAL_DEFAULTS["fingers"], - finger_length: float | int = INTERDIGITAL_DEFAULTS["finger_length"], - finger_gap: float | int = INTERDIGITAL_DEFAULTS["finger_gap"], - thickness: float | int = INTERDIGITAL_DEFAULTS["thickness"], - cpw_dimensions: Sequence[float | int] = (10, 6), - gap_to_ground: float | int = 5, - metal_layer: LayerSpec = INTERDIGITAL_DEFAULTS["layer"], - gap_layer: LayerSpec = "DEEPTRENCH", -) -> Component: - """Generates an interdigital capacitor surrounded by a ground plane and coplanar waveguides with ports on both ends. - See for :func:`~interdigital_capacitor` for details. - - Note: - ``finger_length=0`` effectively provides a plate capacitor. - - Args: - enclosure_box: Bounding box dimensions for a ground metal enclosure. - fingers: total fingers of the capacitor. - finger_length: length of the probing fingers. - finger_gap: length of gap between the fingers. - thickness: Thickness of fingers and section before the fingers. - gap_to_ground: Size of gap from capacitor to ground metal. - cpw_dimensions: Dimensions for the trace width and gap width of connecting coplanar waveguides. - metal_layer: layer for metalization. - gap_layer: layer for trenching. - """ - c = Component() - cap = interdigital_capacitor( - fingers, finger_length, finger_gap, thickness, metal_layer - ).ref_center() - c.add(cap) - - gap = Component() - for port in cap.get_ports_list(): - port2 = port.copy() - direction = -1 if port.orientation > 0 else 1 - port2.move((30 * direction, 0)) - port2 = port2.flip() - - cpw_a, cpw_b = cpw_dimensions - s1 = gf.Section(width=cpw_b, offset=(cpw_a + cpw_b) / 2, layer=gap_layer) - s2 = gf.Section(width=cpw_b, offset=-(cpw_a + cpw_b) / 2, layer=gap_layer) - x = gf.CrossSection( - width=cpw_a, - offset=0, - layer=metal_layer, - port_names=("in", "out"), - sections=[s1, s2], - ) - route = gf.routing.get_route( - port, - port2, - cross_section=x, - ) - c.add(route.references) - - term = c << gf.components.bbox( - [[0, 0], [cpw_b, cpw_a + 2 * cpw_b]], layer=gap_layer - ) - if direction < 0: - term.movex(-cpw_b) - term.move( - destination=route.ports[-1].move_copy(-1 * np.array([0, cpw_a / 2 + cpw_b])) - ) - - c.add_port(route.ports[-1]) - c.auto_rename_ports() - - gap.add_polygon(cap.get_polygon_enclosure(), layer=gap_layer) - gap = gap.offset(gap_to_ground, layer=gap_layer) - gap = gf.geometry.boolean(A=gap, B=c, operation="A-B", layer=gap_layer) - - ground = gf.components.bbox(bbox=enclosure_box, layer=metal_layer) - ground = gf.geometry.boolean( - A=ground, B=[c, gap], operation="A-B", layer=metal_layer - ) - - c << ground - - return c.flatten() - @pytest.fixture @gf.cell diff --git a/gplugins/gmeep/async_utils.py b/gplugins/gmeep/async_utils.py deleted file mode 100644 index 09263eec..00000000 --- a/gplugins/gmeep/async_utils.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import asyncio -from pathlib import Path - - -async def handle_return( - out_or_err: asyncio.streams.StreamReader, - log_file: Path | None = None, - to_console: bool = True, -) -> None: - with open(log_file, "w") as f: - while True: - # Without this sleep, the program won't exit - await asyncio.sleep(0) - data = await out_or_err.readline() - line = data.decode().strip() - if line: - if to_console: - print(line) - f.write(line + "\n") - - -async def execute_and_stream_output( - command: str, log_file_dir: Path, log_file_str: str -) -> asyncio.subprocess.Process: - # Best not to use shell, but I can't get create_subprocess_exec to work here - proc = await asyncio.create_subprocess_shell( - command, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - asyncio.create_task( - handle_return(proc.stdout, log_file=log_file_dir / f"{log_file_str}_out.log") - ) - asyncio.create_task( - handle_return(proc.stderr, log_file=log_file_dir / f"{log_file_str}_err.log") - ) - - # This needs to also handle the "wait_to_finish" flag - await proc.wait() - return proc diff --git a/gplugins/gmeep/write_sparameters_meep_mpi.py b/gplugins/gmeep/write_sparameters_meep_mpi.py index 9a851134..09121a91 100644 --- a/gplugins/gmeep/write_sparameters_meep_mpi.py +++ b/gplugins/gmeep/write_sparameters_meep_mpi.py @@ -198,7 +198,7 @@ def write_sparameters_meep_mpi( if live_output: import asyncio - from gplugins.gmeep.async_utils import execute_and_stream_output + from gplugins.async_utils import execute_and_stream_output asyncio.run( execute_and_stream_output( diff --git a/gplugins/meow/meow_eme.py b/gplugins/meow/meow_eme.py index b08b41ba..da046a4c 100644 --- a/gplugins/meow/meow_eme.py +++ b/gplugins/meow/meow_eme.py @@ -9,7 +9,7 @@ import pandas as pd import sax import yaml -from gdsfactory.config import logger +from gdsfactory.config import PATH, logger from gdsfactory.generic_tech import LAYER from gdsfactory.pdk import get_active_pdk, get_layer_stack from gdsfactory.technology import LayerStack @@ -64,7 +64,7 @@ def __init__( center_y: float | None = None, resolution_y: int = 100, material_to_color: dict[str, ColorRGB] = material_to_color_default, - dirpath: PathType | None = None, + dirpath: PathType | None = PATH.sparameters, filepath: PathType | None = None, overwrite: bool = False, ) -> None: diff --git a/gplugins/modes/find_modes.py b/gplugins/modes/find_modes.py index 4072a662..80273497 100644 --- a/gplugins/modes/find_modes.py +++ b/gplugins/modes/find_modes.py @@ -10,12 +10,14 @@ 1/1.5 = 0.6667. """ +import pathlib import pickle from functools import partial import meep as mp import numpy as np -from gdsfactory.pdk import get_modes_path +from gdsfactory.config import PATH +from gdsfactory.typings import PathType from meep import mpb from gplugins.modes.get_mode_solver_coupler import get_mode_solver_coupler @@ -32,13 +34,52 @@ def find_modes_waveguide( wavelength: float = 1.55, mode_number: int = 1, parity=mp.NO_PARITY, - cache: bool = True, + cache_path: PathType | None = PATH.modes, overwrite: bool = False, single_waveguide: bool = True, **kwargs, ) -> dict[int, Mode]: """Computes mode effective and group index for a rectangular waveguide. + Args: + tol: tolerance when finding modes. + wavelength: wavelength in um. + mode_number: mode order of the first mode. + parity: mp.ODD_Y mp.EVEN_X for TE, mp.EVEN_Y for TM. + cache_path: path to cache folder. None to disable caching. + overwrite: forces simulating again. + single_waveguide: if True, compute a single waveguide. False computes a coupler. + + Keyword Args: + core_width: core_width (um) for the symmetric case. + gap: for the case of only two waveguides. + core_widths: list or tuple of waveguide widths. + gaps: list or tuple of waveguide gaps. + core_thickness: wg height (um). + slab_thickness: thickness for the waveguide slab. + core_material: core material refractive index. + clad_material: clad material refractive index. + nslab: Optional slab material refractive index. Defaults to core_material. + ymargin: margin in y. + sz: simulation region thickness (um). + resolution: resolution (pixels/um). + nmodes: number of modes. + sidewall_angles: waveguide sidewall angle (radians), + tapers from core_width at top of slab, upwards, to top of waveguide. + + Returns: Dict[mode_number, Mode] + + compute mode_number lowest frequencies as a function of k. Also display + "parities", i.e. whether the mode is symmetric or anti_symmetric + through the y=0 and z=0 planes. + mode_solver.run(mpb.display_yparities, mpb.display_zparities) + + Above, we outputted the dispersion relation: frequency (omega) as a + function of wavevector kx (beta). Alternatively, you can compute + beta for a given omega -- for example, you might want to find the + modes and wavevectors at a fixed wavelength of 1.55 microns. You + can do that using the find_k function: + single_waveguide=True :: @@ -82,56 +123,7 @@ def find_modes_waveguide( <---------------------------------------------------> sy - Args: - mode_solver: function that returns mpb.ModeSolver. - tol: tolerance when finding modes. - wavelength: wavelength in um. - mode_number: mode order of the first mode. - parity: mp.ODD_Y mp.EVEN_X for TE, mp.EVEN_Y for TM. - cache: directory path to cache modes. None disables the file cache. - overwrite: forces simulating again. - kwargs: waveguide settings. - - Keyword Args: - core_width: core_width (um). - core_thickness: wg height (um). - slab_thickness: thickness for the waveguide slab. - core_material: core material refractive index. - clad_material: clad material refractive index. - sy: simulation region width (um). - sz: simulation region height (um). - resolution: resolution (pixels/um). - nmodes: number of modes to compute. - Keyword Args: - core_width: core_width (um) for the symmetric case. - gap: for the case of only two waveguides. - core_widths: list or tuple of waveguide widths. - gaps: list or tuple of waveguide gaps. - core_thickness: wg height (um). - slab_thickness: thickness for the waveguide slab. - core_material: core material refractive index. - clad_material: clad material refractive index. - nslab: Optional slab material refractive index. Defaults to core_material. - ymargin: margin in y. - sz: simulation region thickness (um). - resolution: resolution (pixels/um). - nmodes: number of modes. - sidewall_angles: waveguide sidewall angle (radians), - tapers from core_width at top of slab, upwards, to top of waveguide. - - Returns: Dict[mode_number, Mode] - - compute mode_number lowest frequencies as a function of k. Also display - "parities", i.e. whether the mode is symmetric or anti_symmetric - through the y=0 and z=0 planes. - mode_solver.run(mpb.display_yparities, mpb.display_zparities) - - Above, we outputted the dispersion relation: frequency (omega) as a - function of wavevector kx (beta). Alternatively, you can compute - beta for a given omega -- for example, you might want to find the - modes and wavevectors at a fixed wavelength of 1.55 microns. You - can do that using the find_k function: """ modes = {} @@ -150,8 +142,8 @@ def find_modes_waveguide( **kwargs, ) - if cache: - cache_path = get_modes_path() + if cache_path: + cache_path = pathlib.Path(cache_path) cache_path.mkdir(exist_ok=True, parents=True) filepath = cache_path / f"{h}_{mode_number}.pkl" @@ -209,7 +201,7 @@ def find_modes_waveguide( z_num, ), ) - if cache: + if cache_path: filepath = cache_path / f"{h}_{index}.pkl" filepath.write_bytes(pickle.dumps(modes[i])) diff --git a/gplugins/modes/find_modes_cross_section.py b/gplugins/modes/find_modes_cross_section.py index 9af6d5cb..143b7cec 100755 --- a/gplugins/modes/find_modes_cross_section.py +++ b/gplugins/modes/find_modes_cross_section.py @@ -12,12 +12,13 @@ """ from __future__ import annotations +import pathlib import pickle import meep as mp import numpy as np -from gdsfactory.pdk import get_modes_path -from gdsfactory.typings import CrossSectionSpec +from gdsfactory.config import PATH +from gdsfactory.typings import CrossSectionSpec, PathType from meep import mpb from gplugins.modes.get_mode_solver_cross_section import ( @@ -36,7 +37,7 @@ def find_modes_cross_section( wavelength: float = 1.55, mode_number: int = 1, parity=mp.NO_PARITY, - cache: bool = True, + cache_path: PathType | None = PATH.modes, overwrite: bool = False, **kwargs, ) -> dict[int, Mode]: @@ -48,7 +49,7 @@ def find_modes_cross_section( wavelength: wavelength in um. mode_number: mode order of the first mode. parity: mp.ODD_Y mp.EVEN_X for TE, mp.EVEN_Y for TM. - cache: True uses file cache from PDK.modes_path. False skips cache. + cache_path: path to cache folder. None to disable caching. overwrite: forces simulating again. kwargs: waveguide settings. @@ -94,8 +95,8 @@ def find_modes_cross_section( **kwargs, ) - if cache: - cache_path = get_modes_path() + if cache_path: + cache_path = pathlib.Path(cache_path) cache_path.mkdir(exist_ok=True, parents=True) filepath = cache_path / f"{h}_{mode_number}.pkl" @@ -154,7 +155,7 @@ def find_modes_cross_section( ), material_indices=mode_solver.info["material_indices"], ) - if cache: + if cache_path: filepath = cache_path / f"{h}_{index}.pkl" filepath.write_bytes(pickle.dumps(modes[i])) diff --git a/gplugins/modes/tests/test_dw_dh.py b/gplugins/modes/tests/test_dw_dh.py index 8c4c7336..be26922f 100644 --- a/gplugins/modes/tests/test_dw_dh.py +++ b/gplugins/modes/tests/test_dw_dh.py @@ -4,7 +4,7 @@ def test_dw_dh(dataframe_regression) -> None: - df = find_neff_ng_dw_dh(steps=1, resolution=10, cache=None) + df = find_neff_ng_dw_dh(steps=1, resolution=10, cache_path=None) if dataframe_regression: dataframe_regression.check(df, default_tolerance=dict(atol=1e-2, rtol=1e-2)) else: diff --git a/gplugins/modes/tests/test_find_modes.py b/gplugins/modes/tests/test_find_modes.py index a2e1ceb1..9172e801 100644 --- a/gplugins/modes/tests/test_find_modes.py +++ b/gplugins/modes/tests/test_find_modes.py @@ -6,7 +6,7 @@ def test_find_modes_waveguide() -> None: - modes = find_modes_waveguide(core_width=0.45, resolution=20, cache=None) + modes = find_modes_waveguide(core_width=0.45, resolution=20, cache_path=None) m1 = modes[1] m2 = modes[2] diff --git a/gplugins/modes/tests/test_find_modes_dispersion.py b/gplugins/modes/tests/test_find_modes_dispersion.py index 55f15297..269ef404 100644 --- a/gplugins/modes/tests/test_find_modes_dispersion.py +++ b/gplugins/modes/tests/test_find_modes_dispersion.py @@ -6,7 +6,7 @@ def test_find_modes_waveguide_dispersion() -> None: - modes = find_mode_dispersion(core_width=0.45, resolution=20, cache=None) + modes = find_mode_dispersion(core_width=0.45, resolution=20, cache_path=None) m1 = modes # print(f"neff1 = {m1.neff}") diff --git a/gplugins/modes/tests/test_neff_vs_width.py b/gplugins/modes/tests/test_neff_vs_width.py index b5977a07..0aca385d 100644 --- a/gplugins/modes/tests/test_neff_vs_width.py +++ b/gplugins/modes/tests/test_neff_vs_width.py @@ -4,7 +4,7 @@ def test_neff_vs_width(dataframe_regression) -> None: - df = gm.find_neff_vs_width(steps=1, resolution=10, cache=None) + df = gm.find_neff_vs_width(steps=1, resolution=10, cache_path=None) if dataframe_regression: dataframe_regression.check(df) else: diff --git a/gplugins/palace/__init__.py b/gplugins/palace/__init__.py new file mode 100644 index 00000000..a87531a8 --- /dev/null +++ b/gplugins/palace/__init__.py @@ -0,0 +1,5 @@ +from gplugins.palace.get_capacitance import run_capacitive_simulation_palace + +__all__ = [ + "run_capacitive_simulation_palace", +] diff --git a/gplugins/palace/electrostatic.json b/gplugins/palace/electrostatic.json new file mode 100644 index 00000000..d331b84b --- /dev/null +++ b/gplugins/palace/electrostatic.json @@ -0,0 +1,69 @@ +{ + "Problem": { + "Type": "Electrostatic", + "Verbose": 3, + "Output": "postpro" + }, + "Model": { + "Mesh": "#MESH", + "L0": 1.0e-6 + }, + "Domains": { + "Materials": [ + { + "Attributes": [ + 6 + ], + "Permittivity": 1 + }, + { + "Attributes": [ + 5 + ], + "Permittivity": 11.4 + } + ] + }, + "Boundaries": { + "Ground": { + "Attributes": [ + 7 + ] + }, + "Terminal": [ + { + "Index": 1, + "Attributes": [ + 1 + ] + }, + { + "Index": 2, + "Attributes": [ + 2 + ] + }, + { + "Index": 3, + "Attributes": [ + 3 + ] + } + ], + "Postprocessing": { + "Capacitance": [] + } + }, + "Solver": { + "Order": 1, + "Electrostatic": { + "Save": 2 + }, + "Linear": { + "Type": "BoomerAMG", + "KSPType": "CG", + "Tol": 1e-8, + "MaxIts": 100 + } + } +} diff --git a/gplugins/palace/get_capacitance.py b/gplugins/palace/get_capacitance.py new file mode 100644 index 00000000..60bc7dc2 --- /dev/null +++ b/gplugins/palace/get_capacitance.py @@ -0,0 +1,304 @@ +from __future__ import annotations + +import inspect +import itertools +import json +import shutil +from collections.abc import Iterable, Mapping, Sequence +from math import inf +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +import gdsfactory as gf +import gmsh +from gdsfactory.generic_tech import LAYER_STACK +from gdsfactory.technology import LayerStack +from numpy import isfinite +from pandas import read_csv + +from gplugins.async_utils import execute_and_stream_output, run_async_with_event_loop +from gplugins.typings import ElectrostaticResults, RFMaterialSpec + +ELECTROSTATIC_JSON = "electrostatic.json" +ELECTROSTATIC_TEMPLATE = Path(__file__).parent / ELECTROSTATIC_JSON + + +def _generate_json( + simulation_folder: Path, + name: str, + signals: Sequence[Sequence[str]], + bodies: dict[str, dict[str, Any]], + ground_layers: Iterable[str], + layer_stack: LayerStack, + material_spec: RFMaterialSpec, + element_order: int, + physical_name_to_dimtag_map: dict[str, tuple[int, int]], + background_tag: str | None = None, + simulator_params: Mapping[str, Any] | None = None, +): + """Generates a json file for capacitive Palace simulations.""" + # TODO: Generalise to merger with the Elmer implementations""" + used_materials = {v.material for v in layer_stack.layers.values()} | ( + {background_tag} if background_tag else {} + ) + used_materials = { + k: material_spec[k] + for k in used_materials + if isfinite(material_spec[k].get("relative_permittivity", inf)) + } + + with open(ELECTROSTATIC_TEMPLATE) as fp: + palace_json_data = json.load(fp) + + material_to_attributes_map = { + v["material"]: physical_name_to_dimtag_map[k][1] for k, v in bodies.items() + } + + palace_json_data["Model"]["Mesh"] = f"{name}.msh" + palace_json_data["Domains"]["Materials"] = [ + { + "Attributes": [material_to_attributes_map.get(material, None)], + "Permittivity": props["relative_permittivity"], + } + for material, props in used_materials.items() + ] + # TODO 3d volumes as pec???, not needed for capacitance + # palace_json_data['Boundaries']['PEC'] = { + # 'Attributes': [ + # physical_name_to_dimtag_map[pec][1] for pec in + # (set(k for k, v in physical_name_to_dimtag_map.items() if v[0] == 3) - set(bodies) - + # set(ground_layers)) # TODO same in Elmer?? + # ] + # } + palace_json_data["Boundaries"]["Ground"] = { + "Attributes": [physical_name_to_dimtag_map[layer][1] for layer in ground_layers] + } + palace_json_data["Boundaries"]["Terminal"] = [ + { + "Index": i, + "Attributes": [ + physical_name_to_dimtag_map[signal][1] for signal in signal_group + ], + } + for i, signal_group in enumerate(signals, 1) + ] + # TODO try do we get energy method without this?? + palace_json_data["Boundaries"]["Postprocessing"]["Capacitance"] = palace_json_data[ + "Boundaries" + ]["Terminal"] + + palace_json_data["Solver"]["Order"] = element_order + palace_json_data["Solver"]["Electrostatic"]["Save"] = len(signals) + if simulator_params is not None: + palace_json_data["Solver"]["Linear"] |= simulator_params + + with open(simulation_folder / f"{name}.json", "w", encoding="utf-8") as fp: + json.dump(palace_json_data, fp, indent=4) + + +def _palace(simulation_folder: Path, name: str, n_processes: int = 1): + """Run simulations with Palace.""" + palace = shutil.which("palace") + if palace is None: + raise RuntimeError("palace not found. Make sure it is available in your PATH.") + + json_file = simulation_folder / f"{Path(name).stem}.json" + run_async_with_event_loop( + execute_and_stream_output( + [palace, json_file] + if n_processes == 1 + else [palace, "-np", str(n_processes), json_file], + shell=False, + log_file_dir=simulation_folder, + log_file_str=json_file.stem + "_palace", + cwd=simulation_folder, + ) + ) + + +def _read_palace_results( + simulation_folder: Path, + mesh_filename: str, + ports: Iterable[str], + is_temporary: bool, +) -> ElectrostaticResults: + """Fetch results from successful Palace simulations.""" + raw_capacitance_matrix = read_csv( + simulation_folder / "postpro" / "terminal-Cm.csv", dtype=float + ).values[ + :, 1: + ] # remove index + return ElectrostaticResults( + capacitance_matrix={ + (iname, jname): raw_capacitance_matrix[i][j] + for (i, iname), (j, jname) in itertools.product( + enumerate(ports), enumerate(ports) + ) + }, + **( + {} + if is_temporary + else dict( + mesh_location=simulation_folder / mesh_filename, + field_file_location=simulation_folder + / "postpro" + / "paraview" + / "electrostatic" + / "electrostatic.pvd", + ) + ), + ) + + +def run_capacitive_simulation_palace( + component: gf.Component, + element_order: int = 1, + n_processes: int = 1, + layer_stack: LayerStack | None = None, + material_spec: RFMaterialSpec | None = None, + simulation_folder: Path | str | None = None, + simulator_params: Mapping[str, Any] | None = None, + mesh_parameters: dict[str, Any] | None = None, + mesh_file: Path | str | None = None, +) -> ElectrostaticResults: + """Run electrostatic finite element method simulations using + `Palace`_. + Returns the field solution and resulting capacitance matrix. + + .. note:: You should have `palace` in your PATH. + + Args: + component: Simulation environment as a gdsfactory component. + element_order: + Order of polynomial basis functions. + Higher is more accurate but takes more memory and time to run. + n_processes: Number of processes to use for parallelization + layer_stack: + :class:`~LayerStack` defining defining what layers to include in the simulation + and the material properties and thicknesses. + material_spec: + :class:`~RFMaterialSpec` defining material parameters for the ones used in ``layer_stack``. + simulation_folder: + Directory for storing the simulation results. + Default is a temporary directory. + simulator_params: Palace-specific parameters. This will be expanded to ``solver["Linear"]`` in + the Palace config, see `Palace documentation `_ + mesh_parameters: + Keyword arguments to provide to :func:`~Component.to_gmsh`. + mesh_file: Path to a ready mesh to use. Useful for reusing one mesh file. + By default a mesh is generated according to ``mesh_parameters``. + + .. _Palace: https://github.com/awslabs/palace + """ + + if layer_stack is None: + layer_stack = LayerStack( + layers={ + k: LAYER_STACK.layers[k] + for k in ( + "core", + "substrate", + "box", + ) + } + ) + if material_spec is None: + material_spec: RFMaterialSpec = { + "si": {"relative_permittivity": 11.45}, + "sio2": {"relative_permittivity": 1}, + "vacuum": {"relative_permittivity": 1}, + } + + temp_dir = TemporaryDirectory() + simulation_folder = Path(simulation_folder or temp_dir.name) + simulation_folder.mkdir(exist_ok=True, parents=True) + + filename = component.name + ".msh" + if mesh_file: + shutil.copyfile(str(mesh_file), str(simulation_folder / filename)) + else: + component.to_gmsh( + type="3D", + filename=simulation_folder / filename, + layer_stack=layer_stack, + n_threads=n_processes, + gmsh_version=2.2, # see https://mfem.org/mesh-formats/#gmsh-mesh-formats + **(mesh_parameters or {}), + ) + + # re-read the mesh + # `interruptible` works on gmsh versions >= 4.11.2 + gmsh.initialize( + **( + {"interruptible": False} + if "interruptible" in inspect.getfullargspec(gmsh.initialize).args + else {} + ) + ) + gmsh.merge(str(simulation_folder / filename)) + mesh_surface_entities = { + gmsh.model.getPhysicalName(*dimtag) + for dimtag in gmsh.model.getPhysicalGroups(dim=2) + } + + # Signals are converted to Boundaries + ground_layers = { + next(k for k, v in layer_stack.layers.items() if v.layer == port.layer) + for port in component.get_ports() + } # ports allowed only on metal + # TODO infer port delimiter from somewhere + port_delimiter = "__" + metal_surfaces = [ + e for e in mesh_surface_entities if any(ground in e for ground in ground_layers) + ] + # Group signal BCs by ports + metal_signal_surfaces_grouped = [ + [e for e in metal_surfaces if port in e] for port in component.ports + ] + metal_ground_surfaces = set(metal_surfaces) - set( + itertools.chain.from_iterable(metal_signal_surfaces_grouped) + ) + + ground_layers |= metal_ground_surfaces + + # dielectrics + bodies = { + k: { + "material": v.material, + } + for k, v in layer_stack.layers.items() + if port_delimiter not in k and k not in ground_layers + } + if background_tag := (mesh_parameters or {}).get("background_tag", "vacuum"): + bodies = {**bodies, background_tag: {"material": background_tag}} + + # TODO refactor to not require this map, the same information could be transferred with the variables above + physical_name_to_dimtag_map = { + gmsh.model.getPhysicalName(*dimtag): dimtag + for dimtag in gmsh.model.getPhysicalGroups() + } + gmsh.finalize() + + _generate_json( + simulation_folder, + component.name, + metal_signal_surfaces_grouped, + bodies, + ground_layers, + layer_stack, + material_spec, + element_order, + physical_name_to_dimtag_map, + background_tag, + simulator_params, + ) + _palace(simulation_folder, filename, n_processes) + results = _read_palace_results( + simulation_folder, + filename, + component.ports, + is_temporary=str(simulation_folder) == temp_dir.name, + ) + temp_dir.cleanup() + return results diff --git a/gplugins/palace/tests/test_palace.py b/gplugins/palace/tests/test_palace.py new file mode 100644 index 00000000..5fe60117 --- /dev/null +++ b/gplugins/palace/tests/test_palace.py @@ -0,0 +1,140 @@ +from math import inf + +import gdsfactory as gf +import pytest +from gdsfactory.component import Component +from gdsfactory.components.interdigital_capacitor_enclosed import ( + interdigital_capacitor_enclosed, +) +from gdsfactory.generic_tech import LAYER +from gdsfactory.technology import LayerStack +from gdsfactory.technology.layer_stack import LayerLevel + +from gplugins.palace import run_capacitive_simulation_palace + +layer_stack = LayerStack( + layers=dict( + substrate=LayerLevel( + layer=LAYER.WAFER, + thickness=500, + zmin=0, + material="Si", + mesh_order=99, + ), + bw=LayerLevel( + layer=LAYER.WG, + thickness=200e-3, + zmin=500, + material="Nb", + mesh_order=2, + ), + ) +) +material_spec = { + "Si": {"relative_permittivity": 11.45}, + "Nb": {"relative_permittivity": inf}, + "vacuum": {"relative_permittivity": 1}, +} + + +@pytest.fixture +@gf.cell +def geometry(): + simulation_box = [[-200, -200], [200, 200]] + c = gf.Component() + cap = c << interdigital_capacitor_enclosed( + metal_layer=LAYER.WG, gap_layer=LAYER.DEEPTRENCH, enclosure_box=simulation_box + ) + c.add_ports(cap.ports) + substrate = gf.components.bbox(bbox=simulation_box, layer=LAYER.WAFER) + c << substrate + c.flatten() + return c + + +def get_reasonable_mesh_parameters(c: Component): + return dict( + background_tag="vacuum", + background_padding=(0,) * 5 + (700,), + portnames=c.ports, + default_characteristic_length=200, + layer_portname_delimiter=(delimiter := "__"), + resolutions={ + "bw": { + "resolution": 15, + }, + "substrate": { + "resolution": 40, + }, + "vacuum": { + "resolution": 40, + }, + **{ + f"bw{delimiter}{port}": { + "resolution": 20, + "DistMax": 30, + "DistMin": 10, + "SizeMax": 14, + "SizeMin": 3, + } + for port in c.ports + }, + }, + ) + + +@pytest.mark.skip(reason="Palace not in CI") +def test_palace_capacitance_simulation_runs(geometry): + c = geometry + run_capacitive_simulation_palace( + c, + layer_stack=layer_stack, + material_spec=material_spec, + mesh_parameters=get_reasonable_mesh_parameters(c), + ) + + +@pytest.mark.skip(reason="TODO") +@pytest.mark.parametrize("n_processes", [(1), (2), (4)]) +def test_palace_capacitance_simulation_n_processes(geometry, n_processes): + c = geometry + run_capacitive_simulation_palace( + c, + layer_stack=layer_stack, + material_spec=material_spec, + n_processes=n_processes, + mesh_parameters=get_reasonable_mesh_parameters(c), + ) + + +@pytest.mark.skip(reason="TODO") +@pytest.mark.parametrize("element_order", [(1), (2), (3)]) +def test_palace_capacitance_simulation_element_order(geometry, element_order): + c = geometry + run_capacitive_simulation_palace( + c, + layer_stack=layer_stack, + material_spec=material_spec, + element_order=element_order, + mesh_parameters=get_reasonable_mesh_parameters(c), + ) + + +@pytest.mark.skip(reason="TODO") +def test_palace_capacitance_simulation_mesh_size_field(geometry): + pass + + +@pytest.mark.skip(reason="TODO") +def test_palace_capacitance_simulation_flip_chip(geometry): + pass + + +@pytest.mark.skip(reason="TODO") +def test_palace_capacitance_simulation_pyvist_plot(geometry): + pass + + +@pytest.mark.skip(reason="TODO") +def test_palace_capacitance_simulation_cdict_form(geometry): + pass diff --git a/gplugins/tidy3d/get_simulation.py b/gplugins/tidy3d/get_simulation.py index 6f3945ba..f5ba3563 100644 --- a/gplugins/tidy3d/get_simulation.py +++ b/gplugins/tidy3d/get_simulation.py @@ -275,8 +275,8 @@ def get_simulation( for layer, thickness in layer_to_thickness.items(): if layer in layer_to_material and layer in component_layers: - zmin = layer_to_zmin[layer] if is_3d else 0 - zmax = zmin + thickness if is_3d else 0 + zmin = layer_to_zmin[layer] if is_3d else -td.inf + zmax = zmin + thickness if is_3d else td.inf material_name = layer_to_material[layer] @@ -405,7 +405,7 @@ def get_simulation( print("Effective index of computed modes: ", np.array(modes.n_eff)) if is_3d: - fig, axs = plt.subplots(num_modes, 2, figsize=(12, 12)) + fig, axs = plt.subplots(num_modes, 2, figsize=(12, 12), tight_layout=True) for mode_ind in range(num_modes): ms.plot_field( "Ey", "abs", f=freq0, mode_index=mode_ind, ax=axs[mode_ind, 0] @@ -413,13 +413,18 @@ def get_simulation( ms.plot_field( "Ez", "abs", f=freq0, mode_index=mode_ind, ax=axs[mode_ind, 1] ) + axs[mode_ind, 0].set_title(f"|Ey|: mode_index={mode_ind}") + axs[mode_ind, 1].set_title(f"|Ez|: mode_index={mode_ind}") else: - fig, axs = plt.subplots(num_modes, 3, figsize=(12, 12)) + fig, axs = plt.subplots(num_modes, 3, figsize=(12, 12), tight_layout=True) axs = np.atleast_2d(axs) for mode_ind in range(num_modes): - axs[mode_ind, 0].plot(modes.Ex.sel(mode_index=mode_ind).y.abs) - axs[mode_ind, 1].plot(modes.Ey.sel(mode_index=mode_ind).y.abs) - axs[mode_ind, 2].plot(modes.Ez.sel(mode_index=mode_ind).y.abs) + modes.Ex.sel(mode_index=mode_ind).abs.plot(ax=axs[mode_ind, 0]) + modes.Ey.sel(mode_index=mode_ind).abs.plot(ax=axs[mode_ind, 1]) + modes.Ez.sel(mode_index=mode_ind).abs.plot(ax=axs[mode_ind, 2]) + axs[mode_ind, 0].set_title(f"|Ex|: mode_index={mode_ind}") + axs[mode_ind, 1].set_title(f"|Ey|: mode_index={mode_ind}") + axs[mode_ind, 2].set_title(f"|Ez|: mode_index={mode_ind}") plt.show() return sim @@ -513,18 +518,18 @@ def plot_simulation_xz( if __name__ == "__main__": # c = gf.c.taper_sc_nc(length=10) - c = gf.components.taper_strip_to_ridge_trenches() - s = get_simulation(c, plot_modes=False) + # c = gf.components.taper_strip_to_ridge_trenches() + # s = get_simulation(c, plot_modes=False) # c = gf.components.mmi1x2() # c = gf.components.bend_circular(radius=2) # c = gf.components.crossing() # c = gf.c.straight_rib() - # c = gf.c.straight(length=3) + c = gf.c.straight(length=3) # sim = get_simulation(c, plot_modes=True, is_3d=True, sidewall_angle_deg=30) - # sim = get_simulation(c, dilation=-0.2, is_3d=False) + sim = get_simulation(c, is_3d=False, plot_modes=True) # sim = get_simulation(c, is_3d=True) # plot_simulation(sim) @@ -532,8 +537,8 @@ def plot_simulation_xz( # filepath = pathlib.Path(__file__).parent / "extra" / "wg2d.json" # filepath.write_text(sim.json()) - # sim.plotly(z=0) - # plot_simulation_yz(s, wavelength=1.55, y=1) + # sim.plotxy(z=0) + # plot_simulation_yz(sim, wavelength=1.55, y=1) # fig = plt.figure(figsize=(11, 4)) # gs = mpl.gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 1.4]) # ax1 = fig.add_subplot(gs[0, 0]) diff --git a/gplugins/tidy3d/modes.py b/gplugins/tidy3d/modes.py index defb9d1b..72e7e972 100644 --- a/gplugins/tidy3d/modes.py +++ b/gplugins/tidy3d/modes.py @@ -21,8 +21,7 @@ import pydantic import tidy3d as td import xarray -from gdsfactory.config import logger -from gdsfactory.pdk import get_modes_path +from gdsfactory.config import PATH, logger from gdsfactory.typings import PathType from pydantic import BaseModel from tidy3d.plugins import waveguide @@ -40,7 +39,7 @@ def custom_serializer(data: str | float | BaseModel) -> str: return data # If data is a float, convert it to a string. - if isinstance(data, float | int): + if isinstance(data, float | int | pathlib.Path): return str(data) # If data is an instance of Pydantic's BaseModel, serialize it to JSON. @@ -99,7 +98,7 @@ class Waveguide(pydantic.BaseModel): precision: computation precision. grid_resolution: wavelength resolution of the computation grid. max_grid_scaling: grid scaling factor in cladding regions. - cache: controls the use of cached results. + cache_path: Optional path to the cache directory. None disables cache. overwrite: overwrite cache. :: @@ -151,7 +150,7 @@ class Waveguide(pydantic.BaseModel): precision: Precision = "double" grid_resolution: int = 20 max_grid_scaling: float = 1.2 - cache: bool = True + cache_path: PathType | None = PATH.modes overwrite: bool = False _cached_data = pydantic.PrivateAttr() @@ -166,15 +165,10 @@ class Config: def _fix_wavelength_type(cls, value): return np.array(value, dtype=float) - @property - def cache_path(self) -> PathType | None: - """Cache directory""" - return get_modes_path() - @property def filepath(self) -> pathlib.Path | None: """Cache file path""" - if not self.cache: + if not self.cache_path: return None cache_path = pathlib.Path(self.cache_path) cache_path.mkdir(exist_ok=True, parents=True) diff --git a/gplugins/tidy3d/tests/test_modes_waveguide.py b/gplugins/tidy3d/tests/test_modes_waveguide.py index 38b0d1f3..f5de80ab 100644 --- a/gplugins/tidy3d/tests/test_modes_waveguide.py +++ b/gplugins/tidy3d/tests/test_modes_waveguide.py @@ -12,7 +12,7 @@ def test_neff() -> None: core_thickness=0.22, core_material="si", clad_material="sio2", - cache=False, + cache_path=None, ) n_eff = wg.n_eff[0].real assert np.isclose(n_eff, 2.447, rtol=0.1), n_eff @@ -26,7 +26,7 @@ def test_neff_high_accuracy() -> None: core_material="si", clad_material="sio2", grid_resolution=40, - cache=False, + cache_path=None, ) n_eff = wg.n_eff[0].real assert np.isclose(n_eff, 2.447, rtol=0.01), n_eff diff --git a/gplugins/utils/get_capacitance.py b/gplugins/utils/get_capacitance.py new file mode 100644 index 00000000..d9a5278f --- /dev/null +++ b/gplugins/utils/get_capacitance.py @@ -0,0 +1,76 @@ +from collections.abc import Mapping +from functools import partial +from pathlib import Path +from typing import Any + +import gdsfactory as gf +from gdsfactory.typings import ComponentSpec + +from gplugins.elmer.get_capacitance import run_capacitive_simulation_elmer +from gplugins.palace.get_capacitance import ( + run_capacitive_simulation_palace, +) +from gplugins.typings import ElectrostaticResults + + +def get_capacitance_path() -> Path: + return gf.config.PATH.capacitance + + +def get_capacitance( + component: ComponentSpec, + simulator: str = "elmer", + simulator_params: Mapping[str, Any] | None = None, + simulation_folder: Path | str | None = None, + **kwargs, +) -> ElectrostaticResults: + """Simulate component with an electrostatic simulation and return capacitance matrix. + For more details, see Chapter 2.9 `Capacitance matrix` in `N. Savola, “Design and modelling of long-coherence + qubits using energy participation ratios” `_. + + Args: + component: component or component factory. + simulator: Simulator to use. The choices are 'elmer' or 'palace'. Both require manual install. + This changes the format of ``simulator_params``. + simulator_params: Simulator-specific params as a dictionary. See template files for more details. + Has reasonable defaults. + simulation_folder: Directory for storing the simulation results. Default is a temporary directory. + **kwargs: Simulation settings propagated to inner :func:`~run_capacitive_simulation_elmer` or + :func:`~run_capacitive_simulation_palace` implementation. + """ + simulation_folder = Path(simulation_folder or get_capacitance_path()) + component = gf.get_component(component) + + simulation_folder = ( + simulation_folder / component.function_name + if hasattr(component, "function_name") + else simulation_folder + ) + simulation_folder.mkdir(exist_ok=True, parents=True) + + match simulator: + case "elmer": + return run_capacitive_simulation_elmer( + component, + simulation_folder=simulation_folder, + simulator_params=simulator_params, + **kwargs, + ) + case "palace": + return run_capacitive_simulation_palace( + component, + simulation_folder=simulation_folder, + simulator_params=simulator_params, + **kwargs, + ) + case _: + raise UserWarning(f"{simulator=!r} not implemented!") + + +get_capacitance_elmer = partial(get_capacitance, tool="elmer") +get_capacitance_palace = partial(get_capacitance, tool="palace") + + +# if __name__ == "__main__": +# c = gf.components.interdigital_capacitor() +# print(get_capacitance(c)) diff --git a/gplugins/utils/get_sparameters_path.py b/gplugins/utils/get_sparameters_path.py index 37b08c10..90a7684c 100644 --- a/gplugins/utils/get_sparameters_path.py +++ b/gplugins/utils/get_sparameters_path.py @@ -7,8 +7,8 @@ import gdsfactory as gf import numpy as np +from gdsfactory.config import GDSDIR_TEMP, PATH from gdsfactory.name import clean_value -from gdsfactory.pdk import get_sparameters_path from gdsfactory.typings import ComponentSpec, PathType @@ -28,7 +28,7 @@ def get_component_hash(component: gf.Component) -> str: def _get_sparameters_path( component: ComponentSpec, - dirpath: PathType | None = None, + dirpath: PathType | None = PATH.sparameters, **kwargs, ) -> Path: """Return Sparameters npz filepath hashing simulation settings for \ @@ -41,7 +41,8 @@ def _get_sparameters_path( kwargs: simulation settings. """ - dirpath = dirpath or get_sparameters_path() + dirpath = dirpath or GDSDIR_TEMP / "sparameters" + dirpath = pathlib.Path(dirpath) component = gf.get_component(component) dirpath = pathlib.Path(dirpath) diff --git a/gplugins/web/main.py b/gplugins/web/main.py index bb91e768..b4dfd6a7 100644 --- a/gplugins/web/main.py +++ b/gplugins/web/main.py @@ -1,4 +1,5 @@ import base64 +import contextlib import importlib import os import pathlib @@ -13,9 +14,10 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from gdsfactory.cell import Settings -from gdsfactory.config import CONF, GDSDIR_TEMP +from gdsfactory.config import CONF, GDSDIR_TEMP, pdks from gdsfactory.watch import FileWatcher from loguru import logger +from pydantic import BaseModel from starlette.routing import WebSocketRoute from gplugins.config import PATH @@ -36,8 +38,8 @@ templates = Jinja2Templates(directory=PATH.web / "templates") -def load_pdk() -> gf.Pdk: - pdk = os.environ.get("PDK", "generic") +def load_pdk(pdk: str | None = None) -> gf.Pdk: + pdk = pdk or os.environ.get("PDK", "generic") if pdk == "generic": active_pdk = gf.get_active_pdk() @@ -108,6 +110,28 @@ async def load_schematic(): return data +@app.get("/pdk-list", response_model=list[str]) +async def get_pdk_list() -> list[str]: + pdks_installed = [] + for pdk in pdks: + with contextlib.suppress(ImportError, AttributeError): + m = importlib.import_module(pdk) + m.PDK + pdks_installed.append(pdk) + return pdks_installed + + +class PDKItem(BaseModel): + pdk: str + + +@app.post("/pdk-set") +async def set_pdk(pdk_item: PDKItem): + pdk = pdk_item.pdk + load_pdk(pdk) + return {"message": f"PDK {pdk} set successfully!"} + + @app.get("/gds_list", response_class=HTMLResponse) async def gds_list(request: Request): """List all saved GDS files.""" @@ -200,9 +224,12 @@ async def view_cell(request: Request, cell_name: str, variant: str | None = None ) -def _parse_value(value: str): +def _parse_value(value: str) -> str | dict | list | int | float | bool: if not value.startswith("{") and not value.startswith("["): - return value + try: + return float(value) + except ValueError: + return value try: return orjson.loads(value.replace("'", '"')) except orjson.JSONDecodeError as e: @@ -214,6 +241,7 @@ async def update_cell(request: Request, cell_name: str): """Cell name is the name of the PCell function.""" data = await request.form() settings = {k: _parse_value(v) for k, v in data.items() if v != ""} + if not settings: return RedirectResponse( f"/view/{cell_name}", diff --git a/gplugins/web/server.py b/gplugins/web/server.py index 7b228c93..90f83485 100755 --- a/gplugins/web/server.py +++ b/gplugins/web/server.py @@ -34,8 +34,10 @@ def __init__(self, *args, **kwargs) -> None: # self.url = params["gds_file"].replace('/', '\\') # self.layer_props = params.get("layer_props", None) lyp_path = GDSDIR_TEMP / "layer_props.lyp" - gf.get_active_pdk().layer_views.to_lyp(lyp_path) - self.layer_props = lyp_path + active_pdk = gf.get_active_pdk() + if active_pdk.layer_views: + active_pdk.layer_views.to_lyp(lyp_path) + self.layer_props = lyp_path # path_params = args[0]['path_params'] # cell_name = path_params["cell_name"] cell_name = params["variant"] @@ -196,10 +198,12 @@ async def reader(self, websocket, data: str) -> None: self.wheel_event(self.layout_view.send_wheel_event, js) -def get_layer_properties() -> str: +def get_layer_properties() -> str | None: lyp_path = GDSDIR_TEMP / "layers.lyp" - lyp_path = gf.get_active_pdk().layer_views.to_lyp(lyp_path) - return str(lyp_path) + active_pdk = gf.get_active_pdk() + if active_pdk.layer_views: + lyp_path = active_pdk.layer_views.to_lyp(lyp_path) + return str(lyp_path) def get_layout_view(component: gf.Component) -> lay.LayoutView: @@ -209,6 +213,7 @@ def get_layout_view(component: gf.Component) -> lay.LayoutView: layout_view = lay.LayoutView() layout_view.load_layout(str(gds_path)) lyp_path = get_layer_properties() - layout_view.load_layer_props(str(lyp_path)) + if lyp_path: + layout_view.load_layer_props(str(lyp_path)) layout_view.max_hier() return layout_view diff --git a/gplugins/web/templates/header.html.j2 b/gplugins/web/templates/header.html.j2 index 7a0a010f..ad233f9f 100644 --- a/gplugins/web/templates/header.html.j2 +++ b/gplugins/web/templates/header.html.j2 @@ -1,5 +1,6 @@ + @@ -8,4 +9,76 @@ Gdsfactory - {{ title | default("Page") }} + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index 7e735575..a63806f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Operating System :: OS Independent" ] dependencies = [ - "gdsfactory[cad]>=7.0.2", + "gdsfactory[cad]>=7.3.2", "pint" ] description = "gdsfactory plugins" @@ -23,7 +23,7 @@ license = {file = "LICENSE"} name = "gplugins" readme = "README.md" requires-python = ">=3.10" -version = "0.3.0" +version = "0.3.1" [project.optional-dependencies] database = [ @@ -56,10 +56,11 @@ docs = [ "jupytext", "autodoc_pydantic", "matplotlib", - "jupyter-book==0.15.1" + "jupyter-book==0.15.1", + "pyvista[jupyter]" ] femwell = [ - "femwell>=0.1.6,<0.3.0" + "femwell>=0.1.6,<0.3.1" ] gmsh = [ "gmsh", @@ -70,7 +71,7 @@ gmsh = [ "pyvista", "trimesh", "shapely", - "meshwell>=0.0.9,<0.3.0" + "meshwell>=0.0.9,<0.3.1" ] klayout = [ "kfactory[git,ipy]==0.7.5"