Finite-element mode solver

Finite-element mode solver#

You can mesh any component cross-section and solve the PDEs thanks to femwell mode - solver.

Unlike other mode solvers, this actually uses the component geometry instead of a hardcoded geometry.

You can directly compute the modes of a Gdsfactory cross-section (internally, it defines a “uz” mesh perpendicular to a straight component with the provided cross-section).

You can also downsample layers from the LayerStack, and modify both the cross-section and LayerStack prior to simulation to change the geometry. You can also define refractive indices on the active PDK.

import logging
import sys

import gdsfactory as gf
import matplotlib.pyplot as plt
import numpy as np
from gdsfactory.cross_section import rib
from gdsfactory.generic_tech import LAYER_STACK, get_generic_pdk
from gdsfactory.technology import LayerStack
from rich.logging import RichHandler
from tqdm.auto import tqdm

from gplugins.femwell.mode_solver import compute_cross_section_modes

gf.config.rich_output()
PDK = get_generic_pdk()
PDK.activate()

logger = logging.getLogger()
logger.removeHandler(sys.stderr)
logging.basicConfig(level="WARNING", datefmt="[%X]", handlers=[RichHandler()])
filtered_layer_stack = LayerStack(
    layers={
        k: LAYER_STACK.layers[k]
        for k in (
            "core",
            "clad",
            "slab90",
            "box",
            # "substrate",
        )
    }
)

filtered_layer_stack.layers[
    "core"
].thickness = 0.22  # Perturb the layer_stack before simulating

filtered_layer_stack.layers[
    "slab90"
].thickness = 0.09  # Perturb the layer_stack before simulating

resolutions = {
    "core": {"resolution": 0.02, "distance": 1, "SizeMax": 0.2},
    # "clad": {"resolution": 0.2, "distance": 1},
    # "box": {"resolution": 0.2, "distance": 1},
    # "slab90": {"resolution": 0.05, "distance": 1},
}
modes = compute_cross_section_modes(
    cross_section=rib(width=0.6),
    layer_stack=filtered_layer_stack,
    wavelength=1.55,
    num_modes=1,
    resolutions=resolutions,
    n_guess=2.630929889650573,
)

The solver returns the list of modes

mode = modes[0]
mode.show(mode.E.real, colorbar=True, direction="x")

You can use them as inputs to other femwell mode solver functions to inspect or analyze the modes:

print(modes[0].te_fraction)
print(modes[0].n_eff)

Sweep waveguide width#

widths = np.linspace(0.2, 2, 10)
num_modes = 4
all_neffs = np.zeros((widths.shape[0], num_modes))
all_te_fracs = np.zeros((widths.shape[0], num_modes))


for i, width in enumerate(tqdm(widths)):
    modes = compute_cross_section_modes(
        cross_section=gf.cross_section.strip(width=width),
        layer_stack=filtered_layer_stack,
        wavelength=1.55,
        num_modes=num_modes,
        resolutions=resolutions,
        wafer_padding=2,
        solver="scipy",
    )
    all_neffs[i] = np.real([mode.n_eff for mode in modes])
    all_te_fracs[i, :] = [mode.te_fraction for mode in modes]
all_neffs = np.real(all_neffs)
plt.xlabel("Width of waveguide  µm")
plt.ylabel("Effective refractive index")
plt.ylim(1.444, np.max(all_neffs) + 0.1 * (np.max(all_neffs) - 1.444))
for lams, te_fracs in zip(all_neffs.T, all_te_fracs.T):
    plt.plot(widths, lams, c="k")
    plt.scatter(widths, lams, c=te_fracs, cmap="cool")
plt.colorbar().set_label("TE fraction")