Source code for gplugins.femwell.mode_solver

import tempfile
import time
from pathlib import Path
from typing import Any

import gdsfactory as gf
import numpy as np
from femwell.maxwell.waveguide import Modes, compute_modes
from gdsfactory.pdk import get_layer_stack
from gdsfactory.technology import LayerStack
from gdsfactory.typings import ComponentSpec, CrossSectionSpec, PathType
from meshwell.cad import cad
from meshwell.mesh import mesh as mesh_func
from shapely.geometry import LineString
from skfem import (
    Basis,
    ElementTriN2,
    ElementTriP0,
    ElementTriP2,
    Mesh,
)

from gplugins.meshwell import get_meshwell_cross_section

MESH_FILENAME = "mesh.msh"


def load_mesh_basis(mesh_filename: PathType):
    mesh = Mesh.load(mesh_filename)
    basis = Basis(mesh, ElementTriN2() * ElementTriP2())
    basis0 = basis.with_element(ElementTriP0())
    return mesh, basis0


[docs] def compute_cross_section_modes( cross_section: CrossSectionSpec, layer_stack: LayerStack, wavelength: float = 1.55, num_modes: int = 4, order: int = 1, radius: float = np.inf, wafer_padding: float = 2.0, **kwargs: Any, ) -> Modes: """Calculate effective index of a cross-section. Defines a "straight" component of the cross_section, and calls compute_component_slice_modes. Args: cross_section: gdsfactory cross_section. layer_stack: gdsfactory layer_stack. wavelength: in um. num_modes: to compute. order: order of the mesh elements. 1: linear, 2: quadratic. radius: defaults to inf. wafer_padding: in um. kwargs: kwargs for compute_component_slice_modes Keyword Args: solver: can be slepc or scipy. resolution_specs (Dict): meshwell resolution specifications. Format: {"layername": [ConstantInField(resolution=float, apply_to="surfaces")]} default_characteristic_length (float): default gmsh characteristic length. background_tag (str): name of the background layer to add (default: no background added). background_remeshing_file (Path): optional background mesh file for refinement. global_scaling (float): global scaling factor. verbosity (int): GMSH verbosity level. """ # Get meshable component from cross-section c = gf.components.straight(length=10, cross_section=cross_section) dx = c.xsize dy = c.ysize xsection_bounds = [ [dx / 2, dy - wafer_padding], [dx / 2, dy + wafer_padding], ] # Mesh as component return compute_component_slice_modes( component=c, xsection_bounds=xsection_bounds, layer_stack=layer_stack, wavelength=wavelength, num_modes=num_modes, order=order, radius=radius, wafer_padding=wafer_padding, **kwargs, )
_material_name_to_index = { "si": 3.48, "sio2": 1.44, "sin": 2.0, } def compute_component_slice_modes( component: ComponentSpec, xsection_bounds: tuple[tuple[float, float], tuple[float, float]], layer_stack: LayerStack, wavelength: float = 1.55, num_modes: int = 4, order: int = 1, wafer_padding: float = 2.0, radius: float = np.inf, metallic_boundaries: bool = False, n_guess: float | None = None, solver: str = "scipy", material_name_to_index: dict[str, float] | None = None, **kwargs: Any, ) -> Modes: """Calculate effective index of component slice. Args: component: gdsfactory component. xsection_bounds: xy line defining where to take component cross_section. layer_stack: gdsfactory layer_stack. wavelength: wavelength (um). num_modes: number of modes to return. order: order of the mesh elements. 1: linear, 2: quadratic. wafer_padding: padding beyond bbox to add to WAFER layers. radius: bend radius of the cross-section. metallic_boundaries: if True, will set the boundaries to be metallic. n_guess: initial guess for the effective index. solver: can be slepc or scipy. material_name_to_index: dictionary mapping material names to refractive indices. kwargs: kwargs for meshwell.mesh.mesh Keyword Args: resolution_specs (Dict): meshwell resolution specifications. Format: {"layername": [ConstantInField(resolution=float, apply_to="surfaces")]} default_characteristic_length (float): default gmsh characteristic length. background_tag (str): name of the background layer to add (default: no background added). background_remeshing_file (Path): optional background mesh file for refinement. global_scaling (float): global scaling factor. verbosity (int): GMSH verbosity level. wafer_layer: layer to use for WAFER padding. """ material_name_to_index = material_name_to_index or _material_name_to_index # Mesh # Create cross-section line from xsection_bounds cross_section_line = LineString(xsection_bounds) # Generate 2D cross-section surfaces instead of 3D prisms surfaces = get_meshwell_cross_section( component=component, line=cross_section_line, layer_stack=layer_stack, wafer_padding=wafer_padding, ) with tempfile.TemporaryDirectory() as tmpdirname: tmpdirname = Path(tmpdirname) cad(entities_list=surfaces, output_file=tmpdirname / "temp.xao") mesh_func( input_file=tmpdirname / "temp.xao", output_file=MESH_FILENAME, default_characteristic_length=1000, dim=2, verbosity=10, **kwargs, ) # Assign materials to mesh elements mesh, basis0 = load_mesh_basis(MESH_FILENAME) epsilon = basis0.zeros(dtype=complex) for layername, layer in layer_stack.layers.items(): if layername in mesh.subdomains.keys(): epsilon[basis0.get_dofs(elements=layername)] = ( material_name_to_index[layer.material] ** 2 ) if "background_tag" in kwargs: epsilon[basis0.get_dofs(elements=kwargs["background_tag"])] = ( material_name_to_index[kwargs["background_tag"]] ** 2 ) return compute_modes( basis0, epsilon, wavelength=wavelength, mu_r=1, num_modes=num_modes, order=order, radius=radius, solver=solver, n_guess=n_guess, metallic_boundaries=metallic_boundaries, ) if __name__ == "__main__": import matplotlib.pyplot as plt from meshwell.resolution import ConstantInField start = time.time() filtered_layer_stack = LayerStack( layers={ k: get_layer_stack().layers[k] for k in ( "core", "clad", "slab90", "box", ) } ) filtered_layer_stack.layers["core"].thickness = 0.2 # New meshwell resolution format resolution_specs = { "core": [ConstantInField(resolution=0.04, apply_to="surfaces")], "clad": [ConstantInField(resolution=0.4, apply_to="surfaces")], "box": [ConstantInField(resolution=0.4, apply_to="surfaces")], "slab90": [ConstantInField(resolution=0.1, apply_to="surfaces")], } cross_section = False cross_section = True if cross_section: modes = compute_cross_section_modes( cross_section="rib", layer_stack=filtered_layer_stack, wavelength=1.55, num_modes=4, order=1, radius=np.inf, resolution_specs=resolution_specs, ) mode = modes[0] mode.show(mode.E.real, colorbar=True, direction="x") else: component = gf.components.coupler_full(dw=0) component.show() modes = compute_component_slice_modes( component, [[0, -3], [0, 3]], layer_stack=filtered_layer_stack, wavelength=1.55, num_modes=4, order=1, radius=np.inf, resolution_specs=resolution_specs, ) print(modes) print(modes[0].te_fraction) modes[0].show(np.real(modes[0].E)) modes[0].show(np.imag(modes[0].E)) modes[0].show(np.real(modes[0].H)) modes[0].show(np.imag(modes[0].H)) integrals = np.zeros((len(modes),) * 2, dtype=complex) for i in range(len(modes)): for j in range(len(modes)): integrals[i, j] = modes[i].calculate_overlap(modes[j]) plt.imshow(np.real(integrals)) plt.colorbar() plt.show() # Create basis to select a certain simulation extent def sel_fun(x): return (x[0] < 0) * (x[0] > -1) * (x[1] > 0) * (x[1] < 0.5) print(modes.sorted(lambda mode: mode.calculate_power(elements=sel_fun)))