Skip to content

API Design

Meshing

gplugins.meshwell.get_meshwell_prisms(component, layer_stack, wafer_layer=LAYER.WAFER, wafer_padding=0.0, name_by='layer')

Convert LayerStack + Component to meshwell PolyPrism objects.

Source code in gplugins/meshwell/get_meshwell_3D.py
def get_meshwell_prisms(
    component: gf.Component,
    layer_stack: gf.technology.LayerStack,
    wafer_layer: gf.typings.Layer | None = LAYER.WAFER,
    wafer_padding: float | None = 0.0,
    name_by: Literal["layer", "material"] = "layer",
) -> List[PolyPrism]:
    """Convert LayerStack + Component to meshwell PolyPrism objects."""
    prisms = []

    if wafer_padding is not None and wafer_layer is not None:
        component = add_padding_container(
            component=component,
            function=partial(add_padding, layers=(wafer_layer,), default=wafer_padding),
        )

    # Iterate through each layer in the stack
    for layer_name, layer_level in layer_stack.layers.items():
        # Get shapes for this layer from the component
        region = layer_level.layer.get_shapes(component)

        # Skip if no shapes found
        if region.is_empty():
            continue

        # Convert kfactory Region to Shapely polygons
        shapely_polygons = region_to_shapely_polygons(region)

        # Skip if no valid polygons
        if not shapely_polygons:
            continue

        # Build buffer dictionary from layer level properties
        buffers = build_buffer_dict_from_layer_level(layer_level)

        # Create PolyPrism object
        if name_by == "layer":
            physical_name = layer_name
        elif name_by == "material":
            physical_name = layer_level.material
        else:
            raise ValueError("name_by must be 'layer' or 'material'")
        prism = PolyPrism(
            polygons=shapely_polygons,
            buffers=buffers,
            physical_name=physical_name,
            mesh_order=layer_level.mesh_order,
            mesh_bool=True,
            additive=False,
        )

        prisms.append(prism)

    return prisms

Mode Solvers

Mode solver tidy3d

gplugins.tidy3d.modes.Waveguide

Bases: BaseModel

Waveguide Model.

All dimensions must be specified in μm (1e-6 m).

Parameters:

Name Type Description Default
wavelength

wavelength in free space.

required
core_width

waveguide core width.

required
core_thickness

waveguide core thickness (height).

required
core_material

core material. One of: - string: material name. - float: refractive index. - float, float: refractive index real and imaginary part. - td.Medium: tidy3d medium. - function: function of wavelength.

required
clad_material

top cladding material.

required
box_material

bottom cladding material.

required
slab_thickness

thickness of the slab region in a rib waveguide.

required
clad_thickness

thickness of the top cladding.

required
box_thickness

thickness of the bottom cladding.

required
side_margin

domain extension to the side of the waveguide core.

required
sidewall_angle

angle of the core sidewall w.r.t. the substrate normal.

required
sidewall_thickness

thickness of a layer on the sides of the waveguide core to model side-surface losses.

required
sidewall_k

absorption coefficient added to the core material index on the side-surface layer.

required
surface_thickness

thickness of a layer on the top of the waveguide core and slabs to model top-surface losses.

required
surface_k

absorption coefficient added to the core material index on the top-surface layer.

required
bend_radius

radius to simulate circular bend.

required
target_neff

target effective index for the mode solver. Defaults to the real part of the core refractive index if not specified.

required
num_modes

number of modes to compute.

required
group_index_step

if set to True, indicates that the group index must also be calculated. If set to a positive float it defines the fractional frequency step used for the numerical differentiation of the effective index.

required
precision

computation precision.

required
grid_resolution

wavelength resolution of the computation grid.

required
max_grid_scaling

grid scaling factor in cladding regions.

required
cache_path

Optional path to the cache directory. None disables cache.

required
overwrite

overwrite cache.

required

::

________________________________________________
                                        ^
                                        ¦
                                        ¦
                                  clad_thickness
               |<--core_width-->|       ¦
                                        ¦
               .________________.      _v_
               |       ^        |
<-side_margin->|       ¦        |
               |       ¦        |
_______________'       ¦        '_______________
      ^          core_thickness
      ¦                ¦
slab_thickness         ¦
      ¦                ¦
      v                v
________________________________________________
                       ^
                       ¦
                 box_thickness
                       ¦
                       v
________________________________________________
Source code in gplugins/tidy3d/modes.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
class Waveguide(BaseModel, extra="forbid", arbitrary_types_allowed=True):
    """Waveguide Model.

    All dimensions must be specified in μm (1e-6 m).

    Parameters:
        wavelength: wavelength in free space.
        core_width: waveguide core width.
        core_thickness: waveguide core thickness (height).
        core_material: core material. One of:
            - string: material name.
            - float: refractive index.
            - float, float: refractive index real and imaginary part.
            - td.Medium: tidy3d medium.
            - function: function of wavelength.
        clad_material: top cladding material.
        box_material: bottom cladding material.
        slab_thickness: thickness of the slab region in a rib waveguide.
        clad_thickness: thickness of the top cladding.
        box_thickness: thickness of the bottom cladding.
        side_margin: domain extension to the side of the waveguide core.
        sidewall_angle: angle of the core sidewall w.r.t. the substrate
            normal.
        sidewall_thickness: thickness of a layer on the sides of the
            waveguide core to model side-surface losses.
        sidewall_k: absorption coefficient added to the core material
            index on the side-surface layer.
        surface_thickness: thickness of a layer on the top of the
            waveguide core and slabs to model top-surface losses.
        surface_k: absorption coefficient added to the core material
            index on the top-surface layer.
        bend_radius: radius to simulate circular bend.
        target_neff: target effective index for the mode solver. Defaults
            to the real part of the core refractive index if not specified.
        num_modes: number of modes to compute.
        group_index_step: if set to `True`, indicates that the group
            index must also be calculated. If set to a positive float
            it defines the fractional frequency step used for the
            numerical differentiation of the effective index.
        precision: computation precision.
        grid_resolution: wavelength resolution of the computation grid.
        max_grid_scaling: grid scaling factor in cladding regions.
        cache_path: Optional path to the cache directory. None disables cache.
        overwrite: overwrite cache.

    ::

        ________________________________________________
                                                ^
                                                ¦
                                                ¦
                                          clad_thickness
                       |<--core_width-->|       ¦
                                                ¦
                       .________________.      _v_
                       |       ^        |
        <-side_margin->|       ¦        |
                       |       ¦        |
        _______________'       ¦        '_______________
              ^          core_thickness
              ¦                ¦
        slab_thickness         ¦
              ¦                ¦
              v                v
        ________________________________________________
                               ^
                               ¦
                         box_thickness
                               ¦
                               v
        ________________________________________________
    """

    wavelength: float | Sequence[float] | Any
    core_width: float
    core_thickness: float
    core_material: MaterialSpecTidy3d
    clad_material: MaterialSpecTidy3d
    box_material: MaterialSpecTidy3d | None = None
    slab_thickness: float = 0.0
    clad_thickness: float | None = None
    box_thickness: float | None = None
    side_margin: float | None = None
    sidewall_angle: float = 0.0
    sidewall_thickness: float = 0.0
    sidewall_k: float = 0.0
    surface_thickness: float = 0.0
    surface_k: float = 0.0
    bend_radius: float | None = None
    target_neff: float | None = None
    num_modes: int = 2
    group_index_step: bool | float = False
    precision: Precision = "double"
    grid_resolution: int = 20
    max_grid_scaling: float = 1.2
    cache_path: PathType | None = PATH.modes
    overwrite: bool = False

    _cached_data = pydantic.PrivateAttr()
    _waveguide = pydantic.PrivateAttr()

    @pydantic.validator("wavelength")
    def _fix_wavelength_type(cls, v: Any) -> NDArrayF:
        return np.array(v, dtype=float)

    @property
    def filepath(self) -> pathlib.Path | None:
        """Cache file path."""
        if not self.cache_path:
            return None
        cache_path = pathlib.Path(self.cache_path)
        cache_path.mkdir(exist_ok=True, parents=True)

        settings = [
            f"{setting}={custom_serializer(getattr(self, setting))}"
            for setting in sorted(self.__fields__.keys())
        ]
        named_args_string = "_".join(settings)
        h = hashlib.md5(named_args_string.encode()).hexdigest()[:16]
        return cache_path / f"{self.__class__.__name__}_{h}.npz"

    def _resolve_target_neff(self, n_core: complex) -> float:
        """Return target_neff if set, otherwise fall back to n_core.real."""
        return self.target_neff if self.target_neff is not None else n_core.real

    @property
    def waveguide(self):
        """Tidy3D waveguide used by this instance."""
        # if (not hasattr(self, "_waveguide")
        #         or isinstance(self.core_material, td.CustomMedium)):
        if not hasattr(self, "_waveguide"):
            # To include a dn -> custom medium
            if isinstance(self.core_material, td.CustomMedium | td.Medium):
                core_medium = self.core_material
            else:
                core_medium = get_medium(self.core_material)

            if isinstance(self.clad_material, td.CustomMedium | td.Medium):
                clad_medium = self.clad_material
            else:
                clad_medium = get_medium(self.clad_material)

            if self.box_material:
                if isinstance(self.box_material, td.CustomMedium | td.Medium):
                    box_medium = self.box_material
                else:
                    box_medium = get_medium(self.box_material)
            else:
                box_medium = None

            freq0 = td.C_0 / np.mean(self.wavelength)
            n_core = core_medium.eps_model(freq0) ** 0.5
            n_clad = clad_medium.eps_model(freq0) ** 0.5

            sidewall_medium = (
                td.Medium.from_nk(
                    n=n_clad.real, k=n_clad.imag + self.sidewall_k, freq=freq0
                )
                if self.sidewall_k != 0.0
                else None
            )
            surface_medium = (
                td.Medium.from_nk(
                    n=n_clad.real, k=n_clad.imag + self.surface_k, freq=freq0
                )
                if self.surface_k != 0.0
                else None
            )

            target_neff = self._resolve_target_neff(n_core)

            mode_spec = td.ModeSpec(
                num_modes=self.num_modes,
                target_neff=target_neff,
                bend_radius=self.bend_radius,
                bend_axis=1,
                num_pml=(12, 12) if self.bend_radius else (0, 0),
                precision=self.precision,
                group_index_step=self.group_index_step,
            )

            self._waveguide = waveguide.RectangularDielectric(
                wavelength=self.wavelength,
                core_width=self.core_width,
                core_thickness=self.core_thickness,
                core_medium=core_medium,
                clad_medium=clad_medium,
                box_medium=box_medium,
                slab_thickness=self.slab_thickness,
                clad_thickness=self.clad_thickness,
                box_thickness=self.box_thickness,
                side_margin=self.side_margin,
                sidewall_angle=self.sidewall_angle,
                sidewall_thickness=self.sidewall_thickness,
                sidewall_medium=sidewall_medium,
                surface_thickness=self.surface_thickness,
                surface_medium=surface_medium,
                propagation_axis=2,
                normal_axis=1,
                mode_spec=mode_spec,
                grid_resolution=self.grid_resolution,
                max_grid_scaling=self.max_grid_scaling,
            )

        return self._waveguide

    @property
    def _data(self):
        """Mode data for this waveguide (cached if cache is enabled)."""
        if not hasattr(self, "_cached_data"):
            filepath = self.filepath
            if filepath and filepath.exists() and not self.overwrite:
                logger.info(f"load data from {filepath}.")
                self._cached_data = np.load(filepath)
                return self._cached_data

            wg = self.waveguide

            fields = wg.mode_solver.data.field_components
            self._cached_data = {
                f + c: fields[f + c].squeeze(drop=True).values
                for f in "EH"
                for c in "xyz"
            }

            self._cached_data["x"] = fields["Ex"].coords["x"].values
            self._cached_data["y"] = fields["Ex"].coords["y"].values

            self._cached_data["n_eff"] = wg.n_complex.squeeze(drop=True).values
            self._cached_data["mode_area"] = wg.mode_area.squeeze(drop=True).values

            fraction_te = np.zeros(self.num_modes)
            fraction_tm = np.zeros(self.num_modes)

            for i in range(self.num_modes):
                e_fields = (
                    fields["Ex"].sel(mode_index=i),
                    fields["Ey"].sel(mode_index=i),
                )
                areas_e = [np.sum(np.abs(e) ** 2) for e in e_fields]
                areas_e /= np.sum(areas_e)
                areas_e *= 100
                fraction_te[i] = areas_e[0] / (areas_e[0] + areas_e[1])
                fraction_tm[i] = areas_e[1] / (areas_e[0] + areas_e[1])

            self._cached_data["fraction_te"] = fraction_te
            self._cached_data["fraction_tm"] = fraction_tm

            if wg.n_group is not None:
                self._cached_data["n_group"] = wg.n_group.squeeze(drop=True).values

            if filepath:
                logger.info(f"store data into {filepath}.")
                np.savez(filepath, **self._cached_data)

        return self._cached_data

    @property
    def fraction_te(self):
        """Fraction of TE polarization."""
        return self._data["fraction_te"]

    @property
    def fraction_tm(self):
        """Fraction of TM polarization."""
        return self._data["fraction_tm"]

    @property
    def n_eff(self):
        """Effective propagation index."""
        return self._data["n_eff"]

    @property
    def n_group(self):
        """Group index.

        This is only present it the parameter `group_index_step` is set.
        """
        return self._data.get("n_group", None)

    @property
    def mode_area(self):
        """Effective mode area."""
        return self._data["mode_area"]

    @property
    def loss_dB_per_cm(self):
        """Propagation loss for computed modes in dB/cm."""
        wavelength = self.wavelength * 1e-6  # convert to m
        alpha = 2 * np.pi * np.imag(self.n_eff).T / wavelength  # lin/m loss
        return 20 * np.log10(np.e) * alpha.T * 1e-2  # dB/cm loss

    @property
    def index(self) -> None:
        """Refractive index distribution on the simulation domain."""
        plane = self.waveguide.mode_solver.plane
        wavelength = (
            self.wavelength[self.wavelength.size // 2]
            if self.wavelength.size > 1
            else self.wavelength
        )
        eps = self.waveguide.mode_solver.simulation.epsilon(
            plane, freq=td.C_0 / wavelength
        )
        return eps.squeeze(drop=True).T ** 0.5

    def overlap(self, waveguide: Waveguide, conjugate: bool = True):
        """Calculate the mode overlap between waveguide modes.

        Parameters:
            waveguide: waveguide with which to overlap modes.
            conjugate: use the conjugate form of the overlap integral.
        """
        self_data = self.waveguide.mode_solver.data
        other_data = waveguide.waveguide.mode_solver.data
        # self_data = self._data
        # other_data = waveguide._data
        return self_data.outer_dot(other_data, conjugate).squeeze(drop=True).values

    def plot_grid(self) -> None:
        """Plot the waveguide grid."""
        self.waveguide.plot_grid(z=0)

    def plot_index(self, **kwargs):
        """Plot the waveguide index distribution.

        Keyword arguments are passed to xarray.DataArray.plot.
        """
        artist = self.index.real.plot(**kwargs)
        artist.axes.set_aspect("equal")
        return artist

    def plot_field(
        self,
        field_name: str,
        value: str = "real",
        mode_index: int = 0,
        wavelength: float | None = None,
        **kwargs,
    ):
        """Plot the selected field distribution from a waveguide mode.

        Parameters:
            field_name: one of 'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'.
            value: component of the field to plot. One of 'real',
                'imag', 'abs', 'phase', 'dB'.
            mode_index: mode selection.
            wavelength: wavelength selection.
            kwargs: keyword arguments passed to xarray.DataArray.plot.
        """
        data = self._data[field_name]

        if mode_index >= self.num_modes:
            raise ValueError(
                f"mode_index = {mode_index} must be less than num_modes {self.num_modes}"
            )

        if self.num_modes > 1:
            data = data[..., mode_index]
        if self.wavelength.size > 1:
            i = (
                np.argmin(np.abs(wavelength - self.wavelength))
                if wavelength
                else self.wavelength.size // 2
            )
            data = data[..., i]

        if value == "real":
            data = data.real
        elif value == "imag":
            data = data.imag
        elif value == "abs":
            data = np.abs(data)
        elif value == "dB":
            data = 20 * np.log10(np.abs(data))
            data -= np.max(data)
        elif value == "phase":
            data = np.arctan2(data.imag, data.real)
        else:
            raise ValueError(
                "value must be one of 'real', 'imag', 'abs', 'phase', 'dB'"
            )
        data_array = xarray.DataArray(
            data.T, coords={"y": self._data["y"], "x": self._data["x"]}
        )

        if value == "dB":
            kwargs.update(vmin=-20)

        data_array.name = field_name
        artist = data_array.plot(**kwargs)
        artist.axes.set_aspect("equal")
        return artist

    def _ipython_display_(self) -> None:
        """Show index in matplotlib for Jupyter Notebooks."""
        self.plot_index()

    def __repr__(self) -> str:
        """Show waveguide representation."""
        return (
            f"{self.__class__.__name__}("
            + ", ".join(
                f"{k}={custom_serializer(getattr(self, k))!r}"
                for k in self.__fields__.keys()
            )
            + ")"
        )

    def __str__(self) -> str:
        """Show waveguide representation."""
        return self.__repr__()

filepath property

Cache file path.

waveguide property

Tidy3D waveguide used by this instance.

fraction_te property

Fraction of TE polarization.

fraction_tm property

Fraction of TM polarization.

n_eff property

Effective propagation index.

n_group property

Group index.

This is only present it the parameter group_index_step is set.

mode_area property

Effective mode area.

loss_dB_per_cm property

Propagation loss for computed modes in dB/cm.

index property

Refractive index distribution on the simulation domain.

overlap(waveguide, conjugate=True)

Calculate the mode overlap between waveguide modes.

Parameters:

Name Type Description Default
waveguide Waveguide

waveguide with which to overlap modes.

required
conjugate bool

use the conjugate form of the overlap integral.

True
Source code in gplugins/tidy3d/modes.py
def overlap(self, waveguide: Waveguide, conjugate: bool = True):
    """Calculate the mode overlap between waveguide modes.

    Parameters:
        waveguide: waveguide with which to overlap modes.
        conjugate: use the conjugate form of the overlap integral.
    """
    self_data = self.waveguide.mode_solver.data
    other_data = waveguide.waveguide.mode_solver.data
    # self_data = self._data
    # other_data = waveguide._data
    return self_data.outer_dot(other_data, conjugate).squeeze(drop=True).values

plot_grid()

Plot the waveguide grid.

Source code in gplugins/tidy3d/modes.py
def plot_grid(self) -> None:
    """Plot the waveguide grid."""
    self.waveguide.plot_grid(z=0)

plot_index(**kwargs)

Plot the waveguide index distribution.

Keyword arguments are passed to xarray.DataArray.plot.

Source code in gplugins/tidy3d/modes.py
def plot_index(self, **kwargs):
    """Plot the waveguide index distribution.

    Keyword arguments are passed to xarray.DataArray.plot.
    """
    artist = self.index.real.plot(**kwargs)
    artist.axes.set_aspect("equal")
    return artist

plot_field(field_name, value='real', mode_index=0, wavelength=None, **kwargs)

Plot the selected field distribution from a waveguide mode.

Parameters:

Name Type Description Default
field_name str

one of 'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'.

required
value str

component of the field to plot. One of 'real', 'imag', 'abs', 'phase', 'dB'.

'real'
mode_index int

mode selection.

0
wavelength float | None

wavelength selection.

None
kwargs

keyword arguments passed to xarray.DataArray.plot.

{}
Source code in gplugins/tidy3d/modes.py
def plot_field(
    self,
    field_name: str,
    value: str = "real",
    mode_index: int = 0,
    wavelength: float | None = None,
    **kwargs,
):
    """Plot the selected field distribution from a waveguide mode.

    Parameters:
        field_name: one of 'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'.
        value: component of the field to plot. One of 'real',
            'imag', 'abs', 'phase', 'dB'.
        mode_index: mode selection.
        wavelength: wavelength selection.
        kwargs: keyword arguments passed to xarray.DataArray.plot.
    """
    data = self._data[field_name]

    if mode_index >= self.num_modes:
        raise ValueError(
            f"mode_index = {mode_index} must be less than num_modes {self.num_modes}"
        )

    if self.num_modes > 1:
        data = data[..., mode_index]
    if self.wavelength.size > 1:
        i = (
            np.argmin(np.abs(wavelength - self.wavelength))
            if wavelength
            else self.wavelength.size // 2
        )
        data = data[..., i]

    if value == "real":
        data = data.real
    elif value == "imag":
        data = data.imag
    elif value == "abs":
        data = np.abs(data)
    elif value == "dB":
        data = 20 * np.log10(np.abs(data))
        data -= np.max(data)
    elif value == "phase":
        data = np.arctan2(data.imag, data.real)
    else:
        raise ValueError(
            "value must be one of 'real', 'imag', 'abs', 'phase', 'dB'"
        )
    data_array = xarray.DataArray(
        data.T, coords={"y": self._data["y"], "x": self._data["x"]}
    )

    if value == "dB":
        kwargs.update(vmin=-20)

    data_array.name = field_name
    artist = data_array.plot(**kwargs)
    artist.axes.set_aspect("equal")
    return artist

__repr__()

Show waveguide representation.

Source code in gplugins/tidy3d/modes.py
def __repr__(self) -> str:
    """Show waveguide representation."""
    return (
        f"{self.__class__.__name__}("
        + ", ".join(
            f"{k}={custom_serializer(getattr(self, k))!r}"
            for k in self.__fields__.keys()
        )
        + ")"
    )

__str__()

Show waveguide representation.

Source code in gplugins/tidy3d/modes.py
def __str__(self) -> str:
    """Show waveguide representation."""
    return self.__repr__()

gplugins.tidy3d.modes.WaveguideCoupler

Bases: Waveguide

Waveguide coupler Model.

All dimensions must be specified in μm (1e-6 m).

Parameters:

Name Type Description Default
wavelength

wavelength in free space.

required
core_width

with of each core.

required
gap

inter-core separation.

required
core_thickness

waveguide core thickness (height).

required
core_material

core material. One of: - string: material name. - float: refractive index. - float, float: refractive index real and imaginary part. - function: function of wavelength.

required
clad_material

top cladding material.

required
box_material

bottom cladding material.

required
slab_thickness

thickness of the slab region in a rib waveguide.

required
clad_thickness

thickness of the top cladding.

required
box_thickness

thickness of the bottom cladding.

required
side_margin

domain extension to the side of the waveguide core.

required
sidewall_angle

angle of the core sidewall w.r.t. the substrate normal.

required
sidewall_thickness

thickness of a layer on the sides of the waveguide core to model side-surface losses.

required
sidewall_k

absorption coefficient added to the core material index on the side-surface layer.

required
surface_thickness

thickness of a layer on the top of the waveguide core and slabs to model top-surface losses.

required
surface_k

absorption coefficient added to the core material index on the top-surface layer.

required
bend_radius

radius to simulate circular bend.

required
num_modes

number of modes to compute.

required
group_index_step

if set to True, indicates that the group index must also be calculated. If set to a positive float it defines the fractional frequency step used for the numerical differentiation of the effective index.

required
target_neff

target effective index for the mode solver. Defaults to the real part of the core refractive index if not specified.

required
precision

computation precision.

required
grid_resolution

wavelength resolution of the computation grid.

required
max_grid_scaling

grid scaling factor in cladding regions.

required
cache

controls the use of cached results.

required

::

_____________________________________________________________

        ._________________.       ._________________.
        |                 |       |                 |
        |<-core_width[0]->|       |<-core_width[1]->|
        |                 |<-gap->|                 |
________'                 '_______'                 '________

_____________________________________________________________



_____________________________________________________________
Source code in gplugins/tidy3d/modes.py
class WaveguideCoupler(Waveguide):
    """Waveguide coupler Model.

    All dimensions must be specified in μm (1e-6 m).

    Parameters:
        wavelength: wavelength in free space.
        core_width: with of each core.
        gap: inter-core separation.
        core_thickness: waveguide core thickness (height).
        core_material: core material. One of:
            - string: material name.
            - float: refractive index.
            - float, float: refractive index real and imaginary part.
            - function: function of wavelength.
        clad_material: top cladding material.
        box_material: bottom cladding material.
        slab_thickness: thickness of the slab region in a rib waveguide.
        clad_thickness: thickness of the top cladding.
        box_thickness: thickness of the bottom cladding.
        side_margin: domain extension to the side of the waveguide core.
        sidewall_angle: angle of the core sidewall w.r.t. the substrate
            normal.
        sidewall_thickness: thickness of a layer on the sides of the
            waveguide core to model side-surface losses.
        sidewall_k: absorption coefficient added to the core material
            index on the side-surface layer.
        surface_thickness: thickness of a layer on the top of the
            waveguide core and slabs to model top-surface losses.
        surface_k: absorption coefficient added to the core material
            index on the top-surface layer.
        bend_radius: radius to simulate circular bend.
        num_modes: number of modes to compute.
        group_index_step: if set to `True`, indicates that the group
            index must also be calculated. If set to a positive float
            it defines the fractional frequency step used for the
            numerical differentiation of the effective index.
        target_neff: target effective index for the mode solver. Defaults
            to the real part of the core refractive index if not specified.
        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.

    ::

        _____________________________________________________________

                ._________________.       ._________________.
                |                 |       |                 |
                |<-core_width[0]->|       |<-core_width[1]->|
                |                 |<-gap->|                 |
        ________'                 '_______'                 '________

        _____________________________________________________________



        _____________________________________________________________
    """

    core_width: tuple[float, float]
    gap: float

    @property
    def waveguide(self):
        """Tidy3D waveguide used by this instance."""
        if not hasattr(self, "_waveguide"):
            core_medium = get_medium(self.core_material)
            clad_medium = get_medium(self.clad_material)
            box_medium = get_medium(self.box_material) if self.box_material else None

            freq0 = td.C_0 / np.mean(self.wavelength)
            n_core = core_medium.eps_model(freq0) ** 0.5
            n_clad = clad_medium.eps_model(freq0) ** 0.5

            sidewall_medium = (
                td.Medium.from_nk(
                    n=n_clad.real, k=n_clad.imag + self.sidewall_k, freq=freq0
                )
                if self.sidewall_k != 0.0
                else None
            )
            surface_medium = (
                td.Medium.from_nk(
                    n=n_clad.real, k=n_clad.imag + self.surface_k, freq=freq0
                )
                if self.surface_k != 0.0
                else None
            )

            target_neff = self._resolve_target_neff(n_core)

            mode_spec = td.ModeSpec(
                num_modes=self.num_modes,
                target_neff=target_neff,
                bend_radius=self.bend_radius,
                bend_axis=1,
                num_pml=(12, 12) if self.bend_radius else (0, 0),
                precision=self.precision,
                group_index_step=self.group_index_step,
            )

            self._waveguide = waveguide.RectangularDielectric(
                wavelength=self.wavelength,
                core_width=self.core_width,
                core_thickness=self.core_thickness,
                core_medium=core_medium,
                clad_medium=clad_medium,
                box_medium=box_medium,
                slab_thickness=self.slab_thickness,
                clad_thickness=self.clad_thickness,
                box_thickness=self.box_thickness,
                side_margin=self.side_margin,
                sidewall_angle=self.sidewall_angle,
                gap=self.gap,
                sidewall_thickness=self.sidewall_thickness,
                sidewall_medium=sidewall_medium,
                surface_thickness=self.surface_thickness,
                surface_medium=surface_medium,
                propagation_axis=2,
                normal_axis=1,
                mode_spec=mode_spec,
                grid_resolution=self.grid_resolution,
                max_grid_scaling=self.max_grid_scaling,
            )

        return self._waveguide

    def coupling_length(self, power_ratio: float = 1.0) -> float:
        """Coupling length calculated from the effective mode indices.

        Args:
            power_ratio: desired coupling power ratio.
        """
        m = (self.n_eff.size // 2) * 2
        n_even = self.n_eff[:m:2].real
        n_odd = self.n_eff[1:m:2].real
        return (
            self.wavelength / (np.pi * (n_even - n_odd)) * np.arcsin(power_ratio**0.5)
        )

waveguide property

Tidy3D waveguide used by this instance.

coupling_length(power_ratio=1.0)

Coupling length calculated from the effective mode indices.

Parameters:

Name Type Description Default
power_ratio float

desired coupling power ratio.

1.0
Source code in gplugins/tidy3d/modes.py
def coupling_length(self, power_ratio: float = 1.0) -> float:
    """Coupling length calculated from the effective mode indices.

    Args:
        power_ratio: desired coupling power ratio.
    """
    m = (self.n_eff.size // 2) * 2
    n_even = self.n_eff[:m:2].real
    n_odd = self.n_eff[1:m:2].real
    return (
        self.wavelength / (np.pi * (n_even - n_odd)) * np.arcsin(power_ratio**0.5)
    )

gplugins.tidy3d.modes.sweep_n_eff(waveguide, **sweep_kwargs)

Return the effective index for a range of waveguide geometries.

The returned array uses the sweep arguments and the mode index as coordinates to organize the data.

Parameters:

Name Type Description Default
waveguide Waveguide

base waveguide geometry.

required

Other Parameters:

Name Type Description
sweep_kwargs

Waveguide arguments and values to sweep.

wavelength

wavelength in free space.

core_width

waveguide core width.

core_thickness

waveguide core thickness (height).

core_material

core material. One of: - string: material name. - float: refractive index. - float, float: refractive index real and imaginary part. - function: function of wavelength.

clad_material

top cladding material.

box_material

bottom cladding material.

slab_thickness

thickness of the slab region in a rib waveguide.

clad_thickness

thickness of the top cladding.

box_thickness

thickness of the bottom cladding.

side_margin

domain extension to the side of the waveguide core.

sidewall_angle

angle of the core sidewall w.r.t. the substrate normal.

sidewall_thickness

thickness of a layer on the sides of the waveguide core to model side-surface losses.

sidewall_k

absorption coefficient added to the core material index on the side-surface layer.

surface_thickness

thickness of a layer on the top of the waveguide core and slabs to model top-surface losses.

surface_k

absorption coefficient added to the core material index on the top-surface layer.

bend_radius

radius to simulate circular bend.

num_modes

number of modes to compute.

group_index_step

if set to True, indicates that the group index must also be calculated. If set to a positive float it defines the fractional frequency step used for the numerical differentiation of the effective index.

precision

computation precision.

grid_resolution

wavelength resolution of the computation grid.

max_grid_scaling

grid scaling factor in cladding regions.

Example

sweep_n_eff( ... my_waveguide, ... core_width=[0.40, 0.45, 0.50], ... core_thickness=[0.22, 0.25], ... )

Source code in gplugins/tidy3d/modes.py
def sweep_n_eff(waveguide: Waveguide, **sweep_kwargs) -> np.ndarray:
    """Return the effective index for a range of waveguide geometries.

    The returned array uses the sweep arguments and the mode index as
    coordinates to organize the data.

    Args:
        waveguide: base waveguide geometry.

    Keyword Args:
        sweep_kwargs: Waveguide arguments and values to sweep.
        wavelength: wavelength in free space.
        core_width: waveguide core width.
        core_thickness: waveguide core thickness (height).
        core_material: core material. One of:
            - string: material name.
            - float: refractive index.
            - float, float: refractive index real and imaginary part.
            - function: function of wavelength.
        clad_material: top cladding material.
        box_material: bottom cladding material.
        slab_thickness: thickness of the slab region in a rib waveguide.
        clad_thickness: thickness of the top cladding.
        box_thickness: thickness of the bottom cladding.
        side_margin: domain extension to the side of the waveguide core.
        sidewall_angle: angle of the core sidewall w.r.t. the substrate
            normal.
        sidewall_thickness: thickness of a layer on the sides of the
            waveguide core to model side-surface losses.
        sidewall_k: absorption coefficient added to the core material
            index on the side-surface layer.
        surface_thickness: thickness of a layer on the top of the
            waveguide core and slabs to model top-surface losses.
        surface_k: absorption coefficient added to the core material
            index on the top-surface layer.
        bend_radius: radius to simulate circular bend.
        num_modes: number of modes to compute.
        group_index_step: if set to `True`, indicates that the group
            index must also be calculated. If set to a positive float
            it defines the fractional frequency step used for the
            numerical differentiation of the effective index.
        precision: computation precision.
        grid_resolution: wavelength resolution of the computation grid.
        max_grid_scaling: grid scaling factor in cladding regions.

    Example:
        >>> sweep_n_eff(
        ...     my_waveguide,
        ...     core_width=[0.40, 0.45, 0.50],
        ...     core_thickness=[0.22, 0.25],
        ... )
    """
    return _sweep(waveguide, "n_eff", **sweep_kwargs)

gplugins.tidy3d.modes.sweep_n_group(waveguide, **sweep_kwargs)

Return the group index for a range of waveguide geometries.

The returned array uses the sweep arguments and the mode index as coordinates to organize the data.

Parameters:

Name Type Description Default
waveguide Waveguide

base waveguide geometry.

required

Other Parameters:

Name Type Description
sweep_kwargs

Waveguide arguments and values to sweep.

wavelength

wavelength in free space.

core_width

waveguide core width.

core_thickness

waveguide core thickness (height).

core_material

core material. One of: - string: material name. - float: refractive index. - float, float: refractive index real and imaginary part. - function: function of wavelength.

clad_material

top cladding material.

box_material

bottom cladding material.

slab_thickness

thickness of the slab region in a rib waveguide.

clad_thickness

thickness of the top cladding.

box_thickness

thickness of the bottom cladding.

side_margin

domain extension to the side of the waveguide core.

sidewall_angle

angle of the core sidewall w.r.t. the substrate normal.

sidewall_thickness

thickness of a layer on the sides of the waveguide core to model side-surface losses.

sidewall_k

absorption coefficient added to the core material index on the side-surface layer.

surface_thickness

thickness of a layer on the top of the waveguide core and slabs to model top-surface losses.

surface_k

absorption coefficient added to the core material index on the top-surface layer.

bend_radius

radius to simulate circular bend.

num_modes

number of modes to compute.

group_index_step

if set to True, indicates that the group index must also be calculated. If set to a positive float it defines the fractional frequency step used for the numerical differentiation of the effective index.

precision

computation precision.

grid_resolution

wavelength resolution of the computation grid.

max_grid_scaling

grid scaling factor in cladding regions.

Example

sweep_n_group( ... my_waveguide, ... core_width=[0.40, 0.45, 0.50], ... core_thickness=[0.22, 0.25], ... )

Source code in gplugins/tidy3d/modes.py
def sweep_n_group(waveguide: Waveguide, **sweep_kwargs) -> np.ndarray:
    """Return the group index for a range of waveguide geometries.

    The returned array uses the sweep arguments and the mode index as
    coordinates to organize the data.

    Args:
        waveguide: base waveguide geometry.

    Keyword Args:
        sweep_kwargs: Waveguide arguments and values to sweep.
        wavelength: wavelength in free space.
        core_width: waveguide core width.
        core_thickness: waveguide core thickness (height).
        core_material: core material. One of:
            - string: material name.
            - float: refractive index.
            - float, float: refractive index real and imaginary part.
            - function: function of wavelength.
        clad_material: top cladding material.
        box_material: bottom cladding material.
        slab_thickness: thickness of the slab region in a rib waveguide.
        clad_thickness: thickness of the top cladding.
        box_thickness: thickness of the bottom cladding.
        side_margin: domain extension to the side of the waveguide core.
        sidewall_angle: angle of the core sidewall w.r.t. the substrate
            normal.
        sidewall_thickness: thickness of a layer on the sides of the
            waveguide core to model side-surface losses.
        sidewall_k: absorption coefficient added to the core material
            index on the side-surface layer.
        surface_thickness: thickness of a layer on the top of the
            waveguide core and slabs to model top-surface losses.
        surface_k: absorption coefficient added to the core material
            index on the top-surface layer.
        bend_radius: radius to simulate circular bend.
        num_modes: number of modes to compute.
        group_index_step: if set to `True`, indicates that the group
            index must also be calculated. If set to a positive float
            it defines the fractional frequency step used for the
            numerical differentiation of the effective index.
        precision: computation precision.
        grid_resolution: wavelength resolution of the computation grid.
        max_grid_scaling: grid scaling factor in cladding regions.

    Example:
        >>> sweep_n_group(
        ...     my_waveguide,
        ...     core_width=[0.40, 0.45, 0.50],
        ...     core_thickness=[0.22, 0.25],
        ... )
    """
    return _sweep(waveguide, "n_group", **sweep_kwargs)

gplugins.tidy3d.modes.sweep_bend_mismatch(waveguide, bend_radii, track_modes=False, modes_to_track=(0,))

Overlap integral squared for the bend mode mismatch loss.

The loss is squared because you hit the bend loss twice (from bend to straight and from straight to bend).

Parameters:

Name Type Description Default
waveguide Waveguide

base waveguide geometry.

required
bend_radii tuple[float, ...]

radii values to sweep.

required
track_modes bool

if True, for each radius select the bend mode with the best overlap for each tracked straight mode.

False
modes_to_track Sequence[int]

straight mode indices to track. Required when track_modes is True.

(0,)
Source code in gplugins/tidy3d/modes.py
def sweep_bend_mismatch(
    waveguide: Waveguide,
    bend_radii: tuple[float, ...],
    track_modes: bool = False,
    modes_to_track: Sequence[int] = (0,),
) -> np.ndarray:
    """Overlap integral squared for the bend mode mismatch loss.

    The loss is squared because you hit the bend loss twice
    (from bend to straight and from straight to bend).

    Args:
        waveguide: base waveguide geometry.
        bend_radii: radii values to sweep.
        track_modes: if True, for each radius select the bend mode with
            the best overlap for each tracked straight mode.
        modes_to_track: straight mode indices to track. Required when
            track_modes is True.
    """
    if track_modes:
        if len(modes_to_track) == 0:
            raise ValueError("modes_to_track must be provided when track_modes is True")
        if waveguide.num_modes < max(modes_to_track) + 1:
            raise ValueError(
                f"num_modes ({waveguide.num_modes}) must be >= "
                f"{max(modes_to_track) + 1} to track modes {modes_to_track}"
            )
        if waveguide.num_modes < 2:
            raise ValueError("Track modes requires num_modes >= 2")

    kwargs = dict(waveguide)
    kwargs.pop("bend_radius")
    straight = Waveguide(**kwargs)

    results = []
    for radius in tqdm(bend_radii):
        bend = Waveguide(bend_radius=radius, **kwargs)
        overlap = bend.overlap(straight)

        if track_modes:
            best = [np.max(np.abs(overlap[:, m]) ** 2) for m in modes_to_track]
            results.append(best)
        else:
            results.append(
                np.diagonal(overlap) ** 2 if straight.num_modes > 1 else overlap**2
            )

    return np.abs(results) ** 2

gplugins.tidy3d.modes.sweep_coupling_length(coupler, gaps, power_ratio=1.0)

Calculate coupling length for a series of gap sizes.

Parameters:

Name Type Description Default
coupler WaveguideCoupler

base waveguide coupler geometry.

required
gaps tuple[float, ...]

gap values to use for coupling length calculation.

required
power_ratio float

desired coupling power ratio.

1.0
Source code in gplugins/tidy3d/modes.py
def sweep_coupling_length(
    coupler: WaveguideCoupler, gaps: tuple[float, ...], power_ratio: float = 1.0
) -> np.ndarray:
    """Calculate coupling length for a series of gap sizes.

    Parameters:
        coupler: base waveguide coupler geometry.
        gaps: gap values to use for coupling length calculation.
        power_ratio: desired coupling power ratio.
    """
    kwargs = {k: getattr(coupler, k) for k in coupler.__fields__}
    length = []
    for gap in tqdm(gaps):
        kwargs["gap"] = gap
        c = WaveguideCoupler(**kwargs)
        length.append(c.coupling_length(power_ratio))
    return np.array(length)

Mode solver Femwell

gplugins.femwell.mode_solver.compute_cross_section_modes(cross_section, layer_stack, wavelength=1.55, num_modes=4, order=1, radius=np.inf, wafer_padding=2.0, **kwargs)

Calculate effective index of a cross-section.

Defines a "straight" component of the cross_section, and calls compute_component_slice_modes.

Parameters:

Name Type Description Default
cross_section CrossSectionSpec

gdsfactory cross_section.

required
layer_stack LayerStack

gdsfactory layer_stack.

required
wavelength float

in um.

1.55
num_modes int

to compute.

4
order int

order of the mesh elements. 1: linear, 2: quadratic.

1
radius float

defaults to inf.

inf
wafer_padding float

in um.

2.0
kwargs Any

kwargs for compute_component_slice_modes

{}

Other Parameters:

Name Type Description
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.

Source code in gplugins/femwell/mode_solver.py
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,
    )

Mode solver EMode

gplugins.emode.EMode

Bases: EMode

EMode session with gdsfactory geometry helpers.

Creating an instance launches the EMode application and connects to it, so it requires a local EMode installation and license (the emodeconnection client alone is not enough). Any EMode function can be called as a method, e.g. FDM(), EME(), report(), plot(); see https://docs.emodephotonix.com for the full API.

Source code in gplugins/emode/emode.py
class EMode(emc.EMode):
    """EMode session with gdsfactory geometry helpers.

    Creating an instance launches the EMode application and connects to it,
    so it requires a local EMode installation and license (the
    ``emodeconnection`` client alone is not enough). Any EMode function can
    be called as a method, e.g. ``FDM()``, ``EME()``, ``report()``,
    ``plot()``; see https://docs.emodephotonix.com for the full API.
    """

    def build_waveguide(
        self,
        cross_section: CrossSectionSpec,
        layer_stack: LayerStack,
        **settings: Any,
    ) -> None:
        """Build a waveguide in this EMode session from gdsfactory geometry.

        Args:
            cross_section: gdsfactory cross-section (or spec) defining mask
                widths and offsets.
            layer_stack: gdsfactory LayerStack defining layer materials,
                thicknesses, and vertical placement.
            settings: forwarded to EMode's ``settings()`` function, with
                dimensional values in um (see :func:`get_emode_settings`).
        """
        self.settings(**get_emode_settings(**settings))
        materials = self.get("materials")
        for shape in get_shapes_from_layer_stack(cross_section, layer_stack, materials):
            self.shape(**shape)

build_waveguide(cross_section, layer_stack, **settings)

Build a waveguide in this EMode session from gdsfactory geometry.

Parameters:

Name Type Description Default
cross_section CrossSectionSpec

gdsfactory cross-section (or spec) defining mask widths and offsets.

required
layer_stack LayerStack

gdsfactory LayerStack defining layer materials, thicknesses, and vertical placement.

required
settings Any

forwarded to EMode's settings() function, with dimensional values in um (see :func:get_emode_settings).

{}
Source code in gplugins/emode/emode.py
def build_waveguide(
    self,
    cross_section: CrossSectionSpec,
    layer_stack: LayerStack,
    **settings: Any,
) -> None:
    """Build a waveguide in this EMode session from gdsfactory geometry.

    Args:
        cross_section: gdsfactory cross-section (or spec) defining mask
            widths and offsets.
        layer_stack: gdsfactory LayerStack defining layer materials,
            thicknesses, and vertical placement.
        settings: forwarded to EMode's ``settings()`` function, with
            dimensional values in um (see :func:`get_emode_settings`).
    """
    self.settings(**get_emode_settings(**settings))
    materials = self.get("materials")
    for shape in get_shapes_from_layer_stack(cross_section, layer_stack, materials):
        self.shape(**shape)

gplugins.emode.get_emode_settings(**settings)

Convert gdsfactory-style settings to EMode units.

gdsfactory uses microns for all dimensions while EMode defaults to nanometers. Settings named in DIMENSIONAL_SETTINGS are converted from um to nm; all other settings pass through unchanged.

Parameters:

Name Type Description Default
settings Any

keyword arguments for EMode's settings() function, with dimensional values in um.

{}

Returns:

Type Description
dict[str, Any]

The same settings with dimensional values converted to nm.

Source code in gplugins/emode/emode.py
def get_emode_settings(**settings: Any) -> dict[str, Any]:
    """Convert gdsfactory-style settings to EMode units.

    gdsfactory uses microns for all dimensions while EMode defaults to
    nanometers. Settings named in ``DIMENSIONAL_SETTINGS`` are converted from
    um to nm; all other settings pass through unchanged.

    Args:
        settings: keyword arguments for EMode's ``settings()`` function,
            with dimensional values in um.

    Returns:
        The same settings with dimensional values converted to nm.
    """
    return {
        key: value * UM_TO_NM
        if key in DIMENSIONAL_SETTINGS and value is not None
        else value
        for key, value in settings.items()
    }

gplugins.emode.get_shapes_from_layer_stack(cross_section, layer_stack, materials=())

Translate a gdsfactory layer stack and cross-section into EMode shapes.

Each :class:~gdsfactory.technology.LayerLevel becomes one EMode shape. A layer whose layer (or derived_layer) matches a section of the cross-section is patterned: it takes its mask width and offset from that section and is etched through its full thickness. Layers without a matching section become blanket layers (no mask or etch, following the defaults of EMode's shape() function). Vertical positions are referenced to the bottom of the layer stack, and gdsfactory mesh order (lower = higher priority) is converted to EMode shape priority (higher = higher priority).

Parameters:

Name Type Description Default
cross_section CrossSectionSpec

gdsfactory cross-section (or spec) defining mask widths and offsets.

required
layer_stack LayerStack

gdsfactory LayerStack defining layer materials, thicknesses, and vertical placement.

required
materials Sequence[str]

available EMode material names used to match gdsfactory material names case-insensitively, typically from EMode.get('materials').

()

Returns:

Type Description
list[dict[str, Any]]

One dict of keyword arguments for EMode's shape() function per

list[dict[str, Any]]

layer, in layer-stack order, with dimensions in nm.

Source code in gplugins/emode/emode.py
def get_shapes_from_layer_stack(
    cross_section: CrossSectionSpec,
    layer_stack: LayerStack,
    materials: Sequence[str] = (),
) -> list[dict[str, Any]]:
    """Translate a gdsfactory layer stack and cross-section into EMode shapes.

    Each :class:`~gdsfactory.technology.LayerLevel` becomes one EMode shape.
    A layer whose ``layer`` (or ``derived_layer``) matches a section of the
    cross-section is patterned: it takes its mask width and offset from that
    section and is etched through its full thickness. Layers without a
    matching section become blanket layers (no mask or etch, following the
    defaults of EMode's ``shape()`` function). Vertical positions are
    referenced to the bottom of the layer stack, and gdsfactory mesh order
    (lower = higher priority) is converted to EMode shape priority (higher =
    higher priority).

    Args:
        cross_section: gdsfactory cross-section (or spec) defining mask
            widths and offsets.
        layer_stack: gdsfactory LayerStack defining layer materials,
            thicknesses, and vertical placement.
        materials: available EMode material names used to match gdsfactory
            material names case-insensitively, typically from
            ``EMode.get('materials')``.

    Returns:
        One dict of keyword arguments for EMode's ``shape()`` function per
        layer, in layer-stack order, with dimensions in nm.
    """
    if not layer_stack.layers:
        raise ValueError("layer_stack must contain at least one layer.")

    xs = gf.get_cross_section(cross_section)

    max_order = max(level.mesh_order for level in layer_stack.layers.values())
    min_zmin = min(level.zmin for level in layer_stack.layers.values())

    shapes: list[dict[str, Any]] = []
    for name, level in layer_stack.layers.items():
        if level.material is None:
            raise ValueError(
                f"Layer {name!r} has no material defined in the layer stack."
            )

        shape: dict[str, Any] = {
            "name": name,
            "material": get_emode_material(level.material, materials),
            "height": level.thickness * UM_TO_NM,
            "sidewall_angle": level.sidewall_angle,
            "position": [0.0, (level.zmin - min_zmin + level.thickness / 2) * UM_TO_NM],
            "priority": max_order - level.mesh_order + 1,
        }

        level_layer = _level_layer_tuple(level)
        section = next(
            (
                s
                for s in xs.sections
                if level_layer is not None and _layer_tuple(s.layer) == level_layer
            ),
            None,
        )
        if section is not None:
            shape["mask"] = section.width * UM_TO_NM
            shape["mask_offset"] = section.offset * UM_TO_NM
            shape["etch_depth"] = level.thickness * UM_TO_NM

        shapes.append(shape)

    return shapes

EME (Eigen Mode Expansion)

gplugins.meow.MEOW

Source code in gplugins/meow/meow_eme.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
class MEOW:
    def __init__(
        self,
        component: gf.Component,
        layer_stack,
        wavelength: float = 1.55,
        temperature: float = 25.0,
        num_modes: int = 4,
        cell_length: float = 0.5,
        spacing_x: float = 2.0,
        center_x: float | None = None,
        resolution_x: int = 100,
        spacing_y: float = 2.0,
        center_y: float | None = None,
        resolution_y: int = 100,
        material_to_color: dict[str, ColorRGB] = material_to_color_default,
        dirpath: PathType | None = PATH.sparameters,
        filepath: PathType | None = None,
        overwrite: bool = False,
    ) -> None:
        """Computes multimode 2-port S-parameters for a gdsfactory component.

        assumes port 1 is at the left boundary and port 2 at the right boundary.

        Note coordinate systems:
            gdsfactory uses x,y in the plane to represent components, with the layer_stack existing in z
            meow uses x,y to represent a cross-section, with propagation in the z-direction
            hence we have [x,y,z] <--> [y,z,x] for gdsfactory <--> meow

        Arguments:
            component: gdsfactory component.
            layer_stack: gdsfactory layer_stack.
            wavelength: wavelength in microns (for FDE, and for material properties).
            temperature: temperature in C (for material properties). Unused now.
            num_modes: number of modes to compute for the eigenmode expansion.
            cell_length: in un.
            spacing_x: at beginning and end of the simulation region.
            center_x: in um.
            resolution_x: pixels in horizontal region.
            spacing_y: at the beginning and end of simulation region.
            center_y: in um.
            resolution_y: pixels in vertical direction.
            material_to_color: dict of materials colors for struct plot
            dirpath: directory to store Sparameters.
            filepath: to store pandas Dataframe with Sparameters in npz format.
                Defaults to dirpath/component_.npz.
            overwrite: overwrites stored Sparameter npz results.

        Returns:
            S-parameters in form o1@0,o2@0 at wavelength.

        ::

            cross_section view:
               ________________________________
              |                                |
              |                                |
              | spacing_x            spacing_x |  spacing_y
              |<--------->           <-------->|
              |          ___________   _ _ _   |
              |         |           |          |
              |         |           |_ _ _ _ _ |_ center_y
              |         |           |          |
              |         |___________|          |
              |               |                |
              |                                |
              |               |                |  spacing_y
              |                                |
              |_______________|________________|
                          center_x

            top side view:
               ________________________________
              |                                |
              |                                |
              |cell_length                     |
              |<-------->                      |
              |_____________________ __________|
              |         |           |          |
              |         |           |          |
              | cell0   |  cell1    |  cell2   |
              |_________|___________|__________|
              |                                |
              |                                |
              |                                |
              |                                |
              |________________________________|
        """
        # Validate component
        self.validate_component(component)

        # Save parameters
        self.wavelength = wavelength
        self.num_modes = num_modes
        self.temperature = temperature  # unused for now
        self.material_to_color = material_to_color

        # Process simulation bounds
        z_min, x_min, z_max, x_max = (
            component.xmin,
            component.ymin,
            component.xmax,
            component.ymax,
        )
        z_min, z_max = min(z_min, z_max) + 1e-10, max(z_min, z_max) - 1e-10
        x_min, x_max = min(x_min, x_max) + 1e-10, max(x_min, x_max) - 1e-10
        layer_stack = layer_stack.model_copy()
        ys = list_unique_layer_stack_z(layer_stack)
        y_min, y_max = np.min(ys) + 1e-10, np.max(ys) - 1e-10

        self.span_x = x_max - x_min + spacing_x
        self.center_x = center_x if center_x is not None else 0.5 * (x_max + x_min)
        self.resolution_x = resolution_x

        self.span_y = y_max - y_min + spacing_y
        self.center_y = center_y if center_y is not None else 0.5 * (y_max + y_min)
        self.resolution_y = resolution_y

        self.z_min = z_min
        self.z_max = z_max
        self.span_z = z_max - z_min

        self.cell_length = cell_length

        # you need two extra cells without length at beginning and end.
        self.num_cells = max(int(self.span_z / cell_length) + 2, 4)

        # Setup simulation
        self.component, self.layer_stack = self.add_global_layers(
            component, layer_stack
        )
        self.extrusion_rules = self.layer_stack_to_extrusion()
        self.structs = mw.extrude_gds(self.component, self.extrusion_rules)
        self.cells = self.create_cells()
        self.env = mw.Environment(wl=self.wavelength, T=self.temperature)
        self.css = [
            mw.CrossSection.from_cell(cell=cell, env=self.env) for cell in self.cells
        ]
        self.modes_per_cell = [None] * self.num_cells
        self.S = None
        self.port_map = None

        # Cache
        sim_settings = dict(
            wavelength=wavelength,
            temperature=temperature,
            num_modes=num_modes,
            cell_length=cell_length,
            spacing_x=spacing_x,
            center_x=center_x,
            resolution_x=resolution_x,
            spacing_y=spacing_y,
            center_y=center_y,
            resolution_y=resolution_y,
        )

        filepath = filepath or get_sparameters_path(
            component=component,
            dirpath=dirpath,
            layer_stack=layer_stack,
            **sim_settings,
        )

        sim_settings = sim_settings.copy()
        sim_settings["layer_stack"] = layer_stack.to_dict()
        sim_settings["component"] = component.to_dict()
        self.sim_settings = sim_settings
        self.filepath = pathlib.Path(filepath)
        self.filepath_sim_settings = filepath.with_suffix(".yml")
        self.overwrite = overwrite

    def gf_material_to_meow_material(
        self, material_name: str = "si", wavelengths=None, color=None
    ):
        """Converts a gdsfactory material into a MEOW material."""
        wavelengths = wavelengths or np.linspace(1.5, 1.6, 101)
        color = color or (0.9, 0.9, 0.9, 0.9)
        PDK = get_active_pdk()
        ns = PDK.materials_index[material_name](wavelengths)
        if ns.dtype in [np.float64, np.float32]:
            nr = ns
            ni = np.zeros_like(ns)
        else:
            nr = np.real(ns)
            ni = np.imag(ns)
        df = pd.DataFrame({"wl": wavelengths, "nr": nr, "ni": ni})
        return mw.SampledMaterial.from_df(
            material_name,
            df,
            meta={"color": color},
        )

    def add_global_layers(
        self,
        component,
        layer_stack,
        buffer_y: float = 1,
        global_layer_index: int = 10000,
        layer_wafer: LayerSpec = "WAFER",
    ) -> tuple[Component, LayerStack]:
        """Adds bbox polygons for global layers.

        LAYER.WAFER layers are represented as polygons of size [bbox.x, xspan (meow coords)]

        Arguments:
            component: gdsfactory component.
            layer_stack: gdsfactory LayerStack.
            buffer_y: float, y-buffer to add to box.
            xspan: from eme setup.
            global_layer_index: int, layer index at which to starting adding the global layers.
                    Default 10000 with +1 increments to avoid clashing with physical layers.
            layer_wafer: LayerSpec, layer to represent the wafer.

        """
        c = gf.Component()
        c.add_ref(component)
        layer_wafer = gf.get_layer(layer_wafer)

        for level in layer_stack.layers.values():
            if isinstance(level.layer, DerivedLayer):
                continue
            layer = gf.get_layer(level.layer.layer)
            if layer == layer_wafer:
                c.add_ref(
                    gf.components.bbox(
                        component,
                        layer=(global_layer_index, 0),
                    )
                )
                layer = (global_layer_index, 0)
                global_layer_index += 1

        return c, layer_stack

    def layer_stack_to_extrusion(self):
        """Convert LayerStack to meow extrusions."""
        extrusions = {}
        for layer in self.layer_stack.layers.values():
            layer_tuple = gf.get_layer_tuple((layer.derived_layer or layer.layer).layer)
            if layer_tuple not in extrusions.keys():
                extrusions[layer_tuple] = []
            extrusions[layer_tuple].append(
                mw.GdsExtrusionRule(
                    material=self.gf_material_to_meow_material(
                        layer.material,
                        np.array([self.wavelength]),
                        color=self.material_to_color.get(layer.material),
                    ),
                    h_min=layer.zmin,
                    h_max=layer.zmin + layer.thickness,
                    mesh_order=layer.mesh_order,
                )
            )
        return extrusions

    def create_cells(self) -> list[mw.Cell]:
        """Get meow cells from extruded component.

        Args:
            cell_length: in um.
        """
        zs = np.linspace(self.z_min, self.z_max, self.num_cells - 1)

        # add two cells without length:
        zs = np.concatenate([[self.z_min], zs, [self.z_max]])

        mesh = mw.Mesh2D(
            x=np.linspace(
                self.center_x - self.span_x / 2,
                self.center_x + self.span_x / 2,
                self.resolution_x,
            ),
            y=np.linspace(
                self.center_y - self.span_y / 2,
                self.center_y + self.span_y / 2,
                self.resolution_y,
            ),
            ez_interfaces=True,
        )
        cells = []
        for z_min, z_max in itertools.pairwise(zs):
            cell = mw.Cell(
                structures=self.structs,
                mesh=mesh,
                z_min=z_min,
                z_max=z_max,
            )
            cells.append(cell)

        return cells

    def plot_structure(self, scale=(1, 1, 0.2)):
        return mw.visualize(self.structs, scale=scale)

    def plot_cross_section(self, xs_num):
        env = mw.Environment(wl=self.wavelength, T=self.temperature)
        css = [mw.CrossSection.from_cell(cell=cell, env=env) for cell in self.cells]
        return mw.visualize(css[xs_num])

    def plot_mode(self, xs_num, mode_num):
        if self.modes_per_cell[xs_num] is None:
            self.modes_per_cell[xs_num] = self.compute_mode(xs_num)
        return mw.visualize(self.modes_per_cell[xs_num][mode_num])

    def get_port_map(self):
        if self.port_map is None:
            self.compute_sparameters()
        return self.port_map

    def plot_s_params(
        self, fmt: Literal["abs", "phase", "real-imag", "real", "imag"] = "abs"
    ):
        fmt_str = str(fmt).lower()
        supported_fmts = ["abs", "phase", "real-imag", "real", "imag"]
        if fmt_str not in supported_fmts:
            raise ValueError(
                f"EME Plot format '{fmt_str}' not in supported formats: {supported_fmts}."
            )

        if self.S is None:
            self.compute_sparameters()

        S = self.S
        kwargs = {}
        assert S is not None  # make type checker happy
        if fmt_str == "abs":
            S = abs(S)
        elif fmt_str == "real":
            S = np.real(S)
        elif fmt_str == "imag":
            S = np.imag(S)
        elif fmt_str == "phase":
            kwargs["phase"] = True

        return mw.visualize((S, self.port_map), **kwargs)

    def validate_component(self, component) -> None:
        optical_ports = [
            port for port in component.ports if port.port_type == "optical"
        ]
        if len(optical_ports) != 2:
            raise ValueError(
                "Component provided to MEOW does not have exactly 2 optical ports."
            )
        elif component.ports["o1"].orientation != 180:
            raise ValueError("Component port o1 does not face westward (180 deg).")
        elif component.ports["o2"].orientation != 0:
            raise ValueError("Component port o2 does not face eastward (0 deg).")

    def compute_mode(self, xs_num):
        return mw.compute_modes(self.css[xs_num], num_modes=self.num_modes)

    def compute_all_modes(self) -> None:
        self.modes_per_cell = []
        for cs in tqdm(self.css):
            modes_in_cs = mw.compute_modes(cs, num_modes=self.num_modes)
            self.modes_per_cell.append(modes_in_cs)

    def compute_sparameters(self) -> dict[str, np.ndarray]:
        """Returns Sparameters using EME."""
        if self.filepath.exists():
            if not self.overwrite:
                logger.info(f"Simulation loaded from {self.filepath!r}")
                sp = dict(np.load(self.filepath))

                def rename(p):
                    return p.replace("o1", "left").replace("o2", "right")

                sdict = {
                    tuple(rename(p) for p in k.split(",")): np.asarray(v)
                    for k, v in sp.items()
                }
                S, self.port_map = sax.sdense(sdict)
                self.S = np.asarray(S).view(np.ndarray)
                return sp
            else:
                self.filepath.unlink()

        start = time.time()

        self.compute_all_modes()

        self.S, self.port_map = _compute_s_matrix(self.modes_per_cell, self.cells)

        sdict = sax.sdict((self.S, self.port_map))

        def rename(p):
            return p.replace("left", "o1").replace("right", "o2")

        sp = {
            f"{rename(p1)},{rename(p2)}": np.asarray(v) for (p1, p2), v in sdict.items()
        }

        np.savez_compressed(self.filepath, **sp)

        end = time.time()

        self.sim_settings.update(compute_time_seconds=end - start)
        self.sim_settings.update(compute_time_minutes=(end - start) / 60)
        logger.info(f"Write simulation results to {self.filepath!r}")
        self.filepath_sim_settings.write_text(yaml.dump(self.sim_settings))
        logger.info(f"Write simulation settings to {self.filepath_sim_settings!r}")

        return sp

__init__(component, layer_stack, wavelength=1.55, temperature=25.0, num_modes=4, cell_length=0.5, spacing_x=2.0, center_x=None, resolution_x=100, spacing_y=2.0, center_y=None, resolution_y=100, material_to_color=material_to_color_default, dirpath=PATH.sparameters, filepath=None, overwrite=False)

Computes multimode 2-port S-parameters for a gdsfactory component.

assumes port 1 is at the left boundary and port 2 at the right boundary.

Note coordinate systems

gdsfactory uses x,y in the plane to represent components, with the layer_stack existing in z meow uses x,y to represent a cross-section, with propagation in the z-direction hence we have [x,y,z] <--> [y,z,x] for gdsfactory <--> meow

Parameters:

Name Type Description Default
component Component

gdsfactory component.

required
layer_stack

gdsfactory layer_stack.

required
wavelength float

wavelength in microns (for FDE, and for material properties).

1.55
temperature float

temperature in C (for material properties). Unused now.

25.0
num_modes int

number of modes to compute for the eigenmode expansion.

4
cell_length float

in un.

0.5
spacing_x float

at beginning and end of the simulation region.

2.0
center_x float | None

in um.

None
resolution_x int

pixels in horizontal region.

100
spacing_y float

at the beginning and end of simulation region.

2.0
center_y float | None

in um.

None
resolution_y int

pixels in vertical direction.

100
material_to_color dict[str, ColorRGB]

dict of materials colors for struct plot

material_to_color_default
dirpath PathType | None

directory to store Sparameters.

sparameters
filepath PathType | None

to store pandas Dataframe with Sparameters in npz format. Defaults to dirpath/component_.npz.

None
overwrite bool

overwrites stored Sparameter npz results.

False

Returns:

Type Description
None

S-parameters in form o1@0,o2@0 at wavelength.

::

cross_section view:
   ________________________________
  |                                |
  |                                |
  | spacing_x            spacing_x |  spacing_y
  |<--------->           <-------->|
  |          ___________   _ _ _   |
  |         |           |          |
  |         |           |_ _ _ _ _ |_ center_y
  |         |           |          |
  |         |___________|          |
  |               |                |
  |                                |
  |               |                |  spacing_y
  |                                |
  |_______________|________________|
              center_x

top side view:
   ________________________________
  |                                |
  |                                |
  |cell_length                     |
  |<-------->                      |
  |_____________________ __________|
  |         |           |          |
  |         |           |          |
  | cell0   |  cell1    |  cell2   |
  |_________|___________|__________|
  |                                |
  |                                |
  |                                |
  |                                |
  |________________________________|
Source code in gplugins/meow/meow_eme.py
def __init__(
    self,
    component: gf.Component,
    layer_stack,
    wavelength: float = 1.55,
    temperature: float = 25.0,
    num_modes: int = 4,
    cell_length: float = 0.5,
    spacing_x: float = 2.0,
    center_x: float | None = None,
    resolution_x: int = 100,
    spacing_y: float = 2.0,
    center_y: float | None = None,
    resolution_y: int = 100,
    material_to_color: dict[str, ColorRGB] = material_to_color_default,
    dirpath: PathType | None = PATH.sparameters,
    filepath: PathType | None = None,
    overwrite: bool = False,
) -> None:
    """Computes multimode 2-port S-parameters for a gdsfactory component.

    assumes port 1 is at the left boundary and port 2 at the right boundary.

    Note coordinate systems:
        gdsfactory uses x,y in the plane to represent components, with the layer_stack existing in z
        meow uses x,y to represent a cross-section, with propagation in the z-direction
        hence we have [x,y,z] <--> [y,z,x] for gdsfactory <--> meow

    Arguments:
        component: gdsfactory component.
        layer_stack: gdsfactory layer_stack.
        wavelength: wavelength in microns (for FDE, and for material properties).
        temperature: temperature in C (for material properties). Unused now.
        num_modes: number of modes to compute for the eigenmode expansion.
        cell_length: in un.
        spacing_x: at beginning and end of the simulation region.
        center_x: in um.
        resolution_x: pixels in horizontal region.
        spacing_y: at the beginning and end of simulation region.
        center_y: in um.
        resolution_y: pixels in vertical direction.
        material_to_color: dict of materials colors for struct plot
        dirpath: directory to store Sparameters.
        filepath: to store pandas Dataframe with Sparameters in npz format.
            Defaults to dirpath/component_.npz.
        overwrite: overwrites stored Sparameter npz results.

    Returns:
        S-parameters in form o1@0,o2@0 at wavelength.

    ::

        cross_section view:
           ________________________________
          |                                |
          |                                |
          | spacing_x            spacing_x |  spacing_y
          |<--------->           <-------->|
          |          ___________   _ _ _   |
          |         |           |          |
          |         |           |_ _ _ _ _ |_ center_y
          |         |           |          |
          |         |___________|          |
          |               |                |
          |                                |
          |               |                |  spacing_y
          |                                |
          |_______________|________________|
                      center_x

        top side view:
           ________________________________
          |                                |
          |                                |
          |cell_length                     |
          |<-------->                      |
          |_____________________ __________|
          |         |           |          |
          |         |           |          |
          | cell0   |  cell1    |  cell2   |
          |_________|___________|__________|
          |                                |
          |                                |
          |                                |
          |                                |
          |________________________________|
    """
    # Validate component
    self.validate_component(component)

    # Save parameters
    self.wavelength = wavelength
    self.num_modes = num_modes
    self.temperature = temperature  # unused for now
    self.material_to_color = material_to_color

    # Process simulation bounds
    z_min, x_min, z_max, x_max = (
        component.xmin,
        component.ymin,
        component.xmax,
        component.ymax,
    )
    z_min, z_max = min(z_min, z_max) + 1e-10, max(z_min, z_max) - 1e-10
    x_min, x_max = min(x_min, x_max) + 1e-10, max(x_min, x_max) - 1e-10
    layer_stack = layer_stack.model_copy()
    ys = list_unique_layer_stack_z(layer_stack)
    y_min, y_max = np.min(ys) + 1e-10, np.max(ys) - 1e-10

    self.span_x = x_max - x_min + spacing_x
    self.center_x = center_x if center_x is not None else 0.5 * (x_max + x_min)
    self.resolution_x = resolution_x

    self.span_y = y_max - y_min + spacing_y
    self.center_y = center_y if center_y is not None else 0.5 * (y_max + y_min)
    self.resolution_y = resolution_y

    self.z_min = z_min
    self.z_max = z_max
    self.span_z = z_max - z_min

    self.cell_length = cell_length

    # you need two extra cells without length at beginning and end.
    self.num_cells = max(int(self.span_z / cell_length) + 2, 4)

    # Setup simulation
    self.component, self.layer_stack = self.add_global_layers(
        component, layer_stack
    )
    self.extrusion_rules = self.layer_stack_to_extrusion()
    self.structs = mw.extrude_gds(self.component, self.extrusion_rules)
    self.cells = self.create_cells()
    self.env = mw.Environment(wl=self.wavelength, T=self.temperature)
    self.css = [
        mw.CrossSection.from_cell(cell=cell, env=self.env) for cell in self.cells
    ]
    self.modes_per_cell = [None] * self.num_cells
    self.S = None
    self.port_map = None

    # Cache
    sim_settings = dict(
        wavelength=wavelength,
        temperature=temperature,
        num_modes=num_modes,
        cell_length=cell_length,
        spacing_x=spacing_x,
        center_x=center_x,
        resolution_x=resolution_x,
        spacing_y=spacing_y,
        center_y=center_y,
        resolution_y=resolution_y,
    )

    filepath = filepath or get_sparameters_path(
        component=component,
        dirpath=dirpath,
        layer_stack=layer_stack,
        **sim_settings,
    )

    sim_settings = sim_settings.copy()
    sim_settings["layer_stack"] = layer_stack.to_dict()
    sim_settings["component"] = component.to_dict()
    self.sim_settings = sim_settings
    self.filepath = pathlib.Path(filepath)
    self.filepath_sim_settings = filepath.with_suffix(".yml")
    self.overwrite = overwrite

gf_material_to_meow_material(material_name='si', wavelengths=None, color=None)

Converts a gdsfactory material into a MEOW material.

Source code in gplugins/meow/meow_eme.py
def gf_material_to_meow_material(
    self, material_name: str = "si", wavelengths=None, color=None
):
    """Converts a gdsfactory material into a MEOW material."""
    wavelengths = wavelengths or np.linspace(1.5, 1.6, 101)
    color = color or (0.9, 0.9, 0.9, 0.9)
    PDK = get_active_pdk()
    ns = PDK.materials_index[material_name](wavelengths)
    if ns.dtype in [np.float64, np.float32]:
        nr = ns
        ni = np.zeros_like(ns)
    else:
        nr = np.real(ns)
        ni = np.imag(ns)
    df = pd.DataFrame({"wl": wavelengths, "nr": nr, "ni": ni})
    return mw.SampledMaterial.from_df(
        material_name,
        df,
        meta={"color": color},
    )

add_global_layers(component, layer_stack, buffer_y=1, global_layer_index=10000, layer_wafer='WAFER')

Adds bbox polygons for global layers.

LAYER.WAFER layers are represented as polygons of size [bbox.x, xspan (meow coords)]

Parameters:

Name Type Description Default
component

gdsfactory component.

required
layer_stack

gdsfactory LayerStack.

required
buffer_y float

float, y-buffer to add to box.

1
xspan

from eme setup.

required
global_layer_index int

int, layer index at which to starting adding the global layers. Default 10000 with +1 increments to avoid clashing with physical layers.

10000
layer_wafer LayerSpec

LayerSpec, layer to represent the wafer.

'WAFER'
Source code in gplugins/meow/meow_eme.py
def add_global_layers(
    self,
    component,
    layer_stack,
    buffer_y: float = 1,
    global_layer_index: int = 10000,
    layer_wafer: LayerSpec = "WAFER",
) -> tuple[Component, LayerStack]:
    """Adds bbox polygons for global layers.

    LAYER.WAFER layers are represented as polygons of size [bbox.x, xspan (meow coords)]

    Arguments:
        component: gdsfactory component.
        layer_stack: gdsfactory LayerStack.
        buffer_y: float, y-buffer to add to box.
        xspan: from eme setup.
        global_layer_index: int, layer index at which to starting adding the global layers.
                Default 10000 with +1 increments to avoid clashing with physical layers.
        layer_wafer: LayerSpec, layer to represent the wafer.

    """
    c = gf.Component()
    c.add_ref(component)
    layer_wafer = gf.get_layer(layer_wafer)

    for level in layer_stack.layers.values():
        if isinstance(level.layer, DerivedLayer):
            continue
        layer = gf.get_layer(level.layer.layer)
        if layer == layer_wafer:
            c.add_ref(
                gf.components.bbox(
                    component,
                    layer=(global_layer_index, 0),
                )
            )
            layer = (global_layer_index, 0)
            global_layer_index += 1

    return c, layer_stack

layer_stack_to_extrusion()

Convert LayerStack to meow extrusions.

Source code in gplugins/meow/meow_eme.py
def layer_stack_to_extrusion(self):
    """Convert LayerStack to meow extrusions."""
    extrusions = {}
    for layer in self.layer_stack.layers.values():
        layer_tuple = gf.get_layer_tuple((layer.derived_layer or layer.layer).layer)
        if layer_tuple not in extrusions.keys():
            extrusions[layer_tuple] = []
        extrusions[layer_tuple].append(
            mw.GdsExtrusionRule(
                material=self.gf_material_to_meow_material(
                    layer.material,
                    np.array([self.wavelength]),
                    color=self.material_to_color.get(layer.material),
                ),
                h_min=layer.zmin,
                h_max=layer.zmin + layer.thickness,
                mesh_order=layer.mesh_order,
            )
        )
    return extrusions

create_cells()

Get meow cells from extruded component.

Parameters:

Name Type Description Default
cell_length

in um.

required
Source code in gplugins/meow/meow_eme.py
def create_cells(self) -> list[mw.Cell]:
    """Get meow cells from extruded component.

    Args:
        cell_length: in um.
    """
    zs = np.linspace(self.z_min, self.z_max, self.num_cells - 1)

    # add two cells without length:
    zs = np.concatenate([[self.z_min], zs, [self.z_max]])

    mesh = mw.Mesh2D(
        x=np.linspace(
            self.center_x - self.span_x / 2,
            self.center_x + self.span_x / 2,
            self.resolution_x,
        ),
        y=np.linspace(
            self.center_y - self.span_y / 2,
            self.center_y + self.span_y / 2,
            self.resolution_y,
        ),
        ez_interfaces=True,
    )
    cells = []
    for z_min, z_max in itertools.pairwise(zs):
        cell = mw.Cell(
            structures=self.structs,
            mesh=mesh,
            z_min=z_min,
            z_max=z_max,
        )
        cells.append(cell)

    return cells

compute_sparameters()

Returns Sparameters using EME.

Source code in gplugins/meow/meow_eme.py
def compute_sparameters(self) -> dict[str, np.ndarray]:
    """Returns Sparameters using EME."""
    if self.filepath.exists():
        if not self.overwrite:
            logger.info(f"Simulation loaded from {self.filepath!r}")
            sp = dict(np.load(self.filepath))

            def rename(p):
                return p.replace("o1", "left").replace("o2", "right")

            sdict = {
                tuple(rename(p) for p in k.split(",")): np.asarray(v)
                for k, v in sp.items()
            }
            S, self.port_map = sax.sdense(sdict)
            self.S = np.asarray(S).view(np.ndarray)
            return sp
        else:
            self.filepath.unlink()

    start = time.time()

    self.compute_all_modes()

    self.S, self.port_map = _compute_s_matrix(self.modes_per_cell, self.cells)

    sdict = sax.sdict((self.S, self.port_map))

    def rename(p):
        return p.replace("left", "o1").replace("right", "o2")

    sp = {
        f"{rename(p1)},{rename(p2)}": np.asarray(v) for (p1, p2), v in sdict.items()
    }

    np.savez_compressed(self.filepath, **sp)

    end = time.time()

    self.sim_settings.update(compute_time_seconds=end - start)
    self.sim_settings.update(compute_time_minutes=(end - start) / 60)
    logger.info(f"Write simulation results to {self.filepath!r}")
    self.filepath_sim_settings.write_text(yaml.dump(self.sim_settings))
    logger.info(f"Write simulation settings to {self.filepath_sim_settings!r}")

    return sp

FDTD Simulation

S-parameter utils

gplugins.common.utils.plot.plot_sparameters(sp, logscale=True, plot_phase=False, keys=None, with_simpler_input_keys=False, with_simpler_labels=True, units=1000.0)

Plots Sparameters from a dict of np.ndarrays.

Parameters:

Name Type Description Default
sp dict[str, NDArray[floating[Any]]]

Sparameters np.ndarray.

required
logscale bool

plots 20*log10(S).

True
plot_phase bool

plots angle of Sparameters in degrees.

False
keys tuple[str, ...] | None

list of keys to plot, plots all by default.

None
with_simpler_input_keys bool

You can use S12 keys instead of o1@0,o2@0.

False
with_simpler_labels bool

uses S11, S12 in plot labels instead of o1@0,o2@0.

True
units float

wavelength units. Default is 1e3 to convert um to nm.

1000.0
Source code in gplugins/common/utils/plot.py
def plot_sparameters(
    sp: dict[str, npt.NDArray[np.floating[Any]]],
    logscale: bool = True,
    plot_phase: bool = False,
    keys: tuple[str, ...] | None = None,
    with_simpler_input_keys: bool = False,
    with_simpler_labels: bool = True,
    units: float = 1e3,
) -> None:
    """Plots Sparameters from a dict of np.ndarrays.

    Args:
        sp: Sparameters np.ndarray.
        logscale: plots 20*log10(S).
        plot_phase: plots angle of Sparameters in degrees.
        keys: list of keys to plot, plots all by default.
        with_simpler_input_keys: You can use S12 keys instead of o1@0,o2@0.
        with_simpler_labels: uses S11, S12 in plot labels instead of o1@0,o2@0.
        units: wavelength units. Default is 1e3 to convert um to nm.

    """
    w = sp["wavelengths"] * units
    keys = keys or tuple(key for key in sp if not key.lower().startswith("wav"))

    for key in keys:
        if with_simpler_input_keys:
            key = f"o{key[1]}@0,o{key[2]}@0"
            if key not in sp:
                raise ValueError(f"{key!r} not in {list(sp.keys())}")

        if with_simpler_labels and "o" in key and "@" in key:
            port_mode1_port_mode2 = key.split(",")
            if len(port_mode1_port_mode2) != 2:
                raise ValueError(f"{key!r} needs to be 'portin@mode,portout@mode'")
            port_mode1, port_mode2 = port_mode1_port_mode2
            port1, _mode1 = port_mode1.split("@")
            port2, _mode2 = port_mode2.split("@")
            alias = f"S{port1[1:]}{port2[1:]}"
        else:
            alias = key

        if key not in sp:
            raise ValueError(f"{key!r} not in {list(sp.keys())}")
        y = sp[key]
        if plot_phase:
            y = np.angle(y)
            plt.ylabel("S (deg)")
        else:
            y = 20 * np.log10(np.abs(y)) if logscale else np.abs(y) ** 2
            plt.ylabel("|S| (dB)") if logscale else plt.ylabel("$|S|^2$")
        plt.plot(w, y, label=alias)
    plt.legend()
    plt.xlabel("wavelength (nm)")
    plt.show()

gplugins.common.utils.plot.plot_imbalance2x2 = partial(plot_imbalance, ports=['o1@0,o3@0', 'o1@0,o4@0']) module-attribute

gplugins.common.utils.plot.plot_loss2x2 = partial(plot_loss, ports=['o1@0,o3@0', 'o1@0,o4@0']) module-attribute

Common FDTD functions

gplugins.common.utils.get_effective_indices.get_effective_indices(core_material, nsubstrate, clad_materialding, thickness, wavelength, polarization)

Returns the effective refractive indices for a 1D mode.

Parameters:

Name Type Description Default
core_material float

Refractive index of the core material.

required
nsubstrate float

Refractive index of the substrate.

required
clad_materialding float

Refractive index of the cladding.

required
thickness float

Thickness of the film in um.

required
wavelength float

Wavelength in um.

required
polarization Literal['te', 'tm']

Either "te" or "tm".

required

.. code::

-----------------      |
clad_materialding             inf
-----------------      |
core_material              thickness
-----------------      |
nsubstrate            inf
-----------------      |

.. code::

import gplugins as sim

neffs = sim.get_effective_indices(
    core_material=3.4777,
    clad_materialding=1.444,
    nsubstrate=1.444,
    thickness=0.22,
    wavelength=1.55,
    polarization="te",
)
Source code in gplugins/common/utils/get_effective_indices.py
def get_effective_indices(
    core_material: float,
    nsubstrate: float,
    clad_materialding: float,
    thickness: float,
    wavelength: float,
    polarization: Literal["te", "tm"],
) -> list[float]:
    """Returns the effective refractive indices for a 1D mode.

    Args:
        core_material: Refractive index of the core material.
        nsubstrate: Refractive index of the substrate.
        clad_materialding: Refractive index of the cladding.
        thickness: Thickness of the film in um.
        wavelength: Wavelength in um.
        polarization: Either "te" or "tm".

    .. code::

        -----------------      |
        clad_materialding             inf
        -----------------      |
        core_material              thickness
        -----------------      |
        nsubstrate            inf
        -----------------      |

    .. code::

        import gplugins as sim

        neffs = sim.get_effective_indices(
            core_material=3.4777,
            clad_materialding=1.444,
            nsubstrate=1.444,
            thickness=0.22,
            wavelength=1.55,
            polarization="te",
        )

    """
    epsilon_core = core_material**2
    epsilon_cladding = clad_materialding**2
    epsilon_substrate = nsubstrate**2

    thickness *= 1e-6
    wavelength *= 1e-6

    if polarization == "te":
        tm = False
    elif polarization == "tm":
        tm = True
    else:
        raise ValueError('Polarization must be "te" or "tm"')

    k_0 = 2 * np.pi / wavelength

    def k_f(e_eff: npt.NDArray[np.floating[Any]]) -> npt.NDArray[np.floating[Any]]:
        return k_0 * np.sqrt(epsilon_core - e_eff) / (epsilon_core if tm else 1)

    def k_s(e_eff: npt.NDArray[np.floating[Any]]) -> npt.NDArray[np.floating[Any]]:
        return (
            k_0 * np.sqrt(e_eff - epsilon_substrate) / (epsilon_substrate if tm else 1)
        )

    def k_c(e_eff: npt.NDArray[np.floating[Any]]) -> npt.NDArray[np.floating[Any]]:
        return k_0 * np.sqrt(e_eff - epsilon_cladding) / (epsilon_cladding if tm else 1)

    def objective(
        e_eff: npt.NDArray[np.floating[Any]],
    ) -> npt.NDArray[np.floating[Any]]:
        return 1 / np.tan(k_f(e_eff) * thickness) - (
            k_f(e_eff) ** 2 - k_s(e_eff) * k_c(e_eff)
        ) / (k_f(e_eff) * (k_s(e_eff) + k_c(e_eff)))

    # scan roughly for indices
    # use a by 1e-10 smaller search area to avoid division by zero
    x = np.linspace(
        min(epsilon_substrate, epsilon_cladding) + 1e-10, epsilon_core - 1e-10, 1000
    )
    indices_temp = x[np.abs(objective(x)) < 0.1]
    if not len(indices_temp):
        return []

    # and then use fsolve to get exact indices
    indices_temp = cast(npt.NDArray[np.floating[Any]], fsolve(objective, indices_temp))

    indices: list[float] = []
    for index in indices_temp:
        if not any(np.isclose(index, i, atol=1e-5) for i in indices):
            indices.append(index)

    return cast(list[float], np.sqrt(indices).tolist())

S-parameter conversion

gplugins.common.utils.convert_sparameters.pandas_to_float64(df, magnitude_suffix='m', phase_suffix='a')

Converts a pandas CSV sparameters from complex128 format to 2x float64 format.

Adds magnitude_suffix (default m) and phase_suffix (default a) to original keys.

Parameters:

Name Type Description Default
df DataFrame

pandas DataFrame.

required
magnitude_suffix str

m for module.

'm'
phase_suffix str

a for angle.

'a'
Source code in gplugins/common/utils/convert_sparameters.py
def pandas_to_float64(
    df: pd.DataFrame,
    magnitude_suffix: str = "m",
    phase_suffix: str = "a",
) -> pd.DataFrame:
    """Converts a pandas CSV sparameters from complex128 format to 2x float64 format.

    Adds magnitude_suffix (default m) and phase_suffix (default a) to original keys.

    Args:
        df: pandas DataFrame.
        magnitude_suffix: m for module.
        phase_suffix: a for angle.
    """
    new_df = pd.DataFrame()

    for key in df.keys():
        if key != "wavelengths":
            new_df[f"{key}{magnitude_suffix}"] = df[key].real
            new_df[f"{key}{phase_suffix}"] = df[key].imag

    new_df["wavelengths"] = df["wavelengths"]

    return new_df

gplugins.common.utils.convert_sparameters.pandas_to_numpy(df, port_map=None)

Converts a pandas CSV sparameters into a numpy array.

Fundamental mode starts at 0.

Source code in gplugins/common/utils/convert_sparameters.py
def pandas_to_numpy(df: pd.DataFrame, port_map=None) -> np.ndarray:
    """Converts a pandas CSV sparameters into a numpy array.

    Fundamental mode starts at 0.
    """
    s_headers = sorted({c[:-1] for c in df.columns if c.lower().startswith("s")})
    idxs = sorted({idx for c in s_headers for idx in _s_header_to_port_idxs(c)})

    if port_map is None:
        port_map = {f"o{i}@0": i for i in idxs}
    rev_port_map = {i: p for p, i in port_map.items()}
    assert len(rev_port_map) == len(port_map), (
        "Duplicate port indices found in port_map"
    )

    s_map = {
        s: tuple(rev_port_map[i] for i in _s_header_to_port_idxs(s)) for s in s_headers
    }

    dfs = {
        s: df[["wavelengths", f"{s}m", f"{s}a"]]
        .copy()
        .rename(columns={f"{s}m": "magnitude", f"{s}a": "phase"})
        for s in s_map
    }

    S = dict(wavelengths=df["wavelengths"].values)
    for key, df in dfs.items():
        pm1, pm2 = s_map[key]
        (p1, m1), (p2, m2) = pm1.split("@"), pm2.split("@")
        name = f"{p1}@{m1},{p2}@{m2}"
        S[name] = df["magnitude"].values * np.exp(1j * df["phase"].values)

    return S

gplugins.common.utils.convert_sparameters.csv_to_npz(filepath)

Convert CSV files into numpy.

Source code in gplugins/common/utils/convert_sparameters.py
def csv_to_npz(filepath: PathType) -> pathlib.Path:
    """Convert CSV files into numpy."""
    df = pd.read_csv(filepath)
    sp = pandas_to_numpy(df)
    filepath_npz = pathlib.Path(filepath).with_suffix(".npz")
    np.savez_compressed(filepath_npz, **sp)
    return filepath_npz

gplugins.common.utils.convert_sparameters.convert_directory_csv_to_npz(dirpath)

Convert CSV files from directory dirpath into numpy.

Source code in gplugins/common/utils/convert_sparameters.py
def convert_directory_csv_to_npz(dirpath: PathType) -> None:
    """Convert CSV files from directory dirpath into numpy."""
    dirpath = pathlib.Path(dirpath)
    for filepath in tqdm(dirpath.glob("**/*.csv")):
        try:
            csv_to_npz(filepath)
        except Exception as e:
            print(filepath)
            print(e)

FDTD tidy3d

gplugins.tidy3d.write_sparameters(component, layer_stack=None, material_mapping=material_name_to_medium, extend_ports=0.5, port_offset=0.2, pad_xy_inner=2.0, pad_xy_outer=2.0, pad_z_inner=0.0, pad_z_outer=0.0, dilation=0.0, wavelength=1.55, bandwidth=0.2, num_freqs=21, min_steps_per_wvl=30, center_z=None, sim_size_z=4.0, port_size_mult=(4.0, 3.0), run_only=None, element_mappings=(), extra_monitors=None, mode_spec=td.ModeSpec(num_modes=1), boundary_spec=td.BoundarySpec.all_sides(boundary=(td.PML())), symmetry=(0, 0, 0), run_time=1e-12, shutoff=1e-05, folder_name='default', dirpath=dirpath_default, verbose=True, plot_simulation_layer_name=None, plot_simulation_port_index=0, plot_simulation_z=None, plot_simulation_x=None, plot_mode_index=0, plot_mode_port_name=None, plot_epsilon=False, filepath=None, overwrite=False, **kwargs)

Writes the S-parameters for a component.

Parameters:

Name Type Description Default
component Component

gdsfactory component to write the S-parameters for.

required
layer_stack LayerStack | None

The layer stack for the component. If None, uses active pdk layer_stack.

None
material_mapping dict[str, Tidy3DMedium]

A mapping of material names to Tidy3DMedium instances. Defaults to material_name_to_medium.

material_name_to_medium
extend_ports NonNegativeFloat

The extension length for ports.

0.5
port_offset float

The offset for ports. Defaults to 0.2.

0.2
pad_xy_inner NonNegativeFloat

The inner padding in the xy-plane. Defaults to 2.0.

2.0
pad_xy_outer NonNegativeFloat

The outer padding in the xy-plane. Defaults to 2.0.

2.0
pad_z_inner float

The inner padding in the z-direction. Defaults to 0.0.

0.0
pad_z_outer NonNegativeFloat

The outer padding in the z-direction. Defaults to 0.0.

0.0
dilation float

Dilation of the polygon in the base by shifting each edge along its normal outwards direction by a distance;

0.0
wavelength float

The wavelength for the ModalComponentModeler. Defaults to 1.55.

1.55
bandwidth float

The bandwidth for the ModalComponentModeler. Defaults to 0.2.

0.2
num_freqs int

The number of frequencies for the ModalComponentModeler. Defaults to 21.

21
min_steps_per_wvl int

The minimum number of steps per wavelength for the ModalComponentModeler. Defaults to 30.

30
center_z float | str | None

The z-coordinate for the center of the ModalComponentModeler. If None, the z-coordinate of the component is used. Defaults to None.

None
sim_size_z float

simulation size um in the z-direction for the ModalComponentModeler. Defaults to 4.

4.0
port_size_mult float | tuple[float, float]

The size multiplier for the ports in the ModalComponentModeler. Defaults to (4.0, 3.0).

(4.0, 3.0)
run_only tuple[tuple[str, int], ...] | None

The run only specification for the ModalComponentModeler. Defaults to None.

None
element_mappings Tidy3DElementMapping

The element mappings for the ModalComponentModeler. Defaults to ().

()
extra_monitors tuple[Any, ...] | None

The extra monitors for the ModalComponentModeler. Defaults to None.

None
mode_spec ModeSpec

The mode specification for the ModalComponentModeler. Defaults to td.ModeSpec(num_modes=1).

ModeSpec(num_modes=1)
boundary_spec BoundarySpec

The boundary specification for the ModalComponentModeler. Defaults to td.BoundarySpec.all_sides(boundary=td.PML()).

all_sides(boundary=PML())
symmetry tuple[Symmetry, Symmetry, Symmetry]

The symmetry for the simulation. Defaults to (0,0,0).

(0, 0, 0)
run_time float

The run time for the ModalComponentModeler.

1e-12
shutoff float

The shutoff value for the ModalComponentModeler. Defaults to 1e-5.

1e-05
folder_name str

The folder name for the ModalComponentModeler in flexcompute website. Defaults to "default".

'default'
dirpath PathType

Optional directory path for writing the Sparameters. Defaults to "~/.gdsfactory/sparameters".

dirpath_default
verbose bool

Whether to print verbose output for the ModalComponentModeler. Defaults to True.

True
plot_simulation_layer_name str | None

Optional layer name to plot. Defaults to None.

None
plot_simulation_port_index int

which port index to plot. Defaults to 0.

0
plot_simulation_z float | None

which z coordinate to plot. Defaults to None.

None
plot_simulation_x float | None

which x coordinate to plot. Defaults to None.

None
plot_mode_index int | None

which mode index to plot. Defaults to 0.

0
plot_mode_port_name str | None

which port name to plot. Defaults to None.

None
plot_epsilon bool

whether to plot epsilon. Defaults to False.

False
filepath PathType | None

Optional file path for the S-parameters. If None, uses hash of simulation.

None
overwrite bool

Whether to overwrite existing S-parameters. Defaults to False.

False
kwargs Any

Additional keyword arguments for the tidy3d Simulation constructor.

{}
Source code in gplugins/tidy3d/component.py
def write_sparameters(
    component: Component,
    layer_stack: LayerStack | None = None,
    material_mapping: dict[str, Tidy3DMedium] = material_name_to_medium,
    extend_ports: NonNegativeFloat = 0.5,
    port_offset: float = 0.2,
    pad_xy_inner: NonNegativeFloat = 2.0,
    pad_xy_outer: NonNegativeFloat = 2.0,
    pad_z_inner: float = 0.0,
    pad_z_outer: NonNegativeFloat = 0.0,
    dilation: float = 0.0,
    wavelength: float = 1.55,
    bandwidth: float = 0.2,
    num_freqs: int = 21,
    min_steps_per_wvl: int = 30,
    center_z: float | str | None = None,
    sim_size_z: float = 4.0,
    port_size_mult: float | tuple[float, float] = (4.0, 3.0),
    run_only: tuple[tuple[str, int], ...] | None = None,
    element_mappings: Tidy3DElementMapping = (),
    extra_monitors: tuple[Any, ...] | None = None,
    mode_spec: td.ModeSpec = td.ModeSpec(num_modes=1),
    boundary_spec: td.BoundarySpec = td.BoundarySpec.all_sides(boundary=td.PML()),
    symmetry: tuple[Symmetry, Symmetry, Symmetry] = (0, 0, 0),
    run_time: float = 1e-12,
    shutoff: float = 1e-5,
    folder_name: str = "default",
    dirpath: PathType = dirpath_default,
    verbose: bool = True,
    plot_simulation_layer_name: str | None = None,
    plot_simulation_port_index: int = 0,
    plot_simulation_z: float | None = None,
    plot_simulation_x: float | None = None,
    plot_mode_index: int | None = 0,
    plot_mode_port_name: str | None = None,
    plot_epsilon: bool = False,
    filepath: PathType | None = None,
    overwrite: bool = False,
    **kwargs: Any,
) -> Sparameters:
    """Writes the S-parameters for a component.

    Args:
        component: gdsfactory component to write the S-parameters for.
        layer_stack: The layer stack for the component. If None, uses active pdk layer_stack.
        material_mapping: A mapping of material names to Tidy3DMedium instances. Defaults to material_name_to_medium.
        extend_ports: The extension length for ports.
        port_offset: The offset for ports. Defaults to 0.2.
        pad_xy_inner: The inner padding in the xy-plane. Defaults to 2.0.
        pad_xy_outer: The outer padding in the xy-plane. Defaults to 2.0.
        pad_z_inner: The inner padding in the z-direction. Defaults to 0.0.
        pad_z_outer: The outer padding in the z-direction. Defaults to 0.0.
        dilation: Dilation of the polygon in the base by shifting each edge along its normal outwards direction by a distance;
        wavelength: The wavelength for the ModalComponentModeler. Defaults to 1.55.
        bandwidth: The bandwidth for the ModalComponentModeler. Defaults to 0.2.
        num_freqs: The number of frequencies for the ModalComponentModeler. Defaults to 21.
        min_steps_per_wvl: The minimum number of steps per wavelength for the ModalComponentModeler. Defaults to 30.
        center_z: The z-coordinate for the center of the ModalComponentModeler.
            If None, the z-coordinate of the component is used. Defaults to None.
        sim_size_z: simulation size um in the z-direction for the ModalComponentModeler. Defaults to 4.
        port_size_mult: The size multiplier for the ports in the ModalComponentModeler. Defaults to (4.0, 3.0).
        run_only: The run only specification for the ModalComponentModeler. Defaults to None.
        element_mappings: The element mappings for the ModalComponentModeler. Defaults to ().
        extra_monitors: The extra monitors for the ModalComponentModeler. Defaults to None.
        mode_spec: The mode specification for the ModalComponentModeler. Defaults to td.ModeSpec(num_modes=1).
        boundary_spec: The boundary specification for the ModalComponentModeler.
            Defaults to td.BoundarySpec.all_sides(boundary=td.PML()).
        symmetry (tuple[Symmetry, Symmetry, Symmetry], optional): The symmetry for the simulation. Defaults to (0,0,0).
        run_time: The run time for the ModalComponentModeler.
        shutoff: The shutoff value for the ModalComponentModeler. Defaults to 1e-5.
        folder_name: The folder name for the ModalComponentModeler in flexcompute website. Defaults to "default".
        dirpath: Optional directory path for writing the Sparameters. Defaults to "~/.gdsfactory/sparameters".
        verbose: Whether to print verbose output for the ModalComponentModeler. Defaults to True.
        plot_simulation_layer_name: Optional layer name to plot. Defaults to None.
        plot_simulation_port_index: which port index to plot. Defaults to 0.
        plot_simulation_z: which z coordinate to plot. Defaults to None.
        plot_simulation_x: which x coordinate to plot. Defaults to None.
        plot_mode_index: which mode index to plot. Defaults to 0.
        plot_mode_port_name: which port name to plot. Defaults to None.
        plot_epsilon: whether to plot epsilon. Defaults to False.
        filepath: Optional file path for the S-parameters. If None, uses hash of simulation.
        overwrite: Whether to overwrite existing S-parameters. Defaults to False.
        kwargs: Additional keyword arguments for the tidy3d Simulation constructor.

    """
    layer_stack = layer_stack or get_layer_stack()

    c = Tidy3DComponent(
        component=component,
        layer_stack=layer_stack,
        material_mapping=material_mapping,
        extend_ports=extend_ports,
        port_offset=port_offset,
        pad_xy_inner=pad_xy_inner,
        pad_xy_outer=pad_xy_outer,
        pad_z_inner=pad_z_inner,
        pad_z_outer=pad_z_outer,
        dilation=dilation,
    )

    modeler = c.get_component_modeler(
        wavelength=wavelength,
        bandwidth=bandwidth,
        num_freqs=num_freqs,
        min_steps_per_wvl=min_steps_per_wvl,
        center_z=center_z,
        sim_size_z=sim_size_z,
        port_size_mult=port_size_mult,
        run_only=run_only,
        element_mappings=element_mappings,
        extra_monitors=extra_monitors,
        mode_spec=mode_spec,
        boundary_spec=boundary_spec,
        run_time=run_time,
        shutoff=shutoff,
        symmetry=symmetry,
        **kwargs,
    )
    task_name = modeler._hash_self()
    path_dir = pathlib.Path(dirpath) / task_name
    modeler = modeler.updated_copy()

    sp = {}

    if plot_simulation_layer_name or plot_simulation_z or plot_simulation_x:
        if plot_simulation_layer_name is None and plot_simulation_z is None:
            raise ValueError(
                "You need to specify plot_simulation_z or plot_simulation_layer_name"
            )
        z = plot_simulation_z or c.get_layer_center(plot_simulation_layer_name)[2]
        x = plot_simulation_x or c.ports[plot_simulation_port_index].dcenter[0]

        modeler = c.get_component_modeler(
            center_z=plot_simulation_layer_name,
            port_size_mult=port_size_mult,
            sim_size_z=sim_size_z,
        )
        _, ax = plt.subplots(2, 1)
        if plot_epsilon:
            modeler.plot_sim_eps(z=z, ax=ax[0])
            modeler.plot_sim_eps(x=x, ax=ax[1])

        else:
            modeler.plot_sim(z=z, ax=ax[0])
            modeler.plot_sim(x=x, ax=ax[1])
        plt.show()
        return sp

    elif plot_mode_index is not None and plot_mode_port_name:
        modes = get_mode_solvers(modeler, port_name=plot_mode_port_name)
        mode_solver = modes[f"smatrix_{plot_mode_port_name}_{plot_mode_index}"]
        mode_data = mode_solver.solve()

        _, ax = plt.subplots(1, 3, tight_layout=True, figsize=(10, 3))
        abs(mode_data.Ex.isel(mode_index=plot_mode_index, f=0)).plot(
            x="y", y="z", ax=ax[0], cmap="magma"
        )
        abs(mode_data.Ey.isel(mode_index=plot_mode_index, f=0)).plot(
            x="y", y="z", ax=ax[1], cmap="magma"
        )
        abs(mode_data.Ez.isel(mode_index=plot_mode_index, f=0)).plot(
            x="y", y="z", ax=ax[2], cmap="magma"
        )
        ax[0].set_title("|Ex(x, y)|")
        ax[1].set_title("|Ey(x, y)|")
        ax[2].set_title("|Ez(x, y)|")
        plt.setp(ax, aspect="equal")
        plt.show()
        return sp

    dirpath = pathlib.Path(dirpath)
    dirpath.mkdir(parents=True, exist_ok=True)
    filepath = filepath or dirpath / f"{modeler._hash_self()}.npz"
    filepath = pathlib.Path(filepath)
    if filepath.suffix != ".npz":
        filepath = filepath.with_suffix(".npz")

    if filepath.exists() and not overwrite:
        print(f"Simulation loaded from {filepath!r}")
        return dict(np.load(filepath))
    else:
        time.sleep(0.2)
        modeler_data = web.run(
            modeler,  # TODO: web.run does not currently support ModalComponentModeler, need to convert to tidy3d_stub.SimulationType
            task_name=task_name,
            verbose=verbose,
            path=path_dir / "simulation.hdf5",
        )
        s = modeler_data.smatrix()
        for port_in in s.port_in.values:
            for port_out in s.port_out.values:
                for mode_index_in in s.mode_index_in.values:
                    for mode_index_out in s.mode_index_out.values:
                        sp[f"{port_in}@{mode_index_in},{port_out}@{mode_index_out}"] = (
                            s.sel(
                                port_in=port_in,
                                port_out=port_out,
                                mode_index_in=mode_index_in,
                                mode_index_out=mode_index_out,
                            ).values
                        )

        frequency = s.f.values
        sp["wavelengths"] = td.constants.C_0 / frequency
        np.savez_compressed(filepath, **sp)
        print(f"Simulation saved to {filepath!r}")
        return sp

gplugins.tidy3d.write_sparameters_grating_coupler

plot_simulation(sim, z=0.0, y=0.0, wavelength=1.55, figsize=(11, 4))

Returns Simulation visual representation. Returns two views for 3D component and one view for 2D.

Parameters:

Name Type Description Default
sim Simulation

simulation object.

required
z float

(um).

0.0
y float

(um).

0.0
wavelength float | None

(um) for epsilon plot. None plot structures only.

1.55
figsize tuple[float, float]

figure size.

(11, 4)
Source code in gplugins/tidy3d/write_sparameters_grating_coupler.py
def plot_simulation(
    sim: td.Simulation,
    z: float = 0.0,
    y: float = 0.0,
    wavelength: float | None = 1.55,
    figsize: tuple[float, float] = (11, 4),
):
    """Returns Simulation visual representation. Returns two views for 3D component and one view for 2D.

    Args:
        sim: simulation object.
        z: (um).
        y: (um).
        wavelength: (um) for epsilon plot. None plot structures only.
        figsize: figure size.

    """
    fig = plt.figure(figsize=figsize)
    if sim.size[2] > 0.1 and sim.size[1] > 0.1:
        gs = mpl.gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 1.4])
        ax1 = fig.add_subplot(gs[0, 0])
        ax2 = fig.add_subplot(gs[0, 1])
        if wavelength:
            freq = td.constants.C_0 / wavelength
            sim.plot_eps(z=z, ax=ax1, freq=freq)
            sim.plot_eps(y=y, ax=ax2, freq=freq)
        else:
            sim.plot(z=z, ax=ax1)
            sim.plot(y=y, ax=ax2)
    elif sim.size[2] > 0.1:  # 2D grating sim_size_y = 0
        gs = mpl.gridspec.GridSpec(1, 1, figure=fig, width_ratios=[1])
        ax1 = fig.add_subplot(gs[0, 0])
        if wavelength:
            freq = td.constants.C_0 / wavelength
            sim.plot_eps(y=y, ax=ax1, freq=freq)
        else:
            sim.plot(y=y, ax=ax1)

    else:  # 2D planar component size_z = 0
        gs = mpl.gridspec.GridSpec(1, 1, figure=fig, width_ratios=[1])
        ax1 = fig.add_subplot(gs[0, 0])
        if wavelength:
            freq = td.constants.C_0 / wavelength
            sim.plot_eps(z=z, ax=ax1, freq=freq)
        else:
            sim.plot(z=z, ax=ax1)

    plt.show()
    return fig

write_sparameters_grating_coupler(component, dirpath=None, filepath=None, overwrite=False, port_waveguide_name='o1', fiber_port_prefix='o2', verbose=False, run=True, **kwargs)

Get sparameter matrix from a gdsfactory grating coupler.

Assumes grating coupler waveguide port is facing to the left (west).

TODO: add a fiber model (more realistic than a gaussian_beam).

Parameters:

Name Type Description Default
component ComponentSpec

grating coupler gdsfactory Component to simulate.

required
dirpath PathType | None

directory to store sparameters in npz. Defaults to active Pdk.sparameters_path.

None
filepath PathType | None

optional sparameters file.

None
overwrite bool

overwrites stored Sparameter npz results.

False
verbose bool

prints info messages and progressbars.

False
run bool

runs simulation, if False, only plots simulation.

True

Other Parameters:

Name Type Description
port_extension

extend ports beyond the PML.

layer_stack

contains layer to thickness, zmin and material. Defaults to active pdk.layer_stack.

thickness_pml

PML thickness (um).

xmargin

left/right distance from component to PML.

xmargin_left

left distance from component to PML.

xmargin_right

right distance from component to PML.

ymargin

left/right distance from component to PML.

ymargin_top

top distance from component to PML.

ymargin_bot

bottom distance from component to PML.

zmargin

thickness for cladding above and below core.

clad_material

material for cladding.

box_material

for bottom cladding.

substrate_material

for substrate.

box_thickness

bottom cladding thickness in (um).

substrate_thickness

(um).

port_waveguide_name str

input port name.

port_margin

margin on each side of the port.

distance_source_to_monitors

in (um) source goes before monitors.

port_waveguide_offset

mode solver workaround. positive moves source forward, negative moves source backward.

wavelength

source center wavelength (um). if None takes mean between wavelength_start, wavelength_stop.

wavelength_start

in (um).

wavelength_stop

in (um).

wavelength_points

number of wavelengths.

plot_modes

plot source modes.

num_modes

number of modes to plot.

run_time_ps

make sure it's sufficient for the fields to decay. defaults to 10ps and counts on the automatic shutoff to stop earlier if needed.

fiber_port_prefix str

port prefix to place fiber source.

fiber_xoffset

fiber center xoffset to fiber_port_name.

fiber_z

fiber zoffset from grating zmax.

fiber_mfd

fiber mode field diameter (um).

fiber_angle_deg

fiber_angle in degrees with respect to normal.

material_name_to_tidy3d

dispersive materials have a wavelength dependent index. Maps layer_stack names with tidy3d material database names.

is_3d

True by default runs in 3D.

with_all_monitors

stores all monitor fields.

kwargs

simulation settings.

Source code in gplugins/tidy3d/write_sparameters_grating_coupler.py
def write_sparameters_grating_coupler(
    component: ComponentSpec,
    dirpath: PathType | None = None,
    filepath: PathType | None = None,
    overwrite: bool = False,
    port_waveguide_name: str = "o1",
    fiber_port_prefix: str = "o2",
    verbose: bool = False,
    run: bool = True,
    **kwargs,
) -> Sparameters:
    """Get sparameter matrix from a gdsfactory grating coupler.

    Assumes grating coupler waveguide port is facing to the left (west).

    TODO: add a fiber model (more realistic than a gaussian_beam).

    Args:
        component: grating coupler gdsfactory Component to simulate.
        dirpath: directory to store sparameters in npz.
            Defaults to active Pdk.sparameters_path.
        filepath: optional sparameters file.
        overwrite: overwrites stored Sparameter npz results.
        verbose: prints info messages and progressbars.
        run: runs simulation, if False, only plots simulation.

    Keyword Args:
        port_extension: extend ports beyond the PML.
        layer_stack: contains layer to thickness, zmin and material.
            Defaults to active pdk.layer_stack.
        thickness_pml: PML thickness (um).
        xmargin: left/right distance from component to PML.
        xmargin_left: left distance from component to PML.
        xmargin_right: right distance from component to PML.
        ymargin: left/right distance from component to PML.
        ymargin_top: top distance from component to PML.
        ymargin_bot: bottom distance from component to PML.
        zmargin: thickness for cladding above and below core.
        clad_material: material for cladding.
        box_material: for bottom cladding.
        substrate_material: for substrate.
        box_thickness: bottom cladding thickness in (um).
        substrate_thickness: (um).
        port_waveguide_name: input port name.
        port_margin: margin on each side of the port.
        distance_source_to_monitors: in (um) source goes before monitors.
        port_waveguide_offset: mode solver workaround.
            positive moves source forward, negative moves source backward.
        wavelength: source center wavelength (um).
            if None takes mean between wavelength_start, wavelength_stop.
        wavelength_start: in (um).
        wavelength_stop: in (um).
        wavelength_points: number of wavelengths.
        plot_modes: plot source modes.
        num_modes: number of modes to plot.
        run_time_ps: make sure it's sufficient for the fields to decay.
            defaults to 10ps and counts on the automatic shutoff
            to stop earlier if needed.
        fiber_port_prefix: port prefix to place fiber source.
        fiber_xoffset: fiber center xoffset to fiber_port_name.
        fiber_z: fiber zoffset from grating zmax.
        fiber_mfd: fiber mode field diameter (um).
        fiber_angle_deg: fiber_angle in degrees with respect to normal.
        material_name_to_tidy3d: dispersive materials have a wavelength dependent index.
            Maps layer_stack names with tidy3d material database names.
        is_3d: True by default runs in 3D.
        with_all_monitors: stores all monitor fields.
        kwargs: simulation settings.

    """
    component = gf.get_component(component)
    if not isinstance(component, Component):
        raise ValueError(f"component should be a gdsfactory.Component not {component}")

    filepath = filepath or get_sparameters_path(
        component=component,
        dirpath=dirpath,
        **kwargs,
    )
    filepath = pathlib.Path(filepath).with_suffix(".npz")
    filepath_sim_settings = filepath.with_suffix(".yml")

    if filepath.exists() and not overwrite and run:
        logger.info(f"Simulation loaded from {filepath!r}")
        return dict(np.load(filepath))

    sim = get_simulation_grating_coupler(
        component,
        fiber_port_prefix=fiber_port_prefix,
        port_waveguide_name=port_waveguide_name,
        **kwargs,
    )
    if not run:
        plot_simulation(sim)
        return {}

    start = time.time()
    sim_data = get_results(sim, verbose=verbose)
    sim_data = sim_data.result()

    direction_inp = "+"
    monitor_entering = (
        sim_data.monitor_data["waveguide"]
        .amps.sel(direction=direction_inp)
        .values.flatten()
    )
    direction_out = "-"
    monitor_exiting = (
        sim_data.monitor_data["waveguide"]
        .amps.sel(direction=direction_out)
        .values.flatten()
    )
    r = monitor_entering / monitor_exiting
    t = monitor_exiting

    fiber_port_name = None
    port_names = [port.name for port in component.ports]
    for port_name in port_names:
        if port_name.startswith(fiber_port_prefix):
            fiber_port_name = port_name

    if fiber_port_name is None:
        raise ValueError(f"No port named {fiber_port_prefix!r} in {port_names}")

    freqs = sim_data.monitor_data["waveguide"].amps.sel(direction="+").f
    port_name_input = port_waveguide_name
    fiber_port_name = "o2"

    key = f"{port_name_input}@0,{port_name_input}@0"
    sp = {"wavelengths": td.constants.C_0 / freqs.values, key: r}
    key = f"{fiber_port_name}@0,{fiber_port_name}@0"
    sp[key] = r

    key = f"{port_name_input}@0,{fiber_port_name}@0"
    sp[key] = t

    key = f"{fiber_port_name}@0,{port_name_input}@0"
    sp[key] = t

    end = time.time()
    np.savez_compressed(filepath, **sp)
    kwargs.update(compute_time_seconds=end - start)
    kwargs.update(compute_time_minutes=(end - start) / 60)

    filepath_sim_settings.write_text(yaml.dump(clean_value_json(kwargs)))
    logger.info(f"Write simulation results to {str(filepath)!r}")
    logger.info(f"Write simulation settings to {str(filepath_sim_settings)!r}")
    return sp

write_sparameters_grating_coupler_batch(jobs, **kwargs)

Returns Sparameters for a list of write_sparameters.

Each job runs in separate thread and is non blocking. You need to get the results using sp.result().

Parameters:

Name Type Description Default
jobs list[dict[str, Any]]

list of kwargs for write_sparameters_grating_coupler.

required
kwargs

simulation settings.

{}
Source code in gplugins/tidy3d/write_sparameters_grating_coupler.py
def write_sparameters_grating_coupler_batch(
    jobs: list[dict[str, Any]], **kwargs
) -> list[Awaitable[Sparameters]]:
    """Returns Sparameters for a list of write_sparameters.

    Each job runs in separate thread and is non blocking.
    You need to get the results using sp.result().

    Args:
        jobs: list of kwargs for write_sparameters_grating_coupler.
        kwargs: simulation settings.
    """
    kwargs.update(verbose=False)
    return [
        _executor.submit(write_sparameters_grating_coupler, **job, **kwargs)
        for job in jobs
    ]

gplugins.tidy3d.write_sparameters_grating_coupler_batch(jobs, **kwargs)

Returns Sparameters for a list of write_sparameters.

Each job runs in separate thread and is non blocking. You need to get the results using sp.result().

Parameters:

Name Type Description Default
jobs list[dict[str, Any]]

list of kwargs for write_sparameters_grating_coupler.

required
kwargs

simulation settings.

{}
Source code in gplugins/tidy3d/write_sparameters_grating_coupler.py
def write_sparameters_grating_coupler_batch(
    jobs: list[dict[str, Any]], **kwargs
) -> list[Awaitable[Sparameters]]:
    """Returns Sparameters for a list of write_sparameters.

    Each job runs in separate thread and is non blocking.
    You need to get the results using sp.result().

    Args:
        jobs: list of kwargs for write_sparameters_grating_coupler.
        kwargs: simulation settings.
    """
    kwargs.update(verbose=False)
    return [
        _executor.submit(write_sparameters_grating_coupler, **job, **kwargs)
        for job in jobs
    ]

FDTD lumerical

gplugins.lumerical.write_sparameters_lumerical

Write Sparameters with Lumerical FDTD.

set_material(session, structure, material)

Sets the material of a structure.

Parameters:

Name Type Description Default
session

lumerical session.

required
structure str

name of the lumerical structure.

required
material MaterialSpec

material spec, can be a string from lumerical database materials. a float or int, representing refractive index. a complex for n, k materials.

required
Source code in gplugins/lumerical/write_sparameters_lumerical.py
def set_material(session, structure: str, material: MaterialSpec) -> None:
    """Sets the material of a structure.

    Args:
        session: lumerical session.
        structure: name of the lumerical structure.
        material: material spec, can be
            a string from lumerical database materials.
            a float or int, representing refractive index.
            a complex for n, k materials.

    """
    if isinstance(material, str):
        session.setnamed(structure, "material", material)
    elif isinstance(material, int | float):
        session.setnamed(structure, "index", material)
    elif isinstance(material, complex):
        mat = session.addmaterial("(n,k) Material")
        session.setmaterial(mat, "Refractive Index", material.real)
        session.setmaterial(mat, "Imaginary Refractive Index", material.imag)
        session.setnamed(structure, "material", mat)
    elif isinstance(material, tuple | list):
        if len(material) != 2:
            raise ValueError(
                "Complex material requires a tuple or list of two numbers "
                f"(real, imag). Got {material} "
            )
        real, imag = material
        mat = session.addmaterial("(n,k) Material")
        session.setmaterial(mat, "Refractive Index", real)
        session.setmaterial(mat, "Imaginary Refractive Index", imag)
        session.setnamed(structure, "material", mat)
    else:
        raise ValueError(
            f"{material!r} needs to be a float refractive index, a complex number or tuple "
            "or a string from lumerical's material database"
        )

write_sparameters_lumerical(component, session=None, run=True, overwrite=False, dirpath=None, layer_stack=None, simulation_settings=SIMULATION_SETTINGS_LUMERICAL_FDTD, material_name_to_lumerical=None, delete_fsp_files=True, xmargin=0, ymargin=3, xmargin_left=None, xmargin_right=None, ymargin_top=None, ymargin_bot=None, zmargin=1.0, exclude_layers=None, **settings)

Returns and writes component Sparameters using Lumerical FDTD.

If simulation exists it returns the Sparameters directly unless overwrite=True which forces a re-run of the simulation

Writes Sparameters both in .npz and .DAT (interconnect format) as well as simulation settings in .YAML

In the npz format you can see S12m where m stands for magnitude and S12a where a stands for angle in radians

Your components need to have ports, that will extend over the PML.

.. image:: https://i.imgur.com/dHAzZRw.png

For your Fab technology you can overwrite

  • simulation_settings
  • dirpath
  • layerStack

converts gdsfactory units (um) to Lumerical units (m)

Disclaimer: This function tries to extract Sparameters automatically is hard to make a function that will fit all your possible simulation settings. You can use this function for inspiration to create your own.

Parameters:

Name Type Description Default
component ComponentSpec

Component to simulate.

required
session object | None

you can pass a session=lumapi.FDTD() or it will create one.

None
run bool

True runs Lumerical, False only draws simulation.

True
overwrite bool

run even if simulation results already exists.

False
dirpath PathType | None

directory to store sparameters in npz. Defaults to active Pdk.sparameters_path.

None
layer_stack LayerStack | None

contains layer to thickness, zmin and material. Defaults to active pdk.layer_stack.

None
simulation_settings SimulationSettingsLumericalFdtd

dataclass with all simulation_settings.

SIMULATION_SETTINGS_LUMERICAL_FDTD
material_name_to_lumerical dict[str, MaterialSpec] | None

alias to lumerical material's database name or refractive index. translate material name in LayerStack to lumerical's database name.

None
delete_fsp_files bool

deletes lumerical fsp files after simulation.

True
xmargin float

left/right distance from component to PML.

0
xmargin_left float | None

left distance from component to PML.

None
xmargin_right float | None

right distance from component to PML.

None
ymargin float

left/right distance from component to PML.

3
ymargin_top float | None

top distance from component to PML.

None
ymargin_bot float | None

bottom distance from component to PML.

None
zmargin float

thickness for cladding above and below core.

1.0
exclude_layers list[int] | None

list of layer indices to exclude from simulation.

None
settings

additional simulation settings to overwrite

{}

Other Parameters:

Name Type Description
background_material

for the background.

port_margin

on both sides of the port width (um).

port_height

port height (um).

port_extension

port extension (um).

mesh_accuracy

2 (1: coarse, 2: fine, 3: superfine).

wavelength_start

1.2 (um).

wavelength_stop

1.6 (um).

wavelength_points

500.

simulation_time

(s) related to max path length 3e8/2.410e-121e6 = 1.25mm.

simulation_temperature

in kelvin (default = 300).

frequency_dependent_profile

computes mode profiles for different wavelengths.

field_profile_samples

number of wavelengths to compute field profile.

.. code::

 top view
      ________________________________
     |                               |
     | xmargin                       | port_extension
     |<------>          port_margin ||<-->
  o2_|___________          _________||_o3
     |           \        /          |
     |            \      /           |
     |             ======            |
     |            /      \           |
  o1_|___________/        \__________|_o4
     |   |                           |
     |   |ymargin                    |
     |   |                           |
     |___|___________________________|

side view
      ________________________________
     |                               |
     |                               |
     |                               |
     |ymargin                        |
     |<---> _____         _____      |
     |     |     |       |     |     |
     |     |     |       |     |     |
     |     |_____|       |_____|     |
     |       |                       |
     |       |                       |
     |       |zmargin                |
     |       |                       |
     |_______|_______________________|
Return

Sparameters np.ndarray (wavelengths, o1@0,o1@0, o1@0,o2@0 ...) suffix a for angle in radians and m for module.

Source code in gplugins/lumerical/write_sparameters_lumerical.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
def write_sparameters_lumerical(
    component: ComponentSpec,
    session: object | None = None,
    run: bool = True,
    overwrite: bool = False,
    dirpath: PathType | None = None,
    layer_stack: LayerStack | None = None,
    simulation_settings: SimulationSettingsLumericalFdtd = SIMULATION_SETTINGS_LUMERICAL_FDTD,
    material_name_to_lumerical: dict[str, MaterialSpec] | None = None,
    delete_fsp_files: bool = True,
    xmargin: float = 0,
    ymargin: float = 3,
    xmargin_left: float | None = None,
    xmargin_right: float | None = None,
    ymargin_top: float | None = None,
    ymargin_bot: float | None = None,
    zmargin: float = 1.0,
    exclude_layers: list[int] | None = None,
    **settings,
) -> np.ndarray:
    r"""Returns and writes component Sparameters using Lumerical FDTD.

    If simulation exists it returns the Sparameters directly unless overwrite=True
    which forces a re-run of the simulation

    Writes Sparameters both in .npz and .DAT (interconnect format) as well as
    simulation settings in .YAML

    In the npz format you can see `S12m` where `m` stands for magnitude
    and `S12a` where `a` stands for angle in radians

    Your components need to have ports, that will extend over the PML.

    .. image:: https://i.imgur.com/dHAzZRw.png

    For your Fab technology you can overwrite

    - simulation_settings
    - dirpath
    - layerStack

    converts gdsfactory units (um) to Lumerical units (m)

    Disclaimer: This function tries to extract Sparameters automatically
    is hard to make a function that will fit all your possible simulation settings.
    You can use this function for inspiration to create your own.

    Args:
        component: Component to simulate.
        session: you can pass a session=lumapi.FDTD() or it will create one.
        run: True runs Lumerical, False only draws simulation.
        overwrite: run even if simulation results already exists.
        dirpath: directory to store sparameters in npz.
            Defaults to active Pdk.sparameters_path.
        layer_stack: contains layer to thickness, zmin and material.
            Defaults to active pdk.layer_stack.
        simulation_settings: dataclass with all simulation_settings.
        material_name_to_lumerical: alias to lumerical material's database name
            or refractive index.
            translate material name in LayerStack to lumerical's database name.
        delete_fsp_files: deletes lumerical fsp files after simulation.
        xmargin: left/right distance from component to PML.
        xmargin_left: left distance from component to PML.
        xmargin_right: right distance from component to PML.
        ymargin: left/right distance from component to PML.
        ymargin_top: top distance from component to PML.
        ymargin_bot: bottom distance from component to PML.
        zmargin: thickness for cladding above and below core.
        exclude_layers: list of layer indices to exclude from simulation.
        settings: additional simulation settings to overwrite

    Keyword Args:
        background_material: for the background.
        port_margin: on both sides of the port width (um).
        port_height: port height (um).
        port_extension: port extension (um).
        mesh_accuracy: 2 (1: coarse, 2: fine, 3: superfine).
        wavelength_start: 1.2 (um).
        wavelength_stop: 1.6 (um).
        wavelength_points: 500.
        simulation_time: (s) related to max path length 3e8/2.4*10e-12*1e6 = 1.25mm.
        simulation_temperature: in kelvin (default = 300).
        frequency_dependent_profile: computes mode profiles for different wavelengths.
        field_profile_samples: number of wavelengths to compute field profile.


    .. code::

         top view
              ________________________________
             |                               |
             | xmargin                       | port_extension
             |<------>          port_margin ||<-->
          o2_|___________          _________||_o3
             |           \        /          |
             |            \      /           |
             |             ======            |
             |            /      \           |
          o1_|___________/        \__________|_o4
             |   |                           |
             |   |ymargin                    |
             |   |                           |
             |___|___________________________|

        side view
              ________________________________
             |                               |
             |                               |
             |                               |
             |ymargin                        |
             |<---> _____         _____      |
             |     |     |       |     |     |
             |     |     |       |     |     |
             |     |_____|       |_____|     |
             |       |                       |
             |       |                       |
             |       |zmargin                |
             |       |                       |
             |_______|_______________________|



    Return:
        Sparameters np.ndarray (wavelengths, o1@0,o1@0, o1@0,o2@0 ...)
            suffix `a` for angle in radians and `m` for module.

    """
    layer_stack = layer_stack or get_layer_stack()
    component = component
    sim_settings = dict(simulation_settings)

    xmargin_left = xmargin_left or xmargin
    xmargin_right = xmargin_right or xmargin
    ymargin_top = ymargin_top or ymargin
    ymargin_bot = ymargin_bot or ymargin

    layer_to_thickness = layer_stack.get_layer_to_thickness()
    layer_to_zmin = layer_stack.get_layer_to_zmin()
    layer_to_material = layer_stack.get_layer_to_material()

    if hasattr(component.info, "simulation_settings"):
        sim_settings |= component.info.simulation_settings
        logger.info(
            f"Updating {component.name!r} sim settings {component.simulation_settings}"
        )
    for setting in settings:
        if setting not in sim_settings:
            raise ValueError(
                f"Invalid setting {setting!r} not in ({list(sim_settings.keys())})"
            )

    sim_settings.update(**settings)
    ss = SimulationSettingsLumericalFdtd(**sim_settings)

    component_with_booleans = layer_stack.get_component_with_derived_layers(component)
    component_with_padding = gf.add_padding_container(
        component_with_booleans,
        default=0,
        top=ymargin_top,
        bottom=ymargin_bot,
        left=xmargin_left,
        right=xmargin_right,
    )

    component_extended = gf.components.extend_ports(
        component_with_padding, length=ss.distance_monitors_to_pml
    )

    ports = component.ports.filter(port_type="optical")
    if not ports:
        raise ValueError(f"{component.name!r} does not have any optical ports")

    component_extended_beyond_pml = gf.components.extension.extend_ports(
        component=component_extended, length=ss.port_extension
    )
    component_extended_beyond_pml = component_extended_beyond_pml.copy()
    component_extended_beyond_pml.flatten()
    component_extended_beyond_pml.name = "top"

    gdspath = component_extended_beyond_pml.write_gds()

    filepath_npz = get_sparameters_path(
        component=component,
        dirpath=dirpath,
        layer_stack=layer_stack,
        **settings,
    )
    filepath = filepath_npz.with_suffix(".dat")
    filepath_sim_settings = filepath.with_suffix(".yml")
    filepath_fsp = filepath.with_suffix(".fsp")
    fspdir = filepath.parent / f"{filepath.stem}_s-parametersweep"

    if run and filepath_npz.exists() and not overwrite:
        logger.info(f"Reading Sparameters from {filepath_npz.absolute()!r}")
        return np.load(filepath_npz)

    if not run and session is None:
        print(run_false_warning)

    logger.info(f"Writing Sparameters to {filepath_npz.absolute()!r}")
    x_min = (component_extended.xmin - xmargin) * 1e-6
    x_max = (component_extended.xmax + xmargin) * 1e-6
    y_min = (component_extended.ymin - ymargin) * 1e-6
    y_max = (component_extended.ymax + ymargin) * 1e-6

    index_to_thickness = {}
    index_to_zmin = {}
    for level in layer_stack.layers.values():
        layer = level.layer

        if isinstance(layer, LogicalLayer):
            assert isinstance(layer.layer, tuple | LayerEnum)
            layer_tuple = cast(tuple[int, int], tuple(layer.layer))
        elif isinstance(layer, DerivedLayer):
            assert level.derived_layer is not None
            assert isinstance(level.derived_layer.layer, tuple | LayerEnum)
            layer_tuple = cast(tuple[int, int], tuple(level.derived_layer.layer))
        elif isinstance(layer, tuple):
            # Handle plain tuple layers directly
            layer_tuple = cast(tuple[int, int], layer)
        else:
            raise ValueError(
                f"Layer {layer!r} is not a DerivedLayer, LogicalLayer, or tuple"
            )

        layer_index = int(gf.get_layer(layer_tuple))

        index_to_thickness[layer_index] = level.thickness
        index_to_zmin[layer_index] = level.zmin

    layers_thickness = [
        index_to_thickness[gf.get_layer(layer)]
        for layer in component_with_booleans.layers
        if gf.get_layer(layer) in index_to_thickness
    ]
    if not layers_thickness:
        raise ValueError(
            f"no layers for component {component.layers}in layer stack {layer_stack}"
        )
    layers_zmin = [
        index_to_zmin[gf.get_layer(layer)]
        for layer in component_with_booleans.layers
        if gf.get_layer(layer) in index_to_zmin
    ]
    component_thickness = max(layers_thickness)
    component_zmin = min(layers_zmin)

    z = (component_zmin + component_thickness) / 2 * 1e-6
    z_span = (2 * zmargin + component_thickness) * 1e-6

    x_span = x_max - x_min
    y_span = y_max - y_min

    sim_settings.update(dict(layer_stack=layer_stack.to_dict()))

    sim_settings = dict(
        simulation_settings=sim_settings,
        component=component.to_dict(),
        version=__version__,
    )

    logger.info(
        f"Simulation size = {x_span * 1e6:.3f}, {y_span * 1e6:.3f}, {z_span * 1e6:.3f} um"
    )

    # from pprint import pprint
    # filepath_sim_settings.write_text(yaml.dump(sim_settings))
    # print(filepath_sim_settings)
    # pprint(sim_settings)
    # return

    try:
        import lumapi
    except ModuleNotFoundError as e:
        print(
            "Cannot import lumapi (Python Lumerical API). "
            "You can add set the PYTHONPATH variable or add it with `sys.path.append()`"
        )
        raise e
    except OSError as e:
        raise e

    start = time.time()
    s = session or lumapi.FDTD(hide=False)
    s.newproject()
    s.selectall()
    s.deleteall()
    s.addrect(
        x_min=x_min,
        x_max=x_max,
        y_min=y_min,
        y_max=y_max,
        z=z,
        z_span=z_span,
        index=1.5,
        name="clad",
    )

    # Set cladding opacity
    s.setnamed("clad", "alpha", 0.1)

    material_name_to_lumerical_new = material_name_to_lumerical or {}
    material_name_to_lumerical = ss.material_name_to_lumerical.copy()
    material_name_to_lumerical.update(**material_name_to_lumerical_new)

    material = material_name_to_lumerical[ss.background_material]
    set_material(session=s, structure="clad", material=material)

    s.addfdtd(
        dimension="3D",
        x_min=x_min,
        x_max=x_max,
        y_min=y_min,
        y_max=y_max,
        z=z,
        z_span=z_span,
        mesh_accuracy=ss.mesh_accuracy,
        use_early_shutoff=True,
        simulation_time=ss.simulation_time,
        simulation_temperature=ss.simulation_temperature,
    )

    exclude_layers = exclude_layers or []
    polygons_per_layer = component_extended_beyond_pml.get_polygons_points(merge=True)

    for level in layer_stack.layers.values():
        layer = level.layer

        if isinstance(layer, LogicalLayer):
            layer_tuple = gf.get_layer_tuple(layer.layer)
        elif isinstance(layer, DerivedLayer):
            layer_tuple = gf.get_layer_tuple(level.derived_layer.layer)
        elif isinstance(layer, tuple):
            # Handle plain tuple layers directly
            layer_tuple = layer
        else:
            raise ValueError(
                f"Layer {layer!r} is not a DerivedLayer, LogicalLayer, or tuple"
            )

        layer_index = int(gf.get_layer(layer_tuple))

        if layer_index in exclude_layers:
            continue

        if layer_index not in polygons_per_layer:
            continue

        zmin = level.zmin

        if zmin is not None:
            thickness = level.thickness
            material_name = layer_to_material[layer]
            if material_name not in material_name_to_lumerical:
                raise ValueError(
                    f"{material_name!r} not in {list(material_name_to_lumerical.keys())}"
                )
            material = material_name_to_lumerical[material_name]

            if layer not in layer_to_zmin:
                raise ValueError(f"{layer} not in {list(layer_to_zmin.keys())}")

            zmin = layer_to_zmin[layer]
            zmax = zmin + thickness
            z = (zmax + zmin) / 2

            s.gdsimport(str(gdspath), "top", f"{layer_tuple[0]}:{layer_tuple[1]}")
            layername = f"GDS_LAYER_{layer_tuple[0]}:{layer_tuple[1]}"
            s.setnamed(layername, "z", z * 1e-6)
            s.setnamed(layername, "z span", thickness * 1e-6)
            set_material(session=s, structure=layername, material=material)
            logger.info(
                f"adding {layer_tuple}, thickness = {thickness} um, zmin = {zmin} um "
            )

    for i, port in enumerate(ports):
        port_layer_index = gf.get_layer(port.layer)
        zmin = index_to_zmin[port_layer_index]
        thickness = index_to_thickness[port_layer_index]
        z = (zmin + thickness) / 2
        zspan = 2 * ss.port_margin + thickness

        s.addport()
        p = f"FDTD::ports::port {i + 1}"
        s.setnamed(p, "x", port.x * 1e-6)
        s.setnamed(p, "y", port.y * 1e-6)
        s.setnamed(p, "z", z * 1e-6)
        s.setnamed(p, "z span", zspan * 1e-6)
        s.setnamed(p, "frequency dependent profile", ss.frequency_dependent_profile)
        s.setnamed(p, "number of field profile samples", ss.field_profile_samples)

        deg = int(port.orientation)
        # if port.orientation not in [0, 90, 180, 270]:
        #     raise ValueError(f"{port.orientation} needs to be [0, 90, 180, 270]")

        if -45 <= deg <= 45:
            direction = "Backward"
            injection_axis = "x-axis"
            dxp = 0
            dyp = 2 * ss.port_margin + port.dwidth
        elif 45 < deg < 90 + 45:
            direction = "Backward"
            injection_axis = "y-axis"
            dxp = 2 * ss.port_margin + port.dwidth
            dyp = 0
        elif 90 + 45 < deg < 180 + 45:
            direction = "Forward"
            injection_axis = "x-axis"
            dxp = 0
            dyp = 2 * ss.port_margin + port.dwidth
        elif 180 + 45 < deg < 180 + 45 + 90:
            direction = "Forward"
            injection_axis = "y-axis"
            dxp = 2 * ss.port_margin + port.dwidth
            dyp = 0

        else:
            raise ValueError(
                f"port {port.name!r} orientation {port.orientation} is not valid"
            )

        s.setnamed(p, "direction", direction)
        s.setnamed(p, "injection axis", injection_axis)
        s.setnamed(p, "y span", dyp * 1e-6)
        s.setnamed(p, "x span", dxp * 1e-6)
        # s.setnamed(p, "theta", deg)
        s.setnamed(p, "name", port.name)
        # s.setnamed(p, "name", f"o{i+1}")

        logger.info(
            f"port {p} {port.name!r}: at ({port.x}, {port.y}, 0)"
            f"size = ({dxp}, {dyp}, {zspan})"
        )

    s.setglobalsource("wavelength start", ss.wavelength_start * 1e-6)
    s.setglobalsource("wavelength stop", ss.wavelength_stop * 1e-6)
    s.setnamed("FDTD::ports", "monitor frequency points", ss.wavelength_points)

    if run:
        s.save(str(filepath_fsp))
        s.deletesweep("s-parameter sweep")

        s.addsweep(3)
        s.setsweep("s-parameter sweep", "Excite all ports", 0)
        s.setsweep("S sweep", "auto symmetry", True)
        s.runsweep("s-parameter sweep")
        sp = s.getsweepresult("s-parameter sweep", "S parameters")
        s.exportsweep("s-parameter sweep", str(filepath))
        logger.info(f"wrote sparameters to {str(filepath)!r}")

        sp["wavelengths"] = sp.pop("lambda").flatten() * 1e6
        np.savez_compressed(filepath, **sp)

        # keys = [key for key in sp.keys() if key.startswith("S")]
        # ra = {
        #     f"{key.lower()}a": list(np.unwrap(np.angle(sp[key].flatten())))
        #     for key in keys
        # }
        # rm = {f"{key.lower()}m": list(np.abs(sp[key].flatten())) for key in keys}
        # results = {"wavelengths": wavelengths}
        # results.update(ra)
        # results.update(rm)
        # df = pd.DataFrame(results, index=wavelengths)
        # df.to_csv(filepath_npz, index=False)

        end = time.time()
        sim_settings.update(compute_time_seconds=end - start)
        sim_settings.update(compute_time_minutes=(end - start) / 60)
        filepath_sim_settings.write_text(yaml.dump(sim_settings))
        if delete_fsp_files and fspdir.exists():
            shutil.rmtree(fspdir)
            logger.info(
                f"deleting simulation files in {str(fspdir)!r}. "
                "To keep them, use delete_fsp_files=False flag"
            )

        return sp

    filepath_sim_settings.write_text(yaml.dump(sim_settings))
    return s

Circuit Solver

SAX

gplugins.sax.read.model_from_csv(filepath, xkey='wavelengths', xunits=1, prefix='s')

Returns a SAX Sparameters Model from a CSV file.

The SAX Model is a function that returns a SAX SDict interpolated over wavelength.

Parameters:

Name Type Description Default
filepath PathType | DataFrame

CSV Sparameters path or pandas DataFrame.

required
xkey str

key for wavelengths in file.

'wavelengths'
xunits float

x units in um from the loaded file (um). 1 means 1um.

1
prefix str

for the sparameters column names in file.

's'
Source code in gplugins/sax/read.py
def model_from_csv(
    filepath: PathType | pd.DataFrame,
    xkey: str = "wavelengths",
    xunits: float = 1,
    prefix: str = "s",
) -> Model:
    """Returns a SAX Sparameters Model from a CSV file.

    The SAX Model is a function that returns a SAX SDict interpolated over wavelength.

    Args:
        filepath: CSV Sparameters path or pandas DataFrame.
        xkey: key for wavelengths in file.
        xunits: x units in um from the loaded file (um). 1 means 1um.
        prefix: for the sparameters column names in file.
    """
    df = filepath if isinstance(filepath, pd.DataFrame) else pd.read_csv(filepath)
    assert isinstance(df, pd.DataFrame)
    df = df.reset_index()  # maybe there is useful info in the index...
    dic = dict(zip(df.columns, jnp.asarray(df.values.T)))
    keys = list(dic.keys())

    if xkey not in keys:
        raise ValueError(f"{xkey!r} not in {keys}")

    nsparameters = (len(keys) - 1) // 2
    nports = int(nsparameters**0.5)

    x = jnp.asarray(dic[xkey] * xunits)
    wl = jnp.asarray(wl_cband)

    # make sure x is sorted from low to high
    idxs = jnp.argsort(x)
    x = x[idxs]
    dic = {k: v[idxs] for k, v in dic.items()}

    @jax.jit
    def model(wl: Float = wl):
        S = {}
        zero = jnp.zeros_like(x)
        for i in range(1, nports + 1):
            for j in range(1, nports + 1):
                m = jnp.interp(wl, x, dic.get(f"{prefix}{i}{j}m", zero))
                a = jnp.interp(wl, x, dic.get(f"{prefix}{i}{j}a", zero))
                S[f"o{i}", f"o{j}"] = m * jnp.exp(1j * a)

        return S

    return model

gplugins.sax.read.model_from_component(component, simulator, **kwargs)

Returns SAX model from lumerical FDTD simulations.

Parameters:

Name Type Description Default
component

to simulate.

required
simulator Simulator

meep, lumerical or tidy3d.

required
kwargs

simulator settings.

{}
Source code in gplugins/sax/read.py
def model_from_component(component, simulator: Simulator, **kwargs) -> Model:
    """Returns SAX model from lumerical FDTD simulations.

    Args:
        component: to simulate.
        simulator: meep, lumerical or tidy3d.
        kwargs: simulator settings.

    """
    simulators = ["lumerical", "meep", "tidy3d"]

    if simulator == "lumerical":
        filepath = get_sparameters_path_lumerical(component=component, **kwargs)
    elif simulator == "meep":
        filepath = get_sparameters_path_meep(component=component, **kwargs)
    elif simulator == "tidy3d":
        filepath = get_sparameters_path_tidy3d(component=component, **kwargs)
    else:
        raise ValueError(f"{simulator!r} no in {simulators}")
    return model_from_csv(filepath=filepath)

gplugins.sax.plot_model

Useful plot functions.

plot_model(model, port1='o1', ports2=None, logscale=True, min_db_range=0.5, fig=None, wavelength_start=1.5, wavelength_stop=1.6, wavelength_points=2000, phase=False, title=None)

Plot Model Sparameters Magnitude.

Parameters:

Name Type Description Default
model Model

function that returns SDict as function of wavelength.

required
port1 str

input port name.

'o1'
ports2 tuple[str, ...] | None

list of ports.

None
logscale bool

plots in dB logarithmic scale.

True
min_db_range float

minimum dB range. Set to 0 to disable.

0.5
fig

matplotlib figure.

None
wavelength_start float

wavelength min (µm).

1.5
wavelength_stop float

wavelength max (µm).

1.6
wavelength_points int

number of wavelength steps.

2000
phase bool

plot phase instead of magnitude.

False
title str | None

plot title.

None

.. plot:: :include-source:

import gplugins.sax as gs

gs.plot_model(gs.models.straight, phase=True, port1="o1")
Source code in gplugins/sax/plot_model.py
@validate_call
def plot_model(
    model: Model,
    port1: str = "o1",
    ports2: tuple[str, ...] | None = None,
    logscale: bool = True,
    min_db_range: float = 0.5,
    fig=None,
    wavelength_start: float = 1.5,
    wavelength_stop: float = 1.6,
    wavelength_points: int = 2000,
    phase: bool = False,
    title: str | None = None,
) -> None:
    """Plot Model Sparameters Magnitude.

    Args:
        model: function that returns SDict as function of wavelength.
        port1: input port name.
        ports2: list of ports.
        logscale: plots in dB logarithmic scale.
        min_db_range: minimum dB range. Set to 0 to disable.
        fig: matplotlib figure.
        wavelength_start: wavelength min (µm).
        wavelength_stop: wavelength max (µm).
        wavelength_points: number of wavelength steps.
        phase: plot phase instead of magnitude.
        title: plot title.

    .. plot::
        :include-source:

        import gplugins.sax as gs

        gs.plot_model(gs.models.straight, phase=True, port1="o1")

    """
    wavelengths = np.linspace(wavelength_start, wavelength_stop, wavelength_points)
    sdict = model(wl=wavelengths)

    ports = {ports[0] for ports in sdict.keys()}
    ports2 = ports2 or ports

    if port1 not in ports:
        raise ValueError(f"port1 {port1!r} not in {list(ports)}")

    for port in ports2:
        if port not in ports:
            raise ValueError(f"port2 {port!r} not in {list(ports)}")

    fig = fig or plt.subplot()
    ax = fig.axes

    for port2 in ports2:
        if (port1, port2) in sdict:
            if phase:
                y = np.angle(sdict[(port1, port2)])
                ylabel = "angle (rad)"
            else:
                y = np.abs(sdict[(port1, port2)])
                y = 20 * np.log10(y) if logscale else y
                ylabel = "|S (dB)|" if logscale else "|S|"
            ax.plot(wavelengths, y, label=f"{port1}→{port2}")

    if logscale:
        current_ylim = ax.get_ylim()
        if current_ylim[1] - current_ylim[0] < min_db_range:
            ax.set_ylim(y.mean() - min_db_range / 2, y.mean() + min_db_range / 2)

    if title:
        ax.set_title(title)
    else:
        # Handle functools.partial objects
        if hasattr(model, "func"):
            # It's a partial object
            model_name = getattr(model.func, "__name__", "model")
        else:
            # Regular function
            model_name = getattr(model, "__name__", "model")
        ax.set_title(f"{model_name} S-Parameters")
    ax.set_xlabel("wavelength (µm)")
    ax.set_ylabel(ylabel)
    plt.legend()
    return ax

gplugins.sax.models

Electrostatics

Elmer

gplugins.elmer.run_capacitive_simulation_elmer(component, element_order=1, n_processes=1, layer_stack=None, material_spec=None, simulation_folder=None, simulator_params=None, mesh_parameters=None, mesh_file=None)

Run electrostatic finite element method simulations using Elmer_. Returns the field solution and resulting capacitance matrix.

.. note:: You should have ElmerGrid, ElmerSolver and ElmerSolver_mpi and in your PATH.

Parameters:

Name Type Description Default
component Component

Simulation environment as a gdsfactory component.

required
element_order int

Order of polynomial basis functions. Higher is more accurate but takes more memory and time to run.

1
n_processes int

Number of processes to use for parallelization

1
layer_stack LayerStack | None

:class:~LayerStack defining defining what layers to include in the simulation and the material properties and thicknesses.

None
material_spec RFMaterialSpec | None

:class:~RFMaterialSpec defining material parameters for the ones used in layer_stack.

None
simulation_folder Path | str | None

Directory for storing the simulation results. Default is a temporary directory.

None
simulator_params Mapping[str, Any] | None

Elmer-specific parameters. See template file for more details.

None
mesh_parameters dict[str, Any] | None

Keyword arguments to provide to :func:get_mesh.

None
mesh_file Path | str | None

Path to a ready mesh to use. Useful for reusing one mesh file. By default a mesh is generated according to mesh_parameters.

None

.. _Elmer: https://github.com/ElmerCSC/elmerfem

Source code in gplugins/elmer/get_capacitance.py
def run_capacitive_simulation_elmer(
    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
    `Elmer`_.     Returns the field solution and resulting capacitance matrix.

    .. note:: You should have `ElmerGrid`, `ElmerSolver` and `ElmerSolver_mpi` and 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: Elmer-specific parameters. See template file for more details.
        mesh_parameters: Keyword arguments to provide to :func:`get_mesh`.
        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
    """
    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)

    port_delimiter = "__"  # won't cause trouble unlike #
    filename = component.name + ".msh"
    if mesh_file:
        shutil.copyfile(str(mesh_file), str(simulation_folder / filename))
    else:
        prisms = get_meshwell_prisms(
            component=component,
            layer_stack=layer_stack,
        )
        cad(
            entities_list=prisms,
            output_file=(
                cad_output := (simulation_folder / filename).with_suffix(".xao")
            ),
            boundary_delimiter=(boundary_delimiter:="boundary"),
            progress_bars=True,
        )
        mesh(
            input_file=cad_output,
            output_file=(simulation_folder / filename).with_suffix(".msh"),
            boundary_delimiter=boundary_delimiter,
            dim=3,
            **(mesh_parameters or {}),
        )

    # `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)
    ]
    gmsh.finalize()

    # Signals are converted to Elmer Boundary Conditions
    ground_layers = {
        next(k for k, v in layer_stack.layers.items() if v.layer == port.layer)
        for port in component.ports
    }  # ports allowed only on metal
    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}}

    _generate_sif(
        simulation_folder,
        component.name,
        metal_signal_surfaces_grouped,
        bodies,
        ground_layers,
        layer_stack,
        material_spec,
        element_order,
        background_tag,
        simulator_params,
    )
    _elmergrid(simulation_folder, filename, n_processes)
    _elmersolver(simulation_folder, filename, n_processes)
    results = _read_elmer_results(
        simulation_folder,
        filename,
        n_processes,
        component.ports,
        is_temporary=str(simulation_folder) == temp_dir.name,
    )
    temp_dir.cleanup()
    return results

Palace

gplugins.palace.run_capacitive_simulation_palace(component, n_processes=1, layer_stack=None, material_spec=None, simulation_folder=None, solver_config=None, mesh_parameters=None, mesh_file=None)

Run electrostatic finite element method simulations using Palace_. Returns the field solution and resulting capacitance matrix.

.. note:: You should have palace in your PATH.

Parameters:

Name Type Description Default
component Component

Simulation environment as a gdsfactory component.

required
n_processes int

Number of processes to use for parallelization

1
layer_stack LayerStack | None

:class:~LayerStack defining defining what layers to include in the simulation and the material properties and thicknesses.

None
material_spec RFMaterialSpec | None

:class:~RFMaterialSpec defining material parameters for the ones used in layer_stack.

None
simulation_folder Path | str | None

Directory for storing the simulation results. Default is a temporary directory.

None
solver_config Mapping[str, Any] | None

Palace-specific parameters. This will be expanded to config["Solver"] in the Palace config, see Palace documentation <https://awslabs.github.io/palace/stable/config/reference/>_

None
mesh_parameters dict[str, Any] | None

Keyword arguments to provide to :func:~meshwell.mesh.mesh.

None
mesh_file Path | str | None

Path to a ready mesh to use. Useful for reusing one mesh file. By default a mesh is generated according to mesh_parameters.

None

.. _Palace: https://github.com/awslabs/palace

Source code in gplugins/palace/get_capacitance.py
def run_capacitive_simulation_palace(
    component: gf.Component,
    n_processes: int = 1,
    layer_stack: LayerStack | None = None,
    material_spec: RFMaterialSpec | None = None,
    simulation_folder: Path | str | None = None,
    solver_config: 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.
        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.
        solver_config: Palace-specific parameters. This will be expanded to ``config["Solver"]`` in
            the Palace config, see `Palace documentation <https://awslabs.github.io/palace/stable/config/reference/>`_
        mesh_parameters:
            Keyword arguments to provide to :func:`~meshwell.mesh.mesh`.
        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 not isinstance(n_processes, int):
        raise TypeError(f"n_processes must be an integer, got {type(n_processes)}")
    if n_processes < 1:
        raise ValueError(f"n_processes must be >= 1, got {n_processes}")

    if solver_config:
        order = solver_config.get("Order")
        if order is not None:
            if not isinstance(order, int):
                raise TypeError(f"Solver Order must be an integer, got {type(order)}")
            if order < 1:
                raise ValueError(f"Solver Order must be >= 1, got {order}")

    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)

    port_delimiter = "@"  # won't cause trouble unlike #
    boundary_delimiter = "boundary"
    if mesh_file:
        shutil.copyfile(str(mesh_file), str(simulation_folder / filename))
        filename = component.name + ".msh"
    else:
        # Generate a version of the component where layers are split according to ports they touch
        if component.ports:
            mesh_component = component.dup()
            mesh_component.flatten()
            component = get_component_with_net_layers(
                component=mesh_component,
                layer_stack=layer_stack,
                port_names=[p.name for p in component.ports],
                delimiter="@",
            )
        filename = component.name + ".msh"

        prisms = get_meshwell_prisms(
            component=component,
            layer_stack=layer_stack,
        )
        cad(
            entities_list=prisms,
            output_file=(
                cad_output := (simulation_folder / filename).with_suffix(".xao")
            ),
            boundary_delimiter=boundary_delimiter,
            progress_bars=True,
        )
        mesh(
            input_file=cad_output,
            output_file=(simulation_folder / filename).with_suffix(".msh"),
            boundary_delimiter=boundary_delimiter,
            dim=3,
            **(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 = {
        k
        for port in component.ports
        for k, v in layer_stack.layers.items()
        if compare_layerlevel_and_port_layers(v, port)
        # and v.derived_layer is not None
    }
    # ports allowed only on metal

    metal_surfaces = [
        e for e in mesh_surface_entities if any(ground in e for ground in ground_layers)
    ]
    # Group signal BCs by ports
    # TODO we need to remove the port-boundary surfaces for palace to work, why?
    # TODO might as well remove the vacuum boundary and have just 2D sheets
    metal_signal_surfaces_grouped = [
        [e for e in metal_surfaces if port.name in e and boundary_delimiter not 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

    # breakpoint()

    # 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()
    }
    # Use msh version 2.2 for MFEM / Palace compatibility, see https://mfem.org/mesh-formats/#gmsh-mesh-formats
    gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
    gmsh.write(str(simulation_folder / filename))
    gmsh.finalize()

    _generate_json(
        simulation_folder,
        component.name,
        metal_signal_surfaces_grouped,
        bodies,
        ground_layers,
        layer_stack,
        material_spec,
        physical_name_to_dimtag_map,
        background_tag,
        solver_config,
    )
    _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

Full-wave RF

Palace

gplugins.palace.run_scattering_simulation_palace(component, element_order=1, n_processes=1, layer_stack=None, material_spec=None, simulation_folder=None, simulator_params=None, driven_settings=None, mesh_refinement_levels=None, only_one_port=True, mesh_parameters=None, mesh_file=None)

Run full-wave finite element method simulations using Palace.

Returns the field solution and resulting scattering matrix.

.. note:: You should have palace in your PATH.

Parameters:

Name Type Description Default
component Component

Simulation environment as a gdsfactory component.

required
element_order int

Order of polynomial basis functions. Higher is more accurate but takes more memory and time to run.

1
n_processes int

Number of processes to use for parallelization

1
layer_stack LayerStack | None

:class:~LayerStack defining defining what layers to include in the simulation and the material properties and thicknesses.

None
material_spec RFMaterialSpec | None

:class:~RFMaterialSpec defining material parameters for the ones used in layer_stack.

None
simulation_folder Path | str | None

Directory for storing the simulation results. Default is a temporary directory.

None
simulator_params Mapping[str, Any] | None

Palace-specific parameters. This will be expanded to solver["Linear"] in the Palace config, see Palace documentation <https://awslabs.github.io/palace/stable/config/reference/>_

None
driven_settings Mapping[str, float | int | bool] | None

Driven full-wave parameters in Palace. This will be expanded to solver["Driven"] in the Palace config, see Palace documentation <https://awslabs.github.io/palace/stable/config/reference/>_

None
mesh_refinement_levels int | None

Refine mesh this many times, see Palace for details.

None
only_one_port bool | None

Whether to solve only scattering from the first port to other ports, e.g., S11, S12, S13, ...

True
mesh_parameters dict[str, Any] | None

Keyword arguments to provide to :func:get_mesh.

None
mesh_file Path | str | None

Path to a ready mesh to use. Useful for reusing one mesh file. By default a mesh is generated according to mesh_parameters.

None

.. _Palace https://github.com/awslabs/palace

Source code in gplugins/palace/get_scattering.py
def run_scattering_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,
    driven_settings: Mapping[str, float | int | bool] | None = None,
    mesh_refinement_levels: int | None = None,
    only_one_port: bool | None = True,
    mesh_parameters: dict[str, Any] | None = None,
    mesh_file: Path | str | None = None,
) -> DrivenFullWaveResults:
    """Run full-wave finite element method simulations using Palace.

    Returns the field solution and resulting scattering 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 <https://awslabs.github.io/palace/stable/config/reference/>`_
        driven_settings: Driven full-wave parameters in Palace. This will be expanded to ``solver["Driven"]`` in
            the Palace config, see `Palace documentation <https://awslabs.github.io/palace/stable/config/reference/>`_
        mesh_refinement_levels: Refine mesh this many times, see Palace for details.
        only_one_port: Whether to solve only scattering from the first port to other ports, e.g., `S11, S12, S13, ...`
        mesh_parameters:
            Keyword arguments to provide to :func:`get_mesh`.
        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"
    port_delimiter = "__"
    if mesh_file:
        shutil.copyfile(str(mesh_file), str(simulation_folder / filename))
    else:
        prisms = get_meshwell_prisms(
            component=component,
            type="3D",
            filename=simulation_folder / filename,
            layer_stack=layer_stack,
            n_threads=n_processes,
        )
        cad(
            entities_list=prisms,
            output_file=(
                cad_output := (simulation_folder / filename).with_suffix(".xao")
            ),
            boundary_delimiter=(boundary_delimiter:="boundary"),
            progress_bars=True,
        )
        mesh(
            input_file=cad_output,
            output_file=(simulation_folder / filename).with_suffix(".msh"),
            boundary_delimiter=boundary_delimiter,
            dim=3,
            **(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.option.setNumber("Mesh.MshFileVersion", 2.2)
    gmsh.merge(str(simulation_folder / filename))
    mesh_surface_entities = {
        gmsh.model.getPhysicalName(*dimtag)
        for dimtag in gmsh.model.getPhysicalGroups(dim=2)
    }
    background_tag = (mesh_parameters or {}).get("background_tag", "vacuum")

    # Signals are converted to Boundaries
    # TODO currently assumes signal layers have `bw`

    comp_layers_names = [gf.get_layer_name(layer) for layer in component.layers]

    ground_layers = {
        next(k for k, v in layer_stack.layers.items() if v.layer == LogicalLayer(layer=port.layer))
        for port in component.ports
    } | {layer for layer in comp_layers_names if "_bw" in layer}
    # 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 and lumped port pairs
    # TODO tuple pairs by o1_1 o1_2

    lumped_two_ports = [
        e for e in [port.name.split("_") for port in component.ports] if len(e) > 1
    ]
    lumped_two_port_pairs = [
        ("_".join(p1), "_".join(p2))
        for p1, p2 in itertools.combinations(lumped_two_ports, 2)
        if p1[0] == p2[0]
    ]
    metal_signal_surfaces_grouped = [
        [e for e in metal_surfaces if port.name in e and background_tag in e]
        for port in component.ports
    ]
    metal_signal_surfaces_paired = [
        tuple(
            e
            for e in metal_signal_surfaces_grouped
            if all(p1 in s or p2 in s for s in e)
        )
        for p1, p2 in lumped_two_port_pairs
    ]

    metal_ground_surfaces = set(metal_surfaces) - set(
        itertools.chain.from_iterable(metal_signal_surfaces_grouped)
    )

    ground_layers |= metal_ground_surfaces

    def _xy_plusminus_direction(point_1, point_2):
        # TODO update after https://github.com/awslabs/palace/pull/75 is merged
        delta_x = point_2[0] - point_1[0]
        delta_y = point_2[1] - point_1[1]
        angle = atan2(delta_y, delta_x)
        angle_deg = degrees(angle) + 360

        directions = ["+X", "+Y", "-X", "-Y"]
        index = round(angle_deg / 90) % 4  # TODO check if shift is correct

        return directions[index]

    lumped_two_port_directions = {
        ports[0]: _xy_plusminus_direction(
            *[port.center for port in component.get_ports_list()]
            # *[component.get_ports_dict()[port].center for port in ports]
        )
        for ports in itertools.chain(
            lumped_two_port_pairs, [tuple(reversed(e)) for e in lumped_two_port_pairs]
        )
    }

    # 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:
        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()
    }
    absorbing_surfaces = {
        k
        for k in physical_name_to_dimtag_map
        if "___None" in k
        and background_tag in k
        and all(p not in k for p in [port.name for port in component.ports])
    } - set(ground_layers)

    gmsh.finalize()

    jsons = _generate_json(
        simulation_folder,
        component.name,
        bodies,
        absorbing_surfaces,
        layer_stack,
        material_spec,
        element_order,
        physical_name_to_dimtag_map,
        metal_surfaces,
        background_tag,
        None,  # TODO edge
        metal_signal_surfaces_paired,  # internal
        lumped_two_port_directions,
        simulator_params,
        driven_settings,
        mesh_refinement_levels,
        only_one_port,
    )
    run_async_with_event_loop(_palace(simulation_folder, jsons, n_processes))
    results = _read_palace_results(
        simulation_folder,
        filename,
        [e[0] for e in lumped_two_port_pairs][: 1 if only_one_port else -1],
        is_temporary=str(simulation_folder) == temp_dir.name,
    )
    temp_dir.cleanup()
    return results