Skip to content

Api

MEOW: Modeling of Eigenmodes and Overlaps in Waveguides.

AxisDirection = Literal['x', 'y', 'z'] module-attribute

Axis direction: "x", "y", or "z".

BoolArray1D = Annotated[NDArray, Dim(1), DType('bool')] module-attribute

1D boolean numpy array.

BoolArray2D = Annotated[NDArray, Dim(2), DType('bool')] module-attribute

2D boolean numpy array.

Complex = Annotated[NDArray, Dim(0), DType('complex128')] module-attribute

0D (scalar) complex128 numpy array.

ComplexArray1D = Annotated[NDArray, Dim(1), DType('complex128')] module-attribute

1D complex128 numpy array.

ComplexArray2D = Annotated[NDArray, Dim(2), DType('complex128')] module-attribute

2D complex128 numpy array.

FloatArray1D = Annotated[NDArray, Dim(1), DType('float64')] module-attribute

1D float64 numpy array.

FloatArray2D = Annotated[NDArray, Dim(2), DType('float64')] module-attribute

2D float64 numpy array.

Geometry = Geometry2D | Geometry3D module-attribute

Any 2D or 3D geometry.

Geometry2D = Rectangle | Polygon2D module-attribute

A 2D geometry: either a Rectangle or a Polygon2D.

Geometry3D = Box | Prism module-attribute

A 3D geometry: either a Box or a Prism.

IntArray1D = Annotated[NDArray, Dim(1), DType('int64')] module-attribute

1D int64 numpy array.

IntArray2D = Annotated[NDArray, Dim(2), DType('int64')] module-attribute

2D int64 numpy array.

Material = IndexMaterial | SampledMaterial | TidyMaterial module-attribute

A material: IndexMaterial, SampledMaterial, or TidyMaterial.

Materials = list[Material] module-attribute

A list of Material objects.

Modes = list[Mode] module-attribute

A list of Mode objects.

NDArray = Annotated[np.ndarray, ArraySchema, PlainSerializer(_serialize_ndarray), BeforeValidator(_validate_ndarray), AfterValidator(_coerce_immutable)] module-attribute

A numpy ndarray with pydantic serialization support.

PassivityMethod = Literal['none', 'clip', 'invert', 'subtract'] module-attribute

Method for enforcing S-matrix passivity.

Sim = Any module-attribute

A lumerical simulation object.

BaseModel

Bases: BaseModel

Base model class for all models.

Source code in src/meow/base_model.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
class BaseModel(_BaseModel, metaclass=ModelMetaclass):
    """Base model class for all models."""

    type: str = Field(default="", validate_default=True)
    _cache: dict = PrivateAttr(default_factory=dict)

    model_config = ConfigDict(
        extra="ignore",
        frozen=True,
    )

    def __init_subclass__(cls) -> None:
        MODELS[cls.__name__] = cls

    @field_validator("type")
    @classmethod
    def _validate_field_type(cls, field: str) -> str:
        field = cls.__name__
        return field

    @model_validator(mode="before")
    @classmethod
    def _validate_model(cls, obj: Any) -> Any:
        if isinstance(obj, dict):
            tpe = obj.get("type", cls.__name__)
            if tpe != cls.__name__:
                obj["type"] = tpe
                cls = MODELS.get(tpe, cls)  # noqa: PLW0642
                obj = cls.model_validate(obj)
        return obj

    @classmethod
    def model_validate(  # type: ignore[reportIncompatibleMethodOverride]  # ty: ignore[invalid-method-override]
        cls,
        obj: Any,
        *,
        strict: bool | None = None,
        from_attributes: bool | None = None,
        context: dict[str, Any] | None = None,
    ) -> Self:
        """Validate a pydantic model instance.

        Args:
            obj: The object to validate.
            strict: Whether to enforce types strictly.
            from_attributes: Whether to extract data from object attributes.
            context: Additional context to pass to the validator.

        Raises:
            ValidationError: If the object could not be validated.

        Returns:
            The validated model instance.
        """
        __tracebackhide__ = True

        if isinstance(obj, cls):
            return obj

        if isinstance(obj, dict):
            cls = MODELS.get(obj.get("type", cls.__name__), cls)  # noqa: PLW0642

        return cls.__pydantic_validator__.validate_python(
            obj, strict=strict, from_attributes=from_attributes, context=context
        )

    @classmethod
    def model_validate_json(  # type: ignore[reportIncompatibleMethodOverride]  # ty: ignore[invalid-method-override]
        cls,
        json_data: str | bytes | bytearray,
        *,
        strict: bool | None = None,
        context: dict[str, Any] | None = None,
    ) -> Self:
        """Validate a pydantic model instance.

        Args:
            json_data: The object to validate.
            strict: Whether to enforce types strictly.
            from_attributes: Whether to extract data from object attributes.
            context: Additional context to pass to the validator.

        Raises:
            ValidationError: If the object could not be validated.

        Returns:
            The validated model instance.
        """
        if isinstance(json_data, str):
            json_data = json_data.encode()
        dct = orjson.loads(json_data)
        return cls.model_validate(dct, strict=strict, context=context)

    def __hash__(self) -> int:
        to_hash = {}
        for k, v in self:
            if isinstance(v, np.ndarray):
                with suppress(ValueError):
                    v = v.item()
            if isinstance(v, np.ndarray):
                to_hash[k] = np.round(v, 9).tobytes()
            elif isinstance(v, str):
                to_hash[k] = v.encode()
            elif isinstance(v, float):
                to_hash[k] = f"{v:.9f}".encode()
            else:
                to_hash[k] = str(v).encode()
        bts = b""
        for k, v in to_hash.items():
            bts += k.encode() + b":" + v + b"|"
        return abs(int.from_bytes(md5(bts[:-1]).digest(), byteorder="big") % 10**18)  # noqa: S324

    def __eq__(self, other: object) -> bool:
        if isinstance(other, dict):
            try:
                other = self.__class__.model_validate(other)
            except Exception:  # noqa: BLE001
                return False
        elif isinstance(other, str):
            try:
                other = self.__class__.model_validate_json(other)
            except Exception:  # noqa: BLE001
                return False
        return _eq(self, other)

    def _repr(self, *, indent: int = 0, shift: int = 2) -> str:
        start = f"{self.__class__.__name__}("
        dct: dict[str, Any] = dict(self)  # don't model_dump() here.
        return _dict_repr(
            dct,
            indent=indent,
            shift=shift,
            start=start,
            end=")",
            eq="=",
        )

    def __repr__(self) -> str:
        return self._repr()

    def __str__(self) -> str:
        return self._repr()

    def _visualize(self, **_: Any) -> None:
        msg = f"visualization for {self.__class__.__name__!r} not (yet) implemented."
        raise NotImplementedError(msg)

model_validate(obj, *, strict=None, from_attributes=None, context=None) classmethod

Validate a pydantic model instance.

Parameters:

Name Type Description Default
obj Any

The object to validate.

required
strict bool | None

Whether to enforce types strictly.

None
from_attributes bool | None

Whether to extract data from object attributes.

None
context dict[str, Any] | None

Additional context to pass to the validator.

None

Raises:

Type Description
ValidationError

If the object could not be validated.

Returns:

Type Description
Self

The validated model instance.

Source code in src/meow/base_model.py
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
@classmethod
def model_validate(  # type: ignore[reportIncompatibleMethodOverride]  # ty: ignore[invalid-method-override]
    cls,
    obj: Any,
    *,
    strict: bool | None = None,
    from_attributes: bool | None = None,
    context: dict[str, Any] | None = None,
) -> Self:
    """Validate a pydantic model instance.

    Args:
        obj: The object to validate.
        strict: Whether to enforce types strictly.
        from_attributes: Whether to extract data from object attributes.
        context: Additional context to pass to the validator.

    Raises:
        ValidationError: If the object could not be validated.

    Returns:
        The validated model instance.
    """
    __tracebackhide__ = True

    if isinstance(obj, cls):
        return obj

    if isinstance(obj, dict):
        cls = MODELS.get(obj.get("type", cls.__name__), cls)  # noqa: PLW0642

    return cls.__pydantic_validator__.validate_python(
        obj, strict=strict, from_attributes=from_attributes, context=context
    )

model_validate_json(json_data, *, strict=None, context=None) classmethod

Validate a pydantic model instance.

Parameters:

Name Type Description Default
json_data str | bytes | bytearray

The object to validate.

required
strict bool | None

Whether to enforce types strictly.

None
from_attributes

Whether to extract data from object attributes.

required
context dict[str, Any] | None

Additional context to pass to the validator.

None

Raises:

Type Description
ValidationError

If the object could not be validated.

Returns:

Type Description
Self

The validated model instance.

Source code in src/meow/base_model.py
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
@classmethod
def model_validate_json(  # type: ignore[reportIncompatibleMethodOverride]  # ty: ignore[invalid-method-override]
    cls,
    json_data: str | bytes | bytearray,
    *,
    strict: bool | None = None,
    context: dict[str, Any] | None = None,
) -> Self:
    """Validate a pydantic model instance.

    Args:
        json_data: The object to validate.
        strict: Whether to enforce types strictly.
        from_attributes: Whether to extract data from object attributes.
        context: Additional context to pass to the validator.

    Raises:
        ValidationError: If the object could not be validated.

    Returns:
        The validated model instance.
    """
    if isinstance(json_data, str):
        json_data = json_data.encode()
    dct = orjson.loads(json_data)
    return cls.model_validate(dct, strict=strict, context=context)

Box

Bases: Geometry3DBase

A Box is a simple rectangular cuboid.

Source code in src/meow/geometries.py
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
class Box(Geometry3DBase):
    """A Box is a simple rectangular cuboid."""

    x_min: float = Field(description="the minimum x-value of the box")
    x_max: float = Field(description="the maximum x-value of the box")
    y_min: float = Field(description="the minimum y-value of the box")
    y_max: float = Field(description="the maximum y-value of the box")
    z_min: float = Field(description="the minimum z-value of the box")
    z_max: float = Field(description="the maximum z-value of the box")

    def _project(self, z: float) -> list[Geometry2DBase]:
        if z < self.z_min or z > self.z_max:
            return []
        rect = Rectangle(
            x_min=self.x_min,
            x_max=self.x_max,
            y_min=self.y_min,
            y_max=self.y_max,
        )
        return [rect]

    def _lumadd(
        self,
        sim: Any,
        material_name: str,
        mesh_order: int,
        unit: float = 1e-6,
        xyz: str = "yzx",
    ) -> None:
        x, y, z = xyz
        name = token_hex(4)
        kwargs = {
            f"{x}_min": float(self.x_min * unit),
            f"{x}_max": float(self.x_max * unit),
            f"{y}_min": float(self.y_min * unit),
            f"{y}_max": float(self.y_max * unit),
            f"{z}_min": float(self.z_min * unit),
            f"{z}_max": float(self.z_max * unit),
        }
        sim.addrect(
            name=name,
            material=material_name,
            override_mesh_order_from_material_database=True,
            mesh_order=mesh_order,
            **kwargs,
        )

    def _trimesh(
        self, color: str | None = None, scale: tuple[float, float, float] | None = None
    ) -> Any:
        from trimesh import Trimesh  # fmt: skip
        from trimesh.creation import extrude_polygon  # fmt: skip

        sx, sy, sz = scale or (1, 1, 1)
        poly = sg.Polygon(
            [
                (self.x_min * sx, self.y_min * sy),
                (self.x_min * sx, self.y_max * sy),
                (self.x_max * sx, self.y_max * sy),
                (self.x_max * sx, self.y_min * sy),
            ],
        )
        prism = extrude_polygon(poly, self.z_max * sz - self.z_min * sz)
        prism = cast(Trimesh, prism.apply_translation((0, 0, self.z_min * sz)))
        if color is not None:
            prism.visual.face_colors = _to_rgba(color)  # type: ignore[invalid-assignment]  # ty: ignore[invalid-assignment]
        return prism

Cell

Bases: BaseModel

An EME Cell.

This defines an interval in z (the direction of propagation) within the simulation domain. The intersecting Structure3Ds are discretized by a given mesh at the center of the Cell

Source code in src/meow/cell.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
class Cell(BaseModel):
    """An EME Cell.

    This defines an interval in z (the direction of propagation) within
    the simulation domain. The intersecting Structure3Ds are discretized by
    a given mesh at the center of the Cell

    """

    structures: list[Structure3D] = Field(
        description="the 3D structures which will be sliced by the cell"
    )
    mesh: Mesh2D = Field(description="the mesh to discretize the structures with")
    z_min: float = Field(description="the starting z-coordinate of the cell")
    z_max: float = Field(description="the ending z-coordinate of the cell")

    @property
    def z(self) -> float:
        """The z-position of the center of the cell."""
        return 0.5 * (self.z_min + self.z_max)

    @property
    def length(self) -> float:
        """The length of the cell."""
        return np.abs(self.z_max - self.z_min)

    @cached_property
    def materials(self) -> dict[Material, int]:
        """A mapping of the materials in the cell to their indices."""
        materials = {}
        for i, structure in enumerate(sort_structures(self.structures), start=1):
            if structure.material not in materials:
                materials[structure.material] = i
        return materials

    @cached_property
    def structures_2d(self) -> list[Structure2D]:
        """The 2D structures in the cell."""
        z = 0.5 * (self.z_min + self.z_max)
        list_of_list = [s._project(z) for s in self.structures]
        structures = [s for ss in list_of_list for s in ss]
        return structures

    @cached_property
    def m_full(self) -> IntArray2D:
        """The full material mask for the cell."""
        return _create_full_material_array(
            mesh=self.mesh,
            structures=self.structures_2d,
            materials=self.materials,
        )

    def _visualize(
        self,
        *,
        ax: Any = None,
        cbar: bool = True,
        show: bool = True,
        **_: Any,
    ) -> None:
        import matplotlib.pyplot as plt
        from matplotlib.colors import ListedColormap, to_rgba
        from mpl_toolkits.axes_grid1 import make_axes_locatable

        colors = [(0, 0, 0, 0)] + [
            to_rgba(m.meta.get("color", (0, 0, 0, 0))) for m in self.materials
        ]
        cmap = ListedColormap(colors=colors)
        if ax is not None:
            plt.sca(ax)
        else:
            ax = plt.gca()
        plt.pcolormesh(
            self.mesh.X_full,
            self.mesh.Y_full,
            self.m_full,
            cmap=cmap,
            vmin=0,
            vmax=len(self.materials) + 1,
        )
        plt.axis("scaled")
        plt.grid(visible=True)
        if cbar:
            divider = make_axes_locatable(ax)
            cax = divider.append_axes("right", size="5%", pad=0.05)
            _cbar = plt.colorbar(
                ticks=np.concatenate(
                    [np.unique(self.m_full) + 0.5, [len(self.materials) + 1.5]]
                ),
                cax=cax,
            )
            labels = [""] + [m.name for m in self.materials] + [""]
            _cbar.ax.set_yticklabels(labels, rotation=90, va="center")
            plt.sca(ax)
        if show:
            plt.show()

length property

The length of the cell.

z property

The z-position of the center of the cell.

m_full()

The full material mask for the cell.

Source code in src/meow/cell.py
64
65
66
67
68
69
70
71
@cached_property
def m_full(self) -> IntArray2D:
    """The full material mask for the cell."""
    return _create_full_material_array(
        mesh=self.mesh,
        structures=self.structures_2d,
        materials=self.materials,
    )

materials()

A mapping of the materials in the cell to their indices.

Source code in src/meow/cell.py
47
48
49
50
51
52
53
54
@cached_property
def materials(self) -> dict[Material, int]:
    """A mapping of the materials in the cell to their indices."""
    materials = {}
    for i, structure in enumerate(sort_structures(self.structures), start=1):
        if structure.material not in materials:
            materials[structure.material] = i
    return materials

structures_2d()

The 2D structures in the cell.

Source code in src/meow/cell.py
56
57
58
59
60
61
62
@cached_property
def structures_2d(self) -> list[Structure2D]:
    """The 2D structures in the cell."""
    z = 0.5 * (self.z_min + self.z_max)
    list_of_list = [s._project(z) for s in self.structures]
    structures = [s for ss in list_of_list for s in ss]
    return structures

CrossSection

Bases: BaseModel

A CrossSection is built from a Cell with an Environment.

This uniquely defines the refractive index everywhere.

Source code in src/meow/cross_section.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
class CrossSection(BaseModel):
    """A `CrossSection` is built from a `Cell` with an `Environment`.

    This uniquely defines the refractive index everywhere.
    """

    structures: list[Structure2D] = Field(
        description="the 2D structures in the CrossSection"
    )
    mesh: Mesh2D = Field(description="the mesh to discretize the structures with")
    env: Environment = Field(
        description="the environment for which the cross section was calculated"
    )
    subpixel_smoothing: bool = Field(
        default=True,
        description="use subpixel smoothing at interfaces; if False: winner-takes-all",
    )
    _cell: Cell | None = PrivateAttr(default=None)

    @classmethod
    def from_cell(
        cls,
        *,
        cell: Cell,
        env: Environment,
        subpixel_smoothing: bool = True,
    ) -> Self:
        """Create a CrossSection from a Cell and Environment."""
        return cls(
            structures=cell.structures_2d,
            mesh=cell.mesh,
            env=env,
            subpixel_smoothing=subpixel_smoothing,
            _cell=cell,
        )

    @cached_property
    def materials(self) -> dict[Material, int]:
        """Return a dictionary mapping materials to their indices."""
        materials: dict[Material, int] = {}
        for i, structure in enumerate(sort_structures(self.structures), start=1):
            if structure.material not in materials:
                materials[structure.material] = i
        return materials

    @cached_property
    def _m_full(self) -> IntArray2D:
        """Return the material index array for the full mesh."""
        return _create_full_material_array(self.mesh, self.structures, self.materials)

    @cached_property
    def n_full(self) -> ComplexArray2D:
        """Return the refractive index array for the full mesh."""
        n_full = np.ones_like(self.mesh.X_full, dtype=np.complex128)
        for material, idx in self.materials.items():
            n_full = np.where(self._m_full == idx, material(self.env), n_full)
        return n_full

    @cached_property
    def nx(self) -> ComplexArray2D:
        """Return the smoothed refractive index on the Ex positions."""
        if self.subpixel_smoothing:
            return _compute_smoothed_n(
                self.mesh, self._m_full, self.materials, self.env, self.structures, "x"
            )
        return _compute_winner_takes_all_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "x"
        )

    @cached_property
    def ny(self) -> ComplexArray2D:
        """Return the smoothed refractive index on the Ey positions."""
        if self.subpixel_smoothing:
            return _compute_smoothed_n(
                self.mesh, self._m_full, self.materials, self.env, self.structures, "y"
            )
        return _compute_winner_takes_all_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "y"
        )

    @cached_property
    def nz(self) -> ComplexArray2D:
        """Return the smoothed refractive index on the Ez positions."""
        if self.subpixel_smoothing:
            return _compute_smoothed_n(
                self.mesh, self._m_full, self.materials, self.env, self.structures, "z"
            )
        return _compute_winner_takes_all_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "z"
        )

    def _visualize(
        self,
        *,
        ax: Any = None,
        n_cmap: Any = None,
        cbar: bool = True,
        show: bool = True,
        **ignored: Any,
    ) -> None:
        import matplotlib.pyplot as plt  # fmt: skip
        from matplotlib import colors  # fmt: skip
        from mpl_toolkits.axes_grid1 import make_axes_locatable  # fmt: skip

        debug_grid = ignored.pop("debug_grid", False)
        if n_cmap is None:
            n_cmap = colors.LinearSegmentedColormap.from_list(
                name="c_cmap", colors=["#ffffff", "#86b5dc"]
            )
        if ax is not None:
            plt.sca(ax)
        else:
            ax = plt.gca()
        n_full = np.real(self.n_full).copy()
        n_full[0, 0] = 1.0
        plt.pcolormesh(self.mesh.X_full, self.mesh.Y_full, n_full, cmap=n_cmap)
        plt.axis("scaled")
        if not debug_grid:
            plt.grid(visible=True)
        else:
            dx = self.mesh.dx
            dy = self.mesh.dy
            x_ticks = np.sort(np.unique(self.mesh.X_full.ravel()))[::2]
            y_ticks = np.sort(np.unique(self.mesh.Y_full.ravel()))[::2]
            plt.xticks(x_ticks - 0.25 * dx, ["" for _ in x_ticks - 0.25 * dx])
            plt.yticks(y_ticks - 0.25 * dy, ["" for _ in y_ticks - 0.25 * dy])
            plt.xticks(
                x_ticks + 0.25 * dx, ["" for _ in x_ticks + 0.25 * dx], minor=True
            )
            plt.yticks(
                y_ticks + 0.25 * dy, ["" for _ in y_ticks + 0.25 * dy], minor=True
            )
            plt.grid(visible=True, which="major", ls="-")
            plt.grid(visible=True, which="minor", ls=":")
        if cbar:
            divider = make_axes_locatable(ax)
            cax = divider.append_axes("right", size="5%", pad=0.05)
            values = np.unique(np.real(self.n_full))
            _cbar = plt.colorbar(ticks=values, cax=cax)
            # material_names = ['air'] + [mat.name for mat in self.cell.materials]
            # labels = [f"\n{n}\n{v:.3f}" for n, v in zip(material_names, values)]
            labels = [f"{v:.3f}" for v in values]
            _cbar.ax.set_yticklabels(labels, rotation=90, va="center", ha="center")
            plt.sca(ax)
        if show:
            plt.show()

from_cell(*, cell, env, subpixel_smoothing=True) classmethod

Create a CrossSection from a Cell and Environment.

Source code in src/meow/cross_section.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@classmethod
def from_cell(
    cls,
    *,
    cell: Cell,
    env: Environment,
    subpixel_smoothing: bool = True,
) -> Self:
    """Create a CrossSection from a Cell and Environment."""
    return cls(
        structures=cell.structures_2d,
        mesh=cell.mesh,
        env=env,
        subpixel_smoothing=subpixel_smoothing,
        _cell=cell,
    )

materials()

Return a dictionary mapping materials to their indices.

Source code in src/meow/cross_section.py
63
64
65
66
67
68
69
70
@cached_property
def materials(self) -> dict[Material, int]:
    """Return a dictionary mapping materials to their indices."""
    materials: dict[Material, int] = {}
    for i, structure in enumerate(sort_structures(self.structures), start=1):
        if structure.material not in materials:
            materials[structure.material] = i
    return materials

n_full()

Return the refractive index array for the full mesh.

Source code in src/meow/cross_section.py
77
78
79
80
81
82
83
@cached_property
def n_full(self) -> ComplexArray2D:
    """Return the refractive index array for the full mesh."""
    n_full = np.ones_like(self.mesh.X_full, dtype=np.complex128)
    for material, idx in self.materials.items():
        n_full = np.where(self._m_full == idx, material(self.env), n_full)
    return n_full

nx()

Return the smoothed refractive index on the Ex positions.

Source code in src/meow/cross_section.py
85
86
87
88
89
90
91
92
93
94
@cached_property
def nx(self) -> ComplexArray2D:
    """Return the smoothed refractive index on the Ex positions."""
    if self.subpixel_smoothing:
        return _compute_smoothed_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "x"
        )
    return _compute_winner_takes_all_n(
        self.mesh, self._m_full, self.materials, self.env, self.structures, "x"
    )

ny()

Return the smoothed refractive index on the Ey positions.

Source code in src/meow/cross_section.py
 96
 97
 98
 99
100
101
102
103
104
105
@cached_property
def ny(self) -> ComplexArray2D:
    """Return the smoothed refractive index on the Ey positions."""
    if self.subpixel_smoothing:
        return _compute_smoothed_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "y"
        )
    return _compute_winner_takes_all_n(
        self.mesh, self._m_full, self.materials, self.env, self.structures, "y"
    )

nz()

Return the smoothed refractive index on the Ez positions.

Source code in src/meow/cross_section.py
107
108
109
110
111
112
113
114
115
116
@cached_property
def nz(self) -> ComplexArray2D:
    """Return the smoothed refractive index on the Ez positions."""
    if self.subpixel_smoothing:
        return _compute_smoothed_n(
            self.mesh, self._m_full, self.materials, self.env, self.structures, "z"
        )
    return _compute_winner_takes_all_n(
        self.mesh, self._m_full, self.materials, self.env, self.structures, "z"
    )

Environment

Bases: BaseModel

An environment contains all variables that don't depend on the structure.

Source code in src/meow/environment.py
10
11
12
13
14
15
16
17
18
19
class Environment(BaseModel):
    """An environment contains all variables that don't depend on the structure."""

    wl: float = Field(default=1.5, description="the wavelength of the environment")
    T: float = Field(default=25.0, description="the temperature of the environment")

    model_config = ConfigDict(
        extra="allow",
        frozen=True,
    )

GdsExtrusionRule

Bases: BaseModel

A GdsExtrusionRule describes a single extrusion rule.

Multiple of such rules can later be associated with a gds layer tuple.

Source code in src/meow/gds_structures.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class GdsExtrusionRule(BaseModel):
    """A `GdsExtrusionRule` describes a single extrusion rule.

    Multiple of such rules can later be associated with a gds layer tuple.
    """

    material: Material = Field(description="the material of the extrusion")
    h_min: float = Field(description="the extrusion starting height")
    h_max: float = Field(description="the extrusion ending height")
    buffer: float = Field(
        default=0.0,
        description="an extra buffer (grow / shrink) operation applied to the polygon",
    )
    mesh_order: int = Field(
        default=5, description="the mesh order of the resulting `Structure3D`"
    )

    def __call__(self, poly: FloatArray2D) -> Structure3D:
        """Apply the extrusion rule to a polygon."""
        if self.buffer > 0:
            try:
                poly = np.asarray(sg.Polygon(poly).buffer(self.buffer).boundary.coords)
            except NotImplementedError as e:
                import gdspy  # type: ignore[unresolved-import]

                polygonset = gdspy.offset(gdspy.Polygon(poly), 0.25)
                if polygonset is None:
                    msg = (
                        "The polygon could not be offset."
                        "Please check the input polygon."
                    )
                    raise ValueError(msg) from e
                poly = polygonset.polygons[0]
        return Structure3D(
            material=self.material,
            geometry=Prism(
                poly=poly,
                h_min=self.h_min,
                h_max=self.h_max,
                axis="y",
            ),
            mesh_order=self.mesh_order,
        )

__call__(poly)

Apply the extrusion rule to a polygon.

Source code in src/meow/gds_structures.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __call__(self, poly: FloatArray2D) -> Structure3D:
    """Apply the extrusion rule to a polygon."""
    if self.buffer > 0:
        try:
            poly = np.asarray(sg.Polygon(poly).buffer(self.buffer).boundary.coords)
        except NotImplementedError as e:
            import gdspy  # type: ignore[unresolved-import]

            polygonset = gdspy.offset(gdspy.Polygon(poly), 0.25)
            if polygonset is None:
                msg = (
                    "The polygon could not be offset."
                    "Please check the input polygon."
                )
                raise ValueError(msg) from e
            poly = polygonset.polygons[0]
    return Structure3D(
        material=self.material,
        geometry=Prism(
            poly=poly,
            h_min=self.h_min,
            h_max=self.h_max,
            axis="y",
        ),
        mesh_order=self.mesh_order,
    )

Geometry2DBase

Bases: BaseModel

Base class for 2D geometries.

Source code in src/meow/geometries.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Geometry2DBase(BaseModel):
    """Base class for 2D geometries."""

    def _mask(self, X: FloatArray2D, Y: FloatArray2D) -> BoolArray2D:
        msg = f"{self.__class__.__name__!r} cannot be masked."
        raise NotImplementedError(msg)

    def _shapely_polygon(self) -> sg.Polygon:
        msg = f"{self.__class__.__name__!r} has no shapely polygon."
        raise NotImplementedError(msg)

    def _visualize(
        self,
        *,
        ax: Any = None,
        show: bool = True,
        color: str | None = None,
        **ignored: Any,
    ) -> None:
        msg = f"{self.__class__.__name__!r} cannot be visualized."
        raise NotImplementedError(msg)

Geometry3DBase

Bases: BaseModel

Base class for 3D geometries.

Source code in src/meow/geometries.py
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
class Geometry3DBase(BaseModel):
    """Base class for 3D geometries."""

    def _project(self, z: float) -> list[Geometry2DBase]:
        msg = f"{self.__class__.__name__!r} cannot be projected to 2D."

        raise NotImplementedError(msg)

    def _visualize(
        self,
        scale: tuple[float, float, float] | None = None,
        **ignored: Any,  # noqa: ARG002
    ) -> None:
        from trimesh.scene import Scene
        from trimesh.transformations import rotation_matrix

        scene = Scene()
        scene.add_geometry(self._trimesh(scale=scale))
        scene.apply_transform(rotation_matrix(-np.pi / 6, (0, 1, 0)))
        scene.show()

    def _lumadd(
        self,
        sim: Any,
        material_name: str,
        mesh_order: int,
        unit: float = 1e-6,
        xyz: str = "yzx",
    ) -> None:
        msg = f"{self.__class__.__name__!r} cannot be added to Lumerical."
        raise NotImplementedError(msg)

    def _trimesh(
        self, color: str | None = None, scale: tuple[float, float, float] | None = None
    ) -> Any:
        msg = f"{self.__class__.__name__!r} cannot be visualized."
        raise NotImplementedError(msg)

IndexMaterial

Bases: MaterialBase

A material with a constant refractive index.

Source code in src/meow/materials.py
108
109
110
111
112
113
114
115
class IndexMaterial(MaterialBase):
    """A material with a constant refractive index."""

    n: complex = Field(description="the refractive index of the material")

    def __call__(self, env: Environment) -> np.ndarray:  # noqa: ARG002
        """Get the refractive index of the material for the given environment."""
        return np.squeeze(np.asarray(self.n, dtype=np.complex128))

__call__(env)

Get the refractive index of the material for the given environment.

Source code in src/meow/materials.py
113
114
115
def __call__(self, env: Environment) -> np.ndarray:  # noqa: ARG002
    """Get the refractive index of the material for the given environment."""
    return np.squeeze(np.asarray(self.n, dtype=np.complex128))

MaterialBase

Bases: BaseModel

a Material defines the index of a Structure3D in an Environment.

Source code in src/meow/materials.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class MaterialBase(BaseModel):
    """a `Material` defines the index of a `Structure3D` in an `Environment`."""

    name: str = Field(description="the name of the material")
    meta: dict[str, Any] = Field(
        default_factory=dict, description="metadata for the material"
    )

    @model_validator(mode="after")
    def _validate_model(self) -> Self:
        MATERIALS[self.name] = self
        return self

    def __call__(self, env: Environment) -> np.ndarray:
        """Get the refractive index of the material for the given environment."""
        msg = "Please use one of the Material child classes"
        raise NotImplementedError(msg)

    def _lumadd(self, sim: Any, env: Environment, unit: float) -> str:
        from matplotlib.cm import get_cmap

        n = self(env)
        wl = np.asarray(env.wl * unit, dtype=complex).ravel()
        eps = np.asarray(n, dtype=complex).ravel() ** 2
        data = np.stack([c / wl, eps], 1)  # permittivity, not index

        if not sim.materialexists(self.name):
            sim.setmaterial(sim.addmaterial("Sampled data"), "name", self.name)
            color = np.asarray(
                self.meta.get("color") or get_cmap("jet")(np.abs(eps) / 15.0)
            )
            sim.setmaterial(self.name, "color", color)

        sim.setmaterial(self.name, "sampled data", data)

        return self.name

__call__(env)

Get the refractive index of the material for the given environment.

Source code in src/meow/materials.py
36
37
38
39
def __call__(self, env: Environment) -> np.ndarray:
    """Get the refractive index of the material for the given environment."""
    msg = "Please use one of the Material child classes"
    raise NotImplementedError(msg)

Mesh2D

Bases: BaseModel

A Mesh2D describes how a Structure3D is discritized into a Cell.

Source code in src/meow/mesh.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
class Mesh2D(BaseModel):
    """A ``Mesh2D`` describes how a ``Structure3D`` is discritized into a ``Cell``."""

    x: FloatArray1D = Field(
        description="x-coords of the mesh (Ez locations, i.e. corners of the 2D cell)"
    )

    y: FloatArray1D = Field(
        description="y-coords of the mesh (Ez locations, i.e. corners of the 2D cell)"
    )

    angle_phi: float = Field(
        default=0.0,
        description=(
            "Azimuth angle of the propagation axis in the plane orthogonal to the mesh."
        ),
    )
    angle_theta: float = Field(
        default=0.0,
        description="Polar angle of the propagation axis from the injection axis.",
    )
    bend_radius: Annotated[
        float, BeforeValidator(lambda x: np.nan if x is None else x)
    ] = Field(
        default=np.nan,
        description=(
            "A curvature radius for simulation of waveguide bends. Can be negative, "
            "in which case the mode plane center has a smaller value than the "
            "curvature center along the tangential axis perpendicular to the bend axis."
        ),
    )
    bend_axis: Literal[0, 1] = Field(
        default=0,
        description=(
            "Index into the two tangential axes defining the normal to the plane in "
            "which the bend lies. This must be provided if ``bend_radius`` is not "
            "``None``. For example, for a ring in the global xy-plane, and a mode "
            "plane in either the xz or the yz plane, the ``bend_axis`` is always 1 "
            "(the global z axis)."
        ),
    )
    plane_center: tuple[float, float] = Field(
        default=(0.0, 0.0),
        description=(
            "If ``bend_radius`` is not ``None``, the position of the plane "
            "corresponding along the circonference of the circle"
        ),
    )

    num_pml: tuple[NonNegativeInt, NonNegativeInt] = Field(
        default=(0, 0),
        description="Number of standard pml layers to add in the two tangential axes.",
    )

    @cached_property
    def dx(self) -> FloatArray1D:
        """dx at Hz locations, i.e. center of the 2D cell."""
        return self.x[1:] - self.x[:-1]

    @cached_property
    def dy(self) -> FloatArray1D:
        """dy at Hz locations, i.e. center of the 2D cell."""
        return self.y[1:] - self.y[:-1]

    @cached_property
    def x_(self) -> FloatArray1D:
        """x at Hz locations, i.e. center of the 2D cell."""
        return 0.5 * (self.x[1:] + self.x[:-1])

    @cached_property
    def y_(self) -> FloatArray1D:
        """y at Hz locations, i.e. center of the 2D cell."""
        return 0.5 * (self.y[1:] + self.y[:-1])

    @cached_property
    def x_full(self) -> FloatArray1D:
        """x at half-integer locations."""
        return np.stack([self.x[:-1], self.x[:-1] + self.dx / 2], 1).ravel()

    @cached_property
    def y_full(self) -> FloatArray1D:
        """y at half-integer locations."""
        return np.stack([self.y[:-1], self.y[:-1] + self.dy / 2], 1).ravel()

    @cached_property
    def XY_full(self) -> tuple[FloatArray2D, FloatArray2D]:
        """X and Y at half-integer locations."""
        Y_full, X_full = np.meshgrid(self.y_full, self.x_full)
        return X_full, Y_full

    @property
    def X_full(self) -> FloatArray2D:
        """X at half-integer locations."""
        return self.XY_full[0]

    @property
    def Y_full(self) -> FloatArray2D:
        """Y at half-integer locations."""
        return self.XY_full[1]

    @property
    def Xx(self) -> FloatArray2D:
        """X at Ex locations."""
        return self.X_full[1::2, ::2]

    @property
    def Yx(self) -> FloatArray2D:
        """Y at Ex locations."""
        return self.Y_full[1::2, ::2]

    @property
    def Xy(self) -> FloatArray2D:
        """X at Ey locations."""
        return self.X_full[::2, 1::2]

    @property
    def Yy(self) -> FloatArray2D:
        """Y at Ey locations."""
        return self.Y_full[::2, 1::2]

    @property
    def Xz(self) -> FloatArray2D:
        """X at Ez locations."""
        return self.X_full[::2, ::2]

    @property
    def Yz(self) -> FloatArray2D:
        """Y at Ez locations."""
        return self.Y_full[::2, ::2]

    @property
    def Xz_(self) -> FloatArray2D:
        """X at Hz locations."""
        return self.X_full[1::2, 1::2]

    @property
    def Yz_(self) -> FloatArray2D:
        """Y at Hz locations."""
        return self.Y_full[1::2, 1::2]

X_full property

X at half-integer locations.

Xx property

X at Ex locations.

Xy property

X at Ey locations.

Xz property

X at Ez locations.

Xz_ property

X at Hz locations.

Y_full property

Y at half-integer locations.

Yx property

Y at Ex locations.

Yy property

Y at Ey locations.

Yz property

Y at Ez locations.

Yz_ property

Y at Hz locations.

XY_full()

X and Y at half-integer locations.

Source code in src/meow/mesh.py
 99
100
101
102
103
@cached_property
def XY_full(self) -> tuple[FloatArray2D, FloatArray2D]:
    """X and Y at half-integer locations."""
    Y_full, X_full = np.meshgrid(self.y_full, self.x_full)
    return X_full, Y_full

dx()

dx at Hz locations, i.e. center of the 2D cell.

Source code in src/meow/mesh.py
69
70
71
72
@cached_property
def dx(self) -> FloatArray1D:
    """dx at Hz locations, i.e. center of the 2D cell."""
    return self.x[1:] - self.x[:-1]

dy()

dy at Hz locations, i.e. center of the 2D cell.

Source code in src/meow/mesh.py
74
75
76
77
@cached_property
def dy(self) -> FloatArray1D:
    """dy at Hz locations, i.e. center of the 2D cell."""
    return self.y[1:] - self.y[:-1]

x_()

x at Hz locations, i.e. center of the 2D cell.

Source code in src/meow/mesh.py
79
80
81
82
@cached_property
def x_(self) -> FloatArray1D:
    """x at Hz locations, i.e. center of the 2D cell."""
    return 0.5 * (self.x[1:] + self.x[:-1])

x_full()

x at half-integer locations.

Source code in src/meow/mesh.py
89
90
91
92
@cached_property
def x_full(self) -> FloatArray1D:
    """x at half-integer locations."""
    return np.stack([self.x[:-1], self.x[:-1] + self.dx / 2], 1).ravel()

y_()

y at Hz locations, i.e. center of the 2D cell.

Source code in src/meow/mesh.py
84
85
86
87
@cached_property
def y_(self) -> FloatArray1D:
    """y at Hz locations, i.e. center of the 2D cell."""
    return 0.5 * (self.y[1:] + self.y[:-1])

y_full()

y at half-integer locations.

Source code in src/meow/mesh.py
94
95
96
97
@cached_property
def y_full(self) -> FloatArray1D:
    """y at half-integer locations."""
    return np.stack([self.y[:-1], self.y[:-1] + self.dy / 2], 1).ravel()

Mode

Bases: BaseModel

A Mode contains the field information for a given CrossSection.

Source code in src/meow/mode.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
class Mode(BaseModel):
    """A `Mode` contains the field information for a given `CrossSection`."""

    neff: Complex = Field(description="the effective index of the mode")
    cs: CrossSection = Field(
        description="the index cross section for which the mode was calculated"
    )
    Ex: ComplexArray2D = Field(description="the Ex-fields of the mode")
    Ey: ComplexArray2D = Field(description="the Ey-fields of the mode")
    Ez: ComplexArray2D = Field(description="the Ez-fields of the mode")
    Hx: ComplexArray2D = Field(description="the Hx-fields of the mode")
    Hy: ComplexArray2D = Field(description="the Hy-fields of the mode")
    Hz: ComplexArray2D = Field(description="the Hz-fields of the mode")
    interpolation: Literal["Ex", "Ey", "Ez", "Hz", ""] = Field(
        default="",
        description="To which 2D Yee-location the fields are interpolated to.",
    )

    def interpolate(
        self,
        location: Literal["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] | None,
    ) -> Mode:
        """Interpolate the mode to the given location."""
        if location is None or location == self.interpolation:
            return self
        if self.interpolation != "":
            msg = "Cannot interpolate from already interpolated mode!"
            raise RuntimeError(msg)
        interpolate_funcs = {
            "EX": _interpolate_Ex,
            "EY": _interpolate_Ey,
            "EZ": _interpolate_Ez,
            "HX": _interpolate_Ey,
            "HY": _interpolate_Ex,
            "HZ": _interpolate_Hz,
        }
        interpolate_func = interpolate_funcs[location.upper()]
        return interpolate_func(self)

    @property
    def te_fraction(self) -> float:
        """The TE polarization fraction of the mode."""
        return te_fraction(self)

    @cached_property
    def _pointing(self) -> tuple[ComplexArray2D, ComplexArray2D, ComplexArray2D]:
        """Calculate and cache the poynting vector."""
        vecE = np.stack([self.Ex, self.Ey, self.Ez], axis=-1)
        vecH = np.stack([self.Hx, self.Hy, self.Hz], axis=-1)
        vecP = np.cross(vecE, vecH)
        Px, Py, Pz = np.rollaxis(vecP, -1)
        return Px, Py, Pz

    @property
    def Px(self) -> ComplexArray2D:
        """The x-component of the Poynting vector."""
        return self._pointing[0]

    @property
    def Py(self) -> ComplexArray2D:
        """The y-component of the Poynting vector."""
        return self._pointing[1]

    @property
    def Pz(self) -> ComplexArray2D:
        """The z-component of the Poynting vector."""
        return self._pointing[2]

    @property
    def env(self) -> Environment:
        """The environment of the mode."""
        return self.cs.env

    @property
    def mesh(self) -> Mesh2D:
        """The mesh of the mode."""
        return self.cs.mesh

    def _visualize(  # noqa: PLR0915
        self,
        *,
        title: str | None = None,
        title_prefix: str = "",
        fields: tuple[str, ...] = ("Ex",),
        ax: Any = None,
        n_cmap: str | colors.LinearSegmentedColormap | None = None,
        mode_cmap: str | None = None,
        num_levels: int = 8,
        operation: Callable = _power,
        show: bool = True,
        **_: Any,
    ) -> None:
        from meow.visualization import _figsize_visualize_mode

        W, H = _figsize_visualize_mode(self.cs, 6.4)

        if not fields:
            fields = ("Ex",)

        if len(fields) > 1:
            if len(fields) > 2:
                max_width = 15
                current_width = len(fields) * W
                W, H = _figsize_visualize_mode(self.cs, 6.4 * max_width / current_width)
            if ax is None:
                _fig, ax = plt.subplots(1, len(fields), figsize=(len(fields) * W, H))
            if len(ax) < len(fields):
                msg = (
                    f"Not enough axes supplied for the number of fields "
                    f"to plot! {len(ax)} < {len(fields)}."
                )
                raise ValueError(msg)
            for field, ax_ in zip(fields, ax, strict=False):
                self._visualize(
                    title=title,
                    title_prefix=title_prefix,
                    fields=(field,),
                    ax=ax_,
                    n_cmap=n_cmap,
                    mode_cmap=mode_cmap,
                    num_levels=num_levels,
                    operation=operation,
                    show=False,
                )
            if show:
                plt.show()
            return

        field = "Ex" if not fields else fields[0]
        valid_fields = ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz", "Px", "Py", "Pz"]
        if field not in valid_fields:
            msg = f"Invalid field {field!r}. Valid fields: {', '.join(valid_fields)}."
            raise ValueError(msg)

        if ax is None:
            ax = plt.gca()
        plt.sca(ax)

        if n_cmap is None:
            # little bit lighter colored than the one in cs._visualize:
            n_cmap = colors.LinearSegmentedColormap.from_list(
                name="c_cmap", colors=["#ffffff", "#c1d9ed"]
            )
        self.cs._visualize(ax=ax, n_cmap=n_cmap, cbar=False, show=False)

        x, y = "x", "y"  # currently only propagation in z supported, see Mesh2D
        c = {
            "Ex": "x",
            "Ey": "y",
            "Ez": "z",
            "Hx": "y",
            "Hy": "x",
            "Hz": "z_",
            "Px": "x",
            "Py": "y",
            "Pz": "z",
        }[field]
        if mode_cmap is None:
            mode_cmap = "inferno"
        X = getattr(self.mesh, f"X{c}")
        Y = getattr(self.mesh, f"Y{c}")
        mode = operation(getattr(self, field))
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=UserWarning)
            levels = np.linspace(mode.min(), mode.max(), num_levels + 1)[1:]
            plt.contour(X, Y, mode, cmap=mode_cmap, levels=levels)
            # plt.pcolormesh(X, Y, mode, cmap=mode_cmap, alpha=0.5) #, levels=levels)
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)
        plt.colorbar(cax=cax)
        plt.sca(ax)

        plt.xlabel(x)
        plt.ylabel(y)
        plt.grid(visible=True, alpha=0.4)
        if title is None:
            plt.title(f"{title_prefix}{field} [neff={float(np.real(self.neff)):.6f}]")
        else:
            plt.title(f"{title_prefix}{title}")
        plt.xlim(X.min(), X.max())
        plt.ylim(Y.min(), Y.max())
        plt.axis("scaled")
        if show:
            plt.show()

    def save(self, filename: str | Path) -> None:
        """Save the mode to a file."""
        path = Path(filename)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(pickle.dumps(self))

    @classmethod
    def load(cls, filename: str | Path) -> Mode:
        """Load a mode from a file."""
        return cast(Mode, pickle.loads(Path(filename).read_bytes()))

    def __add__(self, other: Mode) -> Mode:
        if not isinstance(other, Mode):
            msg = (
                "unsupported operand type(s) for +: "
                f"'Mode' and '{type(other).__name__}'"
            )
            raise TypeError(msg)
        new_mode = Mode(
            neff=0.5 * (self.neff + other.neff),
            cs=self.cs,
            Ex=self.Ex + other.Ex,
            Ey=self.Ey + other.Ey,
            Ez=self.Ez + other.Ez,
            Hx=self.Hx + other.Hx,
            Hy=self.Hy + other.Hy,
            Hz=self.Hz + other.Hz,
        )
        return new_mode

    def __mul__(self, other: np.complexfloating | complex) -> Mode:
        if not isinstance(
            other, float | np.complexfloating | np.floating | complex | int | np.integer
        ):
            msg = (
                "unsupported operand type(s) for *: "
                f"'Mode' and '{type(other).__name__}'"
            )
            raise TypeError(msg)
        new_mode = Mode(
            neff=self.neff,
            cs=self.cs,
            Ex=self.Ex * other,
            Ey=self.Ey * other,
            Ez=self.Ez * other,
            Hx=self.Hx * other,
            Hy=self.Hy * other,
            Hz=self.Hz * other,
        )
        return new_mode

    def __rmul__(self, other: np.complexfloating | complex) -> Mode:
        return self.__mul__(other)

    def __truediv__(self, other: np.complexfloating | complex) -> Mode:
        if not isinstance(
            other,
            float | np.complexfloating | np.floating | complex | int | np.integer,
        ):
            msg = (
                "unsupported operand type(s) for /: "
                f"'Mode' and '{type(other).__name__}'"
            )
            raise TypeError(msg)
        return self * (1 / other)

    def __sub__(self, other: Mode) -> Mode:
        if not isinstance(other, Mode):
            msg = (
                "unsupported operand type(s) for -: "
                f"'Mode' and '{type(other).__name__}'"
            )
            raise TypeError(msg)
        return self + other * (-1.0)

Px property

The x-component of the Poynting vector.

Py property

The y-component of the Poynting vector.

Pz property

The z-component of the Poynting vector.

env property

The environment of the mode.

mesh property

The mesh of the mode.

te_fraction property

The TE polarization fraction of the mode.

interpolate(location)

Interpolate the mode to the given location.

Source code in src/meow/mode.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def interpolate(
    self,
    location: Literal["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] | None,
) -> Mode:
    """Interpolate the mode to the given location."""
    if location is None or location == self.interpolation:
        return self
    if self.interpolation != "":
        msg = "Cannot interpolate from already interpolated mode!"
        raise RuntimeError(msg)
    interpolate_funcs = {
        "EX": _interpolate_Ex,
        "EY": _interpolate_Ey,
        "EZ": _interpolate_Ez,
        "HX": _interpolate_Ey,
        "HY": _interpolate_Ex,
        "HZ": _interpolate_Hz,
    }
    interpolate_func = interpolate_funcs[location.upper()]
    return interpolate_func(self)

load(filename) classmethod

Load a mode from a file.

Source code in src/meow/mode.py
223
224
225
226
@classmethod
def load(cls, filename: str | Path) -> Mode:
    """Load a mode from a file."""
    return cast(Mode, pickle.loads(Path(filename).read_bytes()))

save(filename)

Save the mode to a file.

Source code in src/meow/mode.py
217
218
219
220
221
def save(self, filename: str | Path) -> None:
    """Save the mode to a file."""
    path = Path(filename)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(pickle.dumps(self))

ModelMetaclass

Bases: ModelMetaclass

Metaclass for all models.

Source code in src/meow/base_model.py
27
28
29
30
31
32
class ModelMetaclass(_ModelMetaclass):
    """Metaclass for all models."""

    def __call__(cls, *args: Any, **kwargs: Any) -> type:  # noqa: N805,D102
        obj = super().__call__(*args, **kwargs)
        return obj

Polygon2D

Bases: Geometry2DBase

A 2D polygon defined by a list of vertices.

Source code in src/meow/geometries.py
 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
class Polygon2D(Geometry2DBase):
    """A 2D polygon defined by a list of vertices."""

    poly: Annotated[NDArray, Shape(-1, 2), DType("float64")] = Field(
        description="the 2D array (Nx2) with polygon vertices"
    )

    def _mask(self, X: FloatArray2D, Y: FloatArray2D) -> BoolArray2D:
        poly = sg.Polygon(self.poly)

        points = shapely.points(X, Y)
        mask = poly.covers(points)

        return np.asarray(mask)

    def _shapely_polygon(self) -> sg.Polygon:
        return sg.Polygon(self.poly)

    def _visualize(
        self,
        *,
        ax: Any = None,
        show: bool = True,
        color: str | None = None,
        **_: Any,
    ) -> None:
        if ax is None:
            ax = plt.gca()
        if color is None:
            color = "grey"

        patch = MplPolygon(self.poly, closed=True, color=color)

        ax.add_patch(patch)

        min_x, max_x = min(self.poly[:, 0]), max(self.poly[:, 0])
        min_y, max_y = min(self.poly[:, 1]), max(self.poly[:, 1])

        extent_x = max_x - min_x
        extent_y = max_y - min_y

        ax.set_xlim(min_x - 0.1 * extent_x, max_x + 0.1 * extent_x)
        ax.set_ylim(min_y - 0.1 * extent_y, max_y + 0.1 * extent_y)
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        plt.grid(visible=True)
        if show:
            plt.show()

Prism

Bases: Geometry3DBase

A prism is a 2D Polygon extruded along a axis direction ('x', 'y', 'z').

Source code in src/meow/geometries.py
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
class Prism(Geometry3DBase):
    """A prism is a 2D Polygon extruded along a axis direction ('x', 'y', 'z')."""

    poly: Annotated[NDArray, Shape(-1, 2), DType("float64")] = Field(
        description="the 2D array (Nx2) with polygon vertices"
    )
    h_min: float = Field(description="the start height of the extrusion")
    h_max: float = Field(description="the end height of the extrusion")
    axis: AxisDirection = Field(
        default="y",
        description="axis along which the polygon will be extruded ('x', 'y', or 'z').",
    )

    def _project_axis_x(self, z: float) -> list[Geometry2DBase]:
        # x, y, z -> y, z, x
        poly = sg.Polygon(self.poly)
        y_min, _ = self.poly.min(0)
        y_max, _ = self.poly.max(0)
        line = sg.LineString([(y_min, z), (y_max, z)])
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=RuntimeWarning, module="shapely")
            intersections = poly.intersection(line)

        if not isinstance(intersections, sg.MultiLineString):
            intersection_array = np.asarray(intersections.coords)
            if not intersection_array.shape[0]:
                return []
            intersections = sg.MultiLineString([cast(sg.LineString, intersections)])

        geoms_2d = []
        for intersection in intersections.geoms:
            intersection = np.asarray(intersection.coords)
            if not intersection.shape[0]:
                continue
            (y_min, _), (y_max, _) = intersection
            y_min, y_max = min(y_min, y_max), max(y_min, y_max)
            x_min, x_max = min(self.h_min, self.h_max), max(self.h_min, self.h_max)
            rect = Rectangle(
                x_min=x_min,
                x_max=x_max,
                y_min=y_min,
                y_max=y_max,
            )
            geoms_2d.append(rect)
        return geoms_2d

    def _project_axis_y(self, z: float) -> list[Geometry2DBase]:
        # x, y, z -> z, x, y
        poly = sg.Polygon(self.poly)
        _, x_min = self.poly.min(0)
        _, x_max = self.poly.max(0)
        line = sg.LineString([(z, x_min), (z, x_max)])
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=RuntimeWarning, module="shapely")
            intersections = poly.intersection(line)

        if not isinstance(intersections, sg.MultiLineString):
            intersection_array = np.asarray(intersections.coords)
            if not intersection_array.shape[0]:
                return []
            intersections = sg.MultiLineString([cast(sg.LineString, intersections)])

        geoms_2d = []
        for intersection in intersections.geoms:
            intersection = np.asarray(intersection.coords)
            if not intersection.shape[0]:
                continue
            (_, x_min), (_, x_max) = intersection
            x_min, x_max = min(x_min, x_max), max(x_min, x_max)
            y_min, y_max = min(self.h_min, self.h_max), max(self.h_min, self.h_max)
            rect = Rectangle(
                x_min=x_min,
                x_max=x_max,
                y_min=y_min,
                y_max=y_max,
            )
            geoms_2d.append(rect)
        return geoms_2d

    def _project_axis_z(self, z: float) -> list[Geometry2DBase]:
        # x, y, z -> x, y, z
        if z < self.h_min or z < self.h_max:
            return []
        return [Polygon2D(poly=self.poly)]

    def _project(self, z: float) -> list[Geometry2DBase]:
        if self.axis == "x":
            return self._project_axis_x(z)
        if self.axis == "y":
            return self._project_axis_y(z)
        return self._project_axis_z(z)

    def _lumadd(
        self,
        sim: Any,
        material_name: str,
        mesh_order: int,
        unit: float = 1e-6,
        xyz: str = "yzx",
    ) -> None:
        name = token_hex(4)

        if xyz not in ("xyz", "yzx", "zxy"):
            msg = (
                "Prism axes should be positively oriented when adding to Lumerical. "
                f"Got: {xyz!r}"
            )
            raise ValueError(msg)

        sim.addpoly(
            name=name,
            material=material_name,
            override_mesh_order_from_material_database=True,
            mesh_order=mesh_order,
            use_relative_coordinates=True,
            vertices=np.asarray(self.poly, float) * float(unit),
            x=0.0,
            y=0.0,
            z_min=float(self.h_min * unit),
            z_max=float(self.h_max * unit),
        )

        x, y, z = xyz
        if self.axis == "x":
            z, x, y = x, y, z
        elif self.axis == "y":
            y, z, x = x, y, z
        xyz = f"{x}{y}{z}"

        if xyz in ["yzx", "zxy"]:
            msg = (
                "Only prisms extruded perpendicular to the 'chip surface' "
                "are currently supported in Lumerical."
            )
            raise NotImplementedError(msg)

    def _trimesh(
        self, color: str | None = None, scale: tuple[float, float, float] | None = None
    ) -> Any:
        from trimesh.creation import extrude_polygon  # fmt: skip

        poly = sg.Polygon(self.poly)
        prism = extrude_polygon(poly, self.h_max - self.h_min)
        prism = prism.apply_translation((0, 0, self.h_min))
        if self.axis == "x":
            prism.vertices = np.roll(prism.vertices, shift=1, axis=1)
        if self.axis == "y":
            prism.vertices = np.roll(prism.vertices, shift=-1, axis=1)
        if scale is not None:
            sx, sy, sz = scale
            prism.vertices *= np.array([[sx, sy, sz]])
        if color is not None:
            prism.visual.face_colors = _to_rgba(color)  # type: ignore[invalid-assignment]
        return prism

    def _center(self) -> tuple[float, float, float]:
        import shapely.geometry as sg  # fmt: skip

        a, b = np.array(sg.Polygon(self.poly).centroid.xy).ravel()
        c = 0.5 * (self.h_min + self.h_max)
        x, y, z = {
            "x": (c, a, b),
            "y": (b, c, a),
            "z": (a, b, c),
        }[self.axis]
        return x, y, z

    def _axis_tuple(self) -> tuple[int, int, int]:
        return {
            "x": (1, 0, 0),
            "y": (0, 1, 0),
            "z": (0, 0, 1),
        }[self.axis]

Rectangle

Bases: Geometry2DBase

A Rectangle.

Source code in src/meow/geometries.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
class Rectangle(Geometry2DBase):
    """A Rectangle."""

    x_min: float = Field(description="the minimum x-value of the box")
    x_max: float = Field(description="the maximum x-value of the box")
    y_min: float = Field(description="the minimum y-value of the box")
    y_max: float = Field(description="the maximum y-value of the box")

    def _mask(self, X: FloatArray2D, Y: FloatArray2D) -> BoolArray2D:
        mask = (
            (self.x_min <= X)
            & (X <= self.x_max)
            & (self.y_min <= Y)
            & (Y <= self.y_max)
        )
        return mask

    def _shapely_polygon(self) -> sg.Polygon:
        return sg.box(self.x_min, self.y_min, self.x_max, self.y_max)

    def _visualize(
        self,
        *,
        ax: Any = None,
        show: bool = True,
        color: str | None = None,
        **_: Any,
    ) -> None:
        if ax is None:
            ax = plt.gca()
        if color is None:
            color = "grey"
        width = self.x_max - self.x_min
        height = self.y_max - self.y_min
        mpl_rect = MplRect(
            xy=(self.x_min, self.y_min),
            width=width,
            height=height,
            color=color,
        )
        ax.add_patch(mpl_rect)
        ax.set_xlim(self.x_min - 0.1 * width, self.x_max + 0.1 * width)
        ax.set_ylim(self.y_min - 0.1 * width, self.y_max + 0.1 * width)
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        plt.grid(visible=True)
        if show:
            plt.show()

SampledMaterial

Bases: MaterialBase

A material with a sampled refractive index.

Source code in src/meow/materials.py
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
class SampledMaterial(MaterialBase):
    """A material with a sampled refractive index."""

    # TODO: use the new sax xarray interpolation

    n: Annotated[NDArray, Dim(1), DType("complex128")] = Field(
        description="the complex refractive index of the material"
    )
    params: dict[str, Annotated[NDArray, Dim(1), DType("float64")]] = Field(
        description="the wavelength over which the refractive index is defined."
    )

    @staticmethod
    def _validate_1d(name: str, arr: np.ndarray) -> np.ndarray:
        if arr.ndim != 1:
            msg = f"{name} should be 1D. Got a {arr.ndim}D array."
            raise ValueError(msg)
        return arr

    @model_validator(mode="after")
    def _validate_params_length(self: Self) -> Self:
        Ln = self.n.shape[0]
        for p, v in self.params.items():
            Lp = v.shape[0]
            if Lp != Ln:
                msg = (
                    f"length of parameter array {p} does not match length "
                    f"of refractive index array n. \n {Lp} != {Ln}"
                )
                raise ValueError(msg)
        return self

    @classmethod
    def from_path(cls, path: str, meta: dict | None = None) -> Self:
        """Create a SampledMaterial from a CSV file."""
        path = _validate_path(path)
        name = re.sub(r"\.csv$", "", os.path.split(path)[-1])
        df = pd.read_csv(path)
        return cls.from_df(name, df, meta=meta)

    @classmethod
    def from_df(cls, name: str, df: pd.DataFrame, meta: dict | None = None) -> Self:
        """Create a SampledMaterial from a DataFrame."""
        meta = meta or {}

        nr = df["nr"].to_numpy()
        ni = np.zeros_like(nr) if "ni" not in df else df["ni"].to_numpy()
        n = nr + 1j * ni

        columns = [c for c in df.columns if c not in ["nr", "ni"]]
        params = {c: np.asarray(df[c].values, dtype=np.float64) for c in columns}

        return cls(name=name, params=params, n=n, meta=meta)

    def __call__(self, env: Environment) -> np.ndarray:
        """Get the refractive index of the material for the given environment."""
        if not isinstance(env, Environment):
            env = Environment(**env)
        df = pd.DataFrame({**self.params, "nr": np.real(self.n), "ni": np.imag(self.n)})
        data, params, strings = _to_ndgrid(df, wl_key="wl")
        result, axs, pos = _evaluate_general_corner_model(
            data,
            params,
            strings,
            **{k: np.atleast_1d(v) for k, v in env.model_dump().items()},
        )
        nr = result.take(pos["targets"]["nr"], axs["targets"])
        ni = result.take(pos["targets"]["ni"], axs["targets"])
        n = nr + 1j * ni
        return np.squeeze(n)

__call__(env)

Get the refractive index of the material for the given environment.

Source code in src/meow/materials.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def __call__(self, env: Environment) -> np.ndarray:
    """Get the refractive index of the material for the given environment."""
    if not isinstance(env, Environment):
        env = Environment(**env)
    df = pd.DataFrame({**self.params, "nr": np.real(self.n), "ni": np.imag(self.n)})
    data, params, strings = _to_ndgrid(df, wl_key="wl")
    result, axs, pos = _evaluate_general_corner_model(
        data,
        params,
        strings,
        **{k: np.atleast_1d(v) for k, v in env.model_dump().items()},
    )
    nr = result.take(pos["targets"]["nr"], axs["targets"])
    ni = result.take(pos["targets"]["ni"], axs["targets"])
    n = nr + 1j * ni
    return np.squeeze(n)

from_df(name, df, meta=None) classmethod

Create a SampledMaterial from a DataFrame.

Source code in src/meow/materials.py
158
159
160
161
162
163
164
165
166
167
168
169
170
@classmethod
def from_df(cls, name: str, df: pd.DataFrame, meta: dict | None = None) -> Self:
    """Create a SampledMaterial from a DataFrame."""
    meta = meta or {}

    nr = df["nr"].to_numpy()
    ni = np.zeros_like(nr) if "ni" not in df else df["ni"].to_numpy()
    n = nr + 1j * ni

    columns = [c for c in df.columns if c not in ["nr", "ni"]]
    params = {c: np.asarray(df[c].values, dtype=np.float64) for c in columns}

    return cls(name=name, params=params, n=n, meta=meta)

from_path(path, meta=None) classmethod

Create a SampledMaterial from a CSV file.

Source code in src/meow/materials.py
150
151
152
153
154
155
156
@classmethod
def from_path(cls, path: str, meta: dict | None = None) -> Self:
    """Create a SampledMaterial from a CSV file."""
    path = _validate_path(path)
    name = re.sub(r"\.csv$", "", os.path.split(path)[-1])
    df = pd.read_csv(path)
    return cls.from_df(name, df, meta=meta)

SerializedArray

Bases: BaseModel

A serialized representation of a numpy array.

Source code in src/meow/arrays.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class SerializedArray(BaseModel):
    """A serialized representation of a numpy array."""

    values: list[Any]
    shape: tuple[int, ...]
    dtype: str

    @classmethod
    def from_array(cls, x: np.ndarray) -> Self:
        """Create a SerializedArray from a numpy array."""
        x = np.asarray(x)
        shape = x.shape
        dtype = str(x.dtype)
        if dtype == "complex64":
            _x = x.ravel().view("float32")
        elif dtype == "complex128":
            _x = x.ravel().view("float64")
        else:
            _x = x.ravel()
        return cls(shape=shape, dtype=dtype, values=_x.tolist())

    def to_array(self) -> np.ndarray:
        """Convert the SerializedArray back to a numpy array."""
        if self.dtype == "complex128":
            arr = np.asarray(self.values, dtype="float64").view("complex128")
        elif self.dtype == "complex64":
            arr = np.asarray(self.values, dtype="float32").view("complex64")
        else:
            arr = np.asarray(self.values, dtype=self.dtype)

        if not self.shape:
            return arr
        return arr.reshape(*self.shape)

from_array(x) classmethod

Create a SerializedArray from a numpy array.

Source code in src/meow/arrays.py
26
27
28
29
30
31
32
33
34
35
36
37
38
@classmethod
def from_array(cls, x: np.ndarray) -> Self:
    """Create a SerializedArray from a numpy array."""
    x = np.asarray(x)
    shape = x.shape
    dtype = str(x.dtype)
    if dtype == "complex64":
        _x = x.ravel().view("float32")
    elif dtype == "complex128":
        _x = x.ravel().view("float64")
    else:
        _x = x.ravel()
    return cls(shape=shape, dtype=dtype, values=_x.tolist())

to_array()

Convert the SerializedArray back to a numpy array.

Source code in src/meow/arrays.py
40
41
42
43
44
45
46
47
48
49
50
51
def to_array(self) -> np.ndarray:
    """Convert the SerializedArray back to a numpy array."""
    if self.dtype == "complex128":
        arr = np.asarray(self.values, dtype="float64").view("complex128")
    elif self.dtype == "complex64":
        arr = np.asarray(self.values, dtype="float32").view("complex64")
    else:
        arr = np.asarray(self.values, dtype=self.dtype)

    if not self.shape:
        return arr
    return arr.reshape(*self.shape)

Structure2D

Bases: BaseModel

A Structure2D is an association between a Geometry2D and a Material.

Source code in src/meow/structures.py
61
62
63
64
65
66
67
68
69
70
71
72
class Structure2D(BaseModel):
    """A `Structure2D` is an association between a `Geometry2D` and a `Material`."""

    material: Material = Field(description="the material of the structure")
    geometry: Geometry2D = Field(description="the geometry of the structure")
    mesh_order: int = Field(
        default=DEFAULT_MESH_ORDER, description="the mesh order of the structure"
    )

    def _visualize(self, **_: Any) -> None:
        color = self.material.meta.get("color", None)
        return self.geometry._visualize(color=color)

Structure3D

Bases: BaseModel

A Structure3D is an association between a Geometry3D and a Material.

Source code in src/meow/structures.py
 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
class Structure3D(BaseModel):
    """A `Structure3D` is an association between a `Geometry3D` and a `Material`."""

    material: Material = Field(description="the material of the structure")
    geometry: Geometry3D = Field(description="the geometry of the structure")
    mesh_order: int = Field(
        default=DEFAULT_MESH_ORDER, description="the mesh order of the structure"
    )

    def _project(self, z: float) -> list[Structure2D]:
        geometry_2d = self.geometry._project(z)
        structs = []
        for geom in geometry_2d:
            struct = Structure2D(
                material=self.material,
                geometry=geom,
                mesh_order=self.mesh_order,
            )
            structs.append(struct)
        return structs

    def _lumadd(
        self, sim: Any, env: Environment, unit: float = 1e-6, xyz: str = "yzx"
    ) -> None:
        material_name = self.material._lumadd(sim, env, unit)
        self.geometry._lumadd(sim, material_name, self.mesh_order, unit, xyz)

    def _trimesh(
        self, color: str | None = None, scale: tuple[float, float, float] | None = None
    ) -> Any:
        return self.geometry._trimesh(
            color=(color or self.material.meta.get("color")),
            scale=scale,
        )

    def _visualize(
        self,
        scale: tuple[float, float, float] | None = None,
        **_: Any,
    ) -> None:
        return self._trimesh(scale=scale).show()

TidyMaterial

Bases: MaterialBase

A material from the Tidy3D material library.

Source code in src/meow/materials.py
 61
 62
 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
class TidyMaterial(MaterialBase):
    """A material from the Tidy3D material library."""

    name: str = Field(description="The material name as also used by tidy3d")
    variant: str = Field(description="The material variant as also used by tidy3d")

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the TidyMaterial."""
        super().__init__(**kwargs)

        if self.name not in material_library:
            msg = (
                "Specified material name is invalid. "
                f"Use one of {material_library.keys()}"
            )
            raise ValueError(msg)
        _material = material_library[self.name]
        _variants = getattr(_material, "variants", {})
        if not _variants:
            msg = f"Tidy3D material '{self.name}' not supported."
            raise ValueError(msg)
        if self.variant not in _variants:
            _variant_options = list(_variants.keys())
            msg = f"Specified variant is invalid. Use one of {_variant_options}."
            raise ValueError(msg)

    def __call__(self, env: Environment) -> np.ndarray:
        """Get the refractive index of the material for the given environment."""
        if not isinstance(env, Environment):
            env = Environment(**env)
        _material = material_library[self.name]
        _variants = getattr(_material, "variants", None)
        if _variants is None:
            msg = f"Tidy3D material '{self.name}' not supported."
            raise ValueError(msg)
        mat = _material[self.variant]
        eps_comp = getattr(mat, "eps_comp", None)
        if eps_comp is None:
            msg = (
                f"Tidy3D material '{self.name}' variant '{self.variant}' "
                "does not have a permittivity function."
            )
            raise ValueError(msg)
        eps = eps_comp(0, 0, td.C_0 / env.wl)
        return np.sqrt(eps)

__call__(env)

Get the refractive index of the material for the given environment.

Source code in src/meow/materials.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def __call__(self, env: Environment) -> np.ndarray:
    """Get the refractive index of the material for the given environment."""
    if not isinstance(env, Environment):
        env = Environment(**env)
    _material = material_library[self.name]
    _variants = getattr(_material, "variants", None)
    if _variants is None:
        msg = f"Tidy3D material '{self.name}' not supported."
        raise ValueError(msg)
    mat = _material[self.variant]
    eps_comp = getattr(mat, "eps_comp", None)
    if eps_comp is None:
        msg = (
            f"Tidy3D material '{self.name}' variant '{self.variant}' "
            "does not have a permittivity function."
        )
        raise ValueError(msg)
    eps = eps_comp(0, 0, td.C_0 / env.wl)
    return np.sqrt(eps)

__init__(**kwargs)

Initialize the TidyMaterial.

Source code in src/meow/materials.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def __init__(self, **kwargs: Any) -> None:
    """Initialize the TidyMaterial."""
    super().__init__(**kwargs)

    if self.name not in material_library:
        msg = (
            "Specified material name is invalid. "
            f"Use one of {material_library.keys()}"
        )
        raise ValueError(msg)
    _material = material_library[self.name]
    _variants = getattr(_material, "variants", {})
    if not _variants:
        msg = f"Tidy3D material '{self.name}' not supported."
        raise ValueError(msg)
    if self.variant not in _variants:
        _variant_options = list(_variants.keys())
        msg = f"Specified variant is invalid. Use one of {_variant_options}."
        raise ValueError(msg)

DType(dtype, *, coerce=True)

Validator to ensure the array has a specific dtype.

Parameters:

Name Type Description Default
dtype str

the required data type as a string (e.g. "float64").

required
coerce bool

if True, cast the array to the dtype; if False, raise on mismatch.

True

Returns:

Type Description
AfterValidator

A pydantic AfterValidator that checks or coerces the array dtype.

Source code in src/meow/arrays.py
147
148
149
150
151
152
153
154
155
156
157
158
def DType(dtype: str, *, coerce: bool = True) -> AfterValidator:  # noqa: N802
    """Validator to ensure the array has a specific dtype.

    Args:
        dtype: the required data type as a string (e.g. "float64").
        coerce: if True, cast the array to the dtype; if False, raise on mismatch.

    Returns:
        A pydantic AfterValidator that checks or coerces the array dtype.
    """
    f = _coerce_dtype if coerce else _assert_dtype
    return AfterValidator(partial(f, dtype=dtype))

Dim(ndim, *, coerce=True)

Validator to ensure the array has a specific number of dimensions.

Parameters:

Name Type Description Default
ndim int

the required number of dimensions.

required
coerce bool

if True, reshape the array to match; if False, raise on mismatch.

True

Returns:

Type Description
AfterValidator

A pydantic AfterValidator that checks or coerces the array dimensions.

Source code in src/meow/arrays.py
133
134
135
136
137
138
139
140
141
142
143
144
def Dim(ndim: int, *, coerce: bool = True) -> AfterValidator:  # noqa: N802
    """Validator to ensure the array has a specific number of dimensions.

    Args:
        ndim: the required number of dimensions.
        coerce: if True, reshape the array to match; if False, raise on mismatch.

    Returns:
        A pydantic AfterValidator that checks or coerces the array dimensions.
    """
    f = _coerce_dim if coerce else _assert_dim
    return AfterValidator(partial(f, ndim=ndim))

Shape(*shape, coerce=True)

Validator to ensure the array has a specific shape.

Parameters:

Name Type Description Default
*shape int

the required dimensions of the array.

()
coerce bool

if True, reshape the array to match; if False, raise on mismatch.

True

Returns:

Type Description
AfterValidator

A pydantic AfterValidator that checks or coerces the array shape.

Source code in src/meow/arrays.py
161
162
163
164
165
166
167
168
169
170
171
172
def Shape(*shape: int, coerce: bool = True) -> AfterValidator:  # noqa: N802
    """Validator to ensure the array has a specific shape.

    Args:
        *shape: the required dimensions of the array.
        coerce: if True, reshape the array to match; if False, raise on mismatch.

    Returns:
        A pydantic AfterValidator that checks or coerces the array shape.
    """
    f = _coerce_shape if coerce else _assert_shape
    return AfterValidator(partial(f, shape=shape))

Structure(*, material, geometry, mesh_order=DEFAULT_MESH_ORDER)

Structure(
    *,
    material: Material,
    geometry: Geometry2D,
    mesh_order: int = DEFAULT_MESH_ORDER,
) -> Structure2D
Structure(
    *,
    material: Material,
    geometry: Geometry3D,
    mesh_order: int = DEFAULT_MESH_ORDER,
) -> Structure3D

Create a Structure from a Material and Geometry.

Parameters:

Name Type Description Default
material Material

the material of the structure.

required
geometry Geometry2D | Geometry3D

a 2D or 3D geometry for the structure.

required
mesh_order int

the mesh order used for rasterization priority.

DEFAULT_MESH_ORDER

Returns:

Type Description
Structure2D | Structure3D

A Structure2D if geometry is 2D, otherwise a Structure3D.

Source code in src/meow/structures.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def Structure(  # noqa: N802
    *,
    material: Material,
    geometry: Geometry2D | Geometry3D,
    mesh_order: int = DEFAULT_MESH_ORDER,
) -> Structure2D | Structure3D:
    """Create a Structure from a Material and Geometry.

    Args:
        material: the material of the structure.
        geometry: a 2D or 3D geometry for the structure.
        mesh_order: the mesh order used for rasterization priority.

    Returns:
        A Structure2D if geometry is 2D, otherwise a Structure3D.
    """
    kwargs = {
        "material": material,
        "geometry": geometry,
        "mesh_order": mesh_order,
    }
    if isinstance(geometry, Geometry2D):
        return Structure2D(**kwargs)
    return Structure3D(**kwargs)

cache(prop)

Decorator to cache the result of a property method.

Parameters:

Name Type Description Default
prop Callable

the property method whose result should be cached.

required

Returns:

Type Description
Callable

A wrapped callable that caches and returns the computed value.

Source code in src/meow/base_model.py
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
def cache(prop: Callable) -> Callable:
    """Decorator to cache the result of a property method.

    Args:
        prop: the property method whose result should be cached.

    Returns:
        A wrapped callable that caches and returns the computed value.
    """
    prop_name = getattr(prop, "__name__", "")
    if not prop_name:
        return prop

    @wraps(prop)
    def getter(self):  # noqa: ANN001,ANN202
        stored_value = self._cache.get(prop_name)

        if stored_value is not None:
            return stored_value

        computed = prop(self)
        self._cache[prop_name] = computed
        return computed

    return getter

cached_property(method)

Decorator to cache the result of a property method.

Parameters:

Name Type Description Default
method Callable

the method to wrap as a cached property.

required

Returns:

Type Description

A property descriptor that caches the method's return value.

Source code in src/meow/base_model.py
210
211
212
213
214
215
216
217
218
219
def cached_property(method: Callable):  # noqa: ANN201
    """Decorator to cache the result of a property method.

    Args:
        method: the method to wrap as a cached property.

    Returns:
        A property descriptor that caches the method's return value.
    """
    return property(cache(method))

compute_interface_s_matrices(modes, *, inner_product=inner_product, conjugate=None, tsvd_rcond=0.001, passivity_method='invert', enforce_reciprocity=True, ignore_warnings=True)

Compute interface S-matrices for each adjacent pair of mode sets.

This is a thin wrapper around :func:compute_interface_s_matrix applied to every neighboring pair in modes.

The same sharp edges apply here as for the single-interface solve: orthonormalization, inner-product choice, TSVD cutoff, and passivity method must all be interpreted consistently across the full stack.

Parameters:

Name Type Description Default
modes list[Modes]

Ordered list of modal bases across the stack.

required
inner_product Callable

Inner-product callable for forming overlap matrices.

inner_product
conjugate bool | None

Whether to use the conjugated formulation. Inferred from inner_product if None.

None
tsvd_rcond float

Relative singular-value cutoff for the TSVD solve.

0.001
passivity_method PassivityMethod

Method for enforcing S-matrix passivity.

'invert'
enforce_reciprocity bool

Whether to symmetrize each interface S-matrix.

True
ignore_warnings bool

Whether to suppress numerical warnings.

True

Returns:

Type Description
dict[str, SDenseMM]

Dict mapping interface names (e.g. "i_0_1") to SAX dense

dict[str, SDenseMM]

multimode S-matrix tuples.

Source code in src/meow/eme/interface.py
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
def compute_interface_s_matrices(
    modes: list[Modes],
    *,
    inner_product: Callable = inner_product,
    conjugate: bool | None = None,
    tsvd_rcond: float = 1e-3,
    passivity_method: PassivityMethod = "invert",
    enforce_reciprocity: bool = True,
    ignore_warnings: bool = True,
) -> dict[str, sax.SDenseMM]:
    """Compute interface S-matrices for each adjacent pair of mode sets.

    This is a thin wrapper around :func:`compute_interface_s_matrix` applied to
    every neighboring pair in ``modes``.

    The same sharp edges apply here as for the single-interface solve:
    orthonormalization, inner-product choice, TSVD cutoff, and passivity method
    must all be interpreted consistently across the full stack.

    Args:
        modes: Ordered list of modal bases across the stack.
        inner_product: Inner-product callable for forming overlap matrices.
        conjugate: Whether to use the conjugated formulation. Inferred from
            inner_product if None.
        tsvd_rcond: Relative singular-value cutoff for the TSVD solve.
        passivity_method: Method for enforcing S-matrix passivity.
        enforce_reciprocity: Whether to symmetrize each interface S-matrix.
        ignore_warnings: Whether to suppress numerical warnings.

    Returns:
        Dict mapping interface names (e.g. ``"i_0_1"``) to SAX dense
        multimode S-matrix tuples.
    """
    return {
        f"i_{i}_{i + 1}": compute_interface_s_matrix(
            modes1=modes1,
            modes2=modes2,
            inner_product=inner_product,
            conjugate=conjugate,
            tsvd_rcond=tsvd_rcond,
            passivity_method=passivity_method,
            enforce_reciprocity=enforce_reciprocity,
            ignore_warnings=ignore_warnings,
        )
        for i, (modes1, modes2) in enumerate(pairwise(modes))
    }

compute_interface_s_matrix(modes1, modes2, *, inner_product=inner_product, conjugate=None, tsvd_rcond=0.001, passivity_method='invert', enforce_reciprocity=True, ignore_warnings=True)

Compute the interface S-matrix between two modal bases.

This implements the overlap-based EME interface solve for a step discontinuity. The transmission blocks are obtained from a TSVD-regularized solve, then the reflection blocks are reconstructed from the two continuity equations and averaged:

  • R_LL = 0.5 * ((O_RL^adj @ T_LR - I) + (I - O_LR @ T_LR))
  • R_RR = 0.5 * ((O_LR^adj @ T_RL - I) + (I - O_RL @ T_RL))
High-level assumptions
  • the supplied mode sets are already orthonormalized in the same metric used by inner_product;
  • the interface formulas are therefore used in their simplified G = I form;
  • any remaining non-passivity is treated as a numerical/truncation issue and corrected afterwards via singular-value processing.
Sharp edges
  • If modes were orthonormalized with a different inner product than the one passed here, the result is not physically meaningful.
  • conjugate must match the intended overlap convention. If omitted, it is inferred from inner_product.
  • tsvd_rcond controls a stability/accuracy tradeoff. Too small can amplify ill-conditioned directions; too large can discard physically relevant channels.
  • passivity correction modifies the raw interface solve. This is often necessary for truncated bases, but it is still a model correction, not a proof that the original solve was complete.

Parameters:

Name Type Description Default
modes1 Modes

Modes on the left side of the interface.

required
modes2 Modes

Modes on the right side of the interface.

required
inner_product Callable

Inner-product callable used to form the overlap matrices. This must be consistent with how the modes were orthonormalized.

inner_product
conjugate bool | None

Whether to use the conjugated (power-conserving) formulation. If None, inferred from the inner_product callable. Must match the conjugate setting used in inner_product for physical consistency: - conjugate=True: uses O_RL.conj().T (Hermitian transpose) - conjugate=False: uses O_RL.T (transpose)

None
tsvd_rcond float

Relative singular-value cutoff for the TSVD solve used to build the transmission blocks.

0.001
passivity_method PassivityMethod

Method for enforcing passivity ("none", "clip", "invert", "subtract").

'invert'
enforce_reciprocity bool

Whether to symmetrize the final S-matrix as 0.5 * (S + S.T).

True
ignore_warnings bool

Whether to suppress numerical warnings.

True

Returns:

Type Description
SDenseMM

A tuple (S, port_map) in SAX dense-matrix format.

Notes

The default path is internally consistent because the default meow.mode.inner_product is the asymmetric, unconjugated overlap and the default mode post-processing uses that same callable. If you change the interface inner product, you should generally use the same callable during mode post-processing.

Source code in src/meow/eme/interface.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
def compute_interface_s_matrix(
    modes1: Modes,
    modes2: Modes,
    *,
    inner_product: Callable = inner_product,
    conjugate: bool | None = None,
    tsvd_rcond: float = 1e-3,
    passivity_method: PassivityMethod = "invert",
    enforce_reciprocity: bool = True,
    ignore_warnings: bool = True,
) -> sax.SDenseMM:
    """Compute the interface S-matrix between two modal bases.

    This implements the overlap-based EME interface solve for a step
    discontinuity. The transmission blocks are obtained from a TSVD-regularized
    solve, then the reflection blocks are reconstructed from the two continuity
    equations and averaged:

    - ``R_LL = 0.5 * ((O_RL^adj @ T_LR - I) + (I - O_LR @ T_LR))``
    - ``R_RR = 0.5 * ((O_LR^adj @ T_RL - I) + (I - O_RL @ T_RL))``

    High-level assumptions:
        - the supplied mode sets are already orthonormalized in the same metric
          used by ``inner_product``;
        - the interface formulas are therefore used in their simplified
          ``G = I`` form;
        - any remaining non-passivity is treated as a numerical/truncation
          issue and corrected afterwards via singular-value processing.

    Sharp edges:
        - If modes were orthonormalized with a different inner product than the
          one passed here, the result is not physically meaningful.
        - ``conjugate`` must match the intended overlap convention. If omitted,
          it is inferred from ``inner_product``.
        - ``tsvd_rcond`` controls a stability/accuracy tradeoff. Too small can
          amplify ill-conditioned directions; too large can discard physically
          relevant channels.
        - passivity correction modifies the raw interface solve. This is often
          necessary for truncated bases, but it is still a model correction, not
          a proof that the original solve was complete.

    Args:
        modes1: Modes on the left side of the interface.
        modes2: Modes on the right side of the interface.
        inner_product: Inner-product callable used to form the overlap matrices.
            This must be consistent with how the modes were orthonormalized.
        conjugate: Whether to use the conjugated (power-conserving) formulation.
            If None, inferred from the inner_product callable. Must match the
            conjugate setting used in inner_product for physical consistency:
            - conjugate=True: uses O_RL.conj().T (Hermitian transpose)
            - conjugate=False: uses O_RL.T (transpose)
        tsvd_rcond: Relative singular-value cutoff for the TSVD solve used to
            build the transmission blocks.
        passivity_method: Method for enforcing passivity
            ("none", "clip", "invert", "subtract").
        enforce_reciprocity: Whether to symmetrize the final S-matrix as
            ``0.5 * (S + S.T)``.
        ignore_warnings: Whether to suppress numerical warnings.

    Returns:
        A tuple ``(S, port_map)`` in SAX dense-matrix format.

    Notes:
        The default path is internally consistent because the default
        ``meow.mode.inner_product`` is the asymmetric, unconjugated overlap and
        the default mode post-processing uses that same callable. If you change
        the interface inner product, you should generally use the same callable
        during mode post-processing.
    """
    if ignore_warnings:
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", message=".*divide by zero.*")
            warnings.filterwarnings("ignore", message=".*overflow encountered.*")
            warnings.filterwarnings("ignore", message=".*invalid value.*")
            return compute_interface_s_matrix(
                modes1=modes1,
                modes2=modes2,
                inner_product=inner_product,
                conjugate=conjugate,
                tsvd_rcond=tsvd_rcond,
                passivity_method=passivity_method,
                enforce_reciprocity=enforce_reciprocity,
                ignore_warnings=False,
            )

    # Infer conjugate from inner_product if not explicitly provided
    if conjugate is None:
        conjugate = _infer_conjugate(inner_product)

    # Supports unequal number of modes on left and right
    N_L, N_R = len(modes1), len(modes2)

    # Overlap matrices: O_LR is (N_L, N_R), O_RL is (N_R, N_L)
    O_LR = overlap_matrix(modes1, modes2, inner_product)
    O_RL = overlap_matrix(modes2, modes1, inner_product)

    I_L = np.eye(N_L)
    I_R = np.eye(N_R)

    # Use Hermitian transpose (.conj().T) for conjugated inner product,
    # plain transpose (.T) for unconjugated inner product.
    # This matches the Lorentz reciprocity relation for each formulation.
    O_RL_adj = O_RL.conj().T if conjugate else O_RL.T
    O_LR_adj = O_LR.conj().T if conjugate else O_LR.T

    # T_LR: (N_R, N_L) — maps N_L left inputs to N_R right outputs
    # Solve: A_LR @ T_LR = 2 * I_L, where A_LR is (N_L, N_R)
    # tsvd_solve(A, B) returns pinv(A) @ B
    # pinv(A_LR) is (N_R, N_L), so result is (N_R, N_L) @ (N_L, N_L) = (N_R, N_L)
    A_LR = O_LR + O_RL_adj  # (N_L, N_R)
    T_LR, *_ = tsvd_solve(A_LR, 2.0 * I_L, rcond=tsvd_rcond)  # (N_R, N_L)

    # T_RL: (N_L, N_R) — maps N_R right inputs to N_L left outputs
    # pinv(A_RL) is (N_L, N_R), so result is (N_L, N_R) @ (N_R, N_R) = (N_L, N_R)
    A_RL = O_RL + O_LR_adj  # (N_R, N_L)
    T_RL, *_ = tsvd_solve(A_RL, 2.0 * I_R, rcond=tsvd_rcond)  # (N_L, N_R)

    # Compute R from both continuity equations and average; this reduces sensitivity
    # to cancellation relative to directly forming (O_RL^adj - O_LR).
    # R_LL: (N_L, N_L)
    R_LL_e = O_RL_adj @ T_LR - I_L  # (N_L, N_R) @ (N_R, N_L) = (N_L, N_L)
    R_LL_h = I_L - O_LR @ T_LR  # (N_L, N_R) @ (N_R, N_L) = (N_L, N_L)
    R_LL = 0.5 * (R_LL_e + R_LL_h)

    # R_RR: (N_R, N_R)
    R_RR_e = O_LR_adj @ T_RL - I_R  # (N_R, N_L) @ (N_L, N_R) = (N_R, N_R)
    R_RR_h = I_R - O_RL @ T_RL  # (N_R, N_L) @ (N_L, N_R) = (N_R, N_R)
    R_RR = 0.5 * (R_RR_e + R_RR_h)

    # Full S-matrix: [a-; b+] = S [a+; b-]
    # Shape: (N_L + N_R, N_L + N_R)
    # Block structure:
    #   [[R_LL (N_L, N_L),  T_RL (N_L, N_R)],
    #    [T_LR (N_R, N_L),  R_RR (N_R, N_R)]]
    S = np.block(
        [
            [R_LL, T_RL],
            [T_LR, R_RR],
        ]
    )

    # Passivity enforcement via SVD
    U, sigma, Vh = np.linalg.svd(S, full_matrices=False)
    sigma_corrected = enforce_passivity(sigma, method=passivity_method)
    S = (U * sigma_corrected) @ Vh

    if enforce_reciprocity:
        S = 0.5 * (S + S.T)

    in_ports = [f"left@{i}" for i in range(N_L)]
    out_ports = [f"right@{i}" for i in range(N_R)]
    port_map = {p: i for i, p in enumerate(in_ports + out_ports)}
    return jnp.asarray(S), port_map

compute_mode_amplitudes(u, v, m, excitation_l, excitation_r)

Solve for the forward and backward modal amplitudes in one cell.

Parameters:

Name Type Description Default
u ComplexArray2D

Cumulative left-to-right S-matrix (dense array).

required
v ComplexArray2D

Cumulative right-to-left S-matrix (dense array).

required
m int

Number of right-side (forward) modes.

required
excitation_l ComplexArray1D

Left boundary excitation vector.

required
excitation_r ComplexArray1D

Right boundary excitation vector.

required

Returns:

Type Description
tuple[ComplexArray1D, ComplexArray1D]

Tuple (forward, backward) of complex amplitude vectors.

Source code in src/meow/eme/propagation.py
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
def compute_mode_amplitudes(
    u: ComplexArray2D,
    v: ComplexArray2D,
    m: int,
    excitation_l: ComplexArray1D,
    excitation_r: ComplexArray1D,
) -> tuple[ComplexArray1D, ComplexArray1D]:
    """Solve for the forward and backward modal amplitudes in one cell.

    Args:
        u: Cumulative left-to-right S-matrix (dense array).
        v: Cumulative right-to-left S-matrix (dense array).
        m: Number of right-side (forward) modes.
        excitation_l: Left boundary excitation vector.
        excitation_r: Right boundary excitation vector.

    Returns:
        Tuple ``(forward, backward)`` of complex amplitude vectors.
    """
    n = u.shape[0] - m
    _, [u21, u22] = split_square_matrix(u, n)
    [v11, v12], _ = split_square_matrix(v, m)

    rhs = u21 @ excitation_l + u22 @ v12 @ excitation_r
    lhs = np.eye(m, dtype=complex) - u22 @ v11
    forward = np.linalg.solve(lhs, rhs)
    backward = v12 @ excitation_r + v11 @ forward
    return forward, backward

compute_modes_lumerical(cs, num_modes=10, unit=1e-06, post_process=post_process_modes, sim=None)

Compute Modes for a given CrossSection (Lumerical backend).

Parameters:

Name Type Description Default
cs CrossSection

the cross-section to solve modes for.

required
num_modes PositiveInt

number of modes to compute.

10
unit float

unit scaling factor (default 1e-6 for micrometres).

1e-06
post_process Callable

callable applied to the raw mode list before returning.

post_process_modes
sim Sim | None

optional Lumerical simulation object; uses the global one if None.

None

Returns:

Type Description
list[Mode]

The computed and post-processed list of modes.

Source code in src/meow/fde/lumerical.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
def compute_modes_lumerical(
    cs: CrossSection,
    num_modes: PositiveInt = 10,
    unit: float = 1e-6,
    post_process: Callable = post_process_modes,
    sim: Sim | None = None,
) -> list[Mode]:
    """Compute ``Modes`` for a given ``CrossSection`` (Lumerical backend).

    Args:
        cs: the cross-section to solve modes for.
        num_modes: number of modes to compute.
        unit: unit scaling factor (default 1e-6 for micrometres).
        post_process: callable applied to the raw mode list before returning.
        sim: optional Lumerical simulation object; uses the global one if None.

    Returns:
        The computed and post-processed list of modes.
    """
    from lumapi import (  # ty: ignore[unresolved-import]
        LumApiError,  # type: ignore[reportMissingImports]
    )

    sim = get_sim(sim=sim)

    if sim is None:
        msg = "Lumerical simulation object not given or found."
        raise RuntimeError(msg)

    cell = cs._cell

    if cell is None:
        msg = (
            "The cross-section does not have a cell defined. "
            "Please define a cell before computing modes."
        )
        raise ValueError(msg)

    _assert_default_mesh_setting(cell.mesh.angle_phi == 0, "angle_phi")
    _assert_default_mesh_setting(cell.mesh.angle_theta == 0, "angle_theta")
    _assert_default_mesh_setting(cell.mesh.bend_radius is None, "bend_radius")

    create_lumerical_geometries(sim, cell.structures, cs.env, unit)

    sim.select("FDE")
    sim.delete()
    pml_settings = {}
    num_pml_y, num_pml_z = 0, 0
    if cell.mesh.num_pml[0] > 0:
        pml_settings.update(
            {
                "y_min_bc": "PML",
                "y_max_bc": "PML",
            }
        )
        num_pml_y = 22  # TODO: allow adjusting these values
    if cell.mesh.num_pml[1] > 0:
        pml_settings.update(
            {
                "z_min_bc": "PML",
                "z_max_bc": "PML",
            }
        )
        num_pml_z = 22  # TODO: allow adjusting these values
    sim.addfde(
        background_index=1.0,
        solver_type="2D X normal",
        x=float(cell.z * unit),
        y_min=float(cell.mesh.x.min() * unit),
        y_max=float(cell.mesh.x.max() * unit),
        z_min=float(cell.mesh.y.min() * unit),
        z_max=float(cell.mesh.y.max() * unit),
        define_y_mesh_by="number of mesh cells",
        define_z_mesh_by="number of mesh cells",
        mesh_cells_y=cell.mesh.x_.shape[0],
        mesh_cells_z=cell.mesh.y_.shape[0],
        **pml_settings,
    )
    # set mesh size again, because PML messes with it:
    if cell.mesh.num_pml[0] > 0:
        sim.setnamed("FDE", "mesh cells y", cell.mesh.x_.shape[0] - num_pml_y)
    if cell.mesh.num_pml[1] > 0:
        sim.setnamed("FDE", "mesh cells z", cell.mesh.y_.shape[0] - num_pml_z)
    sim.setanalysis("number of trial modes", int(num_modes))
    sim.setanalysis("search", "near n")
    sim.setanalysis("use max index", True)  # noqa: FBT003
    sim.setanalysis("wavelength", float(cs.env.wl * unit))
    sim.findmodes()
    modes = []
    for j in range(1, num_modes + 1):
        try:
            mode = _lumerical_fields_to_mode(
                cs=cs,
                lneff=sim.getdata(f"mode{j}", "neff"),
                lEx=sim.getdata(f"mode{j}", "Ex"),
                lEy=sim.getdata(f"mode{j}", "Ey"),
                lEz=sim.getdata(f"mode{j}", "Ez"),
                lHx=sim.getdata(f"mode{j}", "Hx"),
                lHy=sim.getdata(f"mode{j}", "Hy"),
                lHz=sim.getdata(f"mode{j}", "Hz"),
            )
        except LumApiError:
            break
        modes.append(mode)

    modes = sorted(modes, key=lambda m: np.real(m.neff), reverse=True)
    return post_process(modes)

compute_modes_tidy3d(cs, num_modes=10, target_neff=None, precision='double', post_process=post_process_modes)

Compute Modes for a given CrossSection.

Parameters:

Name Type Description Default
cs CrossSection

the cross-section to solve modes for.

required
num_modes PositiveInt

number of modes to compute.

10
target_neff PositiveFloat | None

effective index near which to search for modes.

None
precision Literal['single', 'double']

floating-point precision, "single" or "double".

'double'
post_process Callable

callable applied to the raw mode list before returning.

post_process_modes

Returns:

Type Description
Modes

The computed and post-processed collection of modes.

Source code in src/meow/fde/tidy3d.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 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
def compute_modes_tidy3d(
    cs: CrossSection,
    num_modes: PositiveInt = 10,
    target_neff: PositiveFloat | None = None,
    precision: Literal["single", "double"] = "double",
    post_process: Callable = post_process_modes,
) -> Modes:
    """Compute ``Modes`` for a given ``CrossSection``.

    Args:
        cs: the cross-section to solve modes for.
        num_modes: number of modes to compute.
        target_neff: effective index near which to search for modes.
        precision: floating-point precision, ``"single"`` or ``"double"``.
        post_process: callable applied to the raw mode list before returning.

    Returns:
        The computed and post-processed collection of modes.
    """
    if num_modes < 1:
        msg = "You need to request at least 1 mode."
        raise ValueError(msg)

    od = np.zeros_like(cs.nx)  # off diagonal entry
    eps_cross = [cs.nx**2, od, od, od, cs.ny**2, od, od, od, cs.nz**2]

    if np.isinf(cs.mesh.bend_radius) or np.isnan(cs.mesh.bend_radius):
        bend_radius = None
        bend_axis = None
    else:
        bend_radius = cs.mesh.bend_radius
        bend_axis = cs.mesh.bend_axis

    mode_spec = SimpleNamespace(  # tidy3d.ModeSpec alternative (prevents type checking)
        num_modes=num_modes,
        target_neff=target_neff,
        num_pml=cs.mesh.num_pml,
        filter_pol=None,
        angle_theta=cs.mesh.angle_theta,
        angle_phi=cs.mesh.angle_phi,
        bend_radius=bend_radius,
        precision=precision,
        bend_axis=bend_axis,
        track_freq="central",
        group_index_step=False,
    )

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", message=".*Input has data type int64.*")
        warnings.filterwarnings("ignore", message=".*divide by zero.*")
        warnings.filterwarnings("ignore", message=".*overflow encountered.*")
        warnings.filterwarnings("ignore", message=".*invalid value.*")
        ((Ex, Ey, Ez), (Hx, Hy, Hz)), neffs = (
            x.squeeze()
            for x in _compute_modes(
                eps_cross=eps_cross,
                coords=[cs.mesh.x, cs.mesh.y],
                freq=c / (cs.env.wl * 1e-6),
                mode_spec=mode_spec,
                precision=precision,
                plane_center=cs.mesh.plane_center,
            )[:2]
        )

    if num_modes == 1:
        modes = [
            Mode(
                cs=cs,
                Ex=Ex,
                Ey=Ey,
                Ez=Ez,
                Hx=Hx,
                Hy=Hy,
                Hz=Hz,
                neff=np.asarray(neffs, dtype=np.complex128).item(),
            )
            for _ in range(num_modes)
        ]
    else:  # num_modes > 1
        modes = [
            Mode(
                cs=cs,
                Ex=Ex[..., i],
                Ey=Ey[..., i],
                Ez=Ez[..., i],
                Hx=Hx[..., i],
                Hy=Hy[..., i],
                Hz=Hz[..., i],
                neff=neffs[i],
            )
            for i in range(num_modes)
        ]

    modes = sorted(modes, key=lambda m: float(np.real(m.neff)), reverse=True)
    return post_process(modes)

compute_propagation_s_matrices(modes, cells, *, cell_lengths=None)

Return the propagation S-matrix for every cell in a stack.

Parameters:

Name Type Description Default
modes list[Modes]

Modal basis for each cell.

required
cells list[Cell]

Cells through which the modes propagate.

required
cell_lengths list[float] | None

Optional explicit lengths. If omitted, they are derived from cell.length.

None
Source code in src/meow/eme/propagation.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def compute_propagation_s_matrices(
    modes: list[Modes],
    cells: list[Cell],
    *,
    cell_lengths: list[float] | None = None,
) -> dict[str, sax.SDictMM]:
    """Return the propagation S-matrix for every cell in a stack.

    Args:
        modes: Modal basis for each cell.
        cells: Cells through which the modes propagate.
        cell_lengths: Optional explicit lengths. If omitted, they are derived
            from ``cell.length``.
    """
    if cell_lengths is None:
        if cells is None:
            msg = "Either cells or cell_lengths must be provided."
            raise ValueError(msg)
        if len(cells) != len(modes):
            msg = f"len(cells) != len(modes): {len(cells)} != {len(modes)}"
            raise ValueError(msg)
        cell_lengths = [cell.length for cell in cells]

    if len(cell_lengths) != len(modes):
        msg = f"len(cell_lengths) != len(modes): {len(cell_lengths)} != {len(modes)}"
        raise ValueError(msg)

    return {
        f"p_{i}": compute_propagation_s_matrix(modes_, cell_length=cell_length)
        for i, (modes_, cell_length) in enumerate(zip(modes, cell_lengths, strict=True))
    }

compute_propagation_s_matrix(modes, cell_length)

Return the diagonal propagation S-matrix for one cell.

Each mode acquires a phase exp(2j * pi * neff / wl * cell_length) while propagating through the cell. Backward propagation is mirrored by the bidirectional port mapping in the returned SAX dictionary.

Parameters:

Name Type Description Default
modes Modes

Modal basis for the cell.

required
cell_length float

Physical length of the cell.

required

Returns:

Type Description
SDictMM

SAX dictionary mapping port pairs to complex transmission values.

Source code in src/meow/eme/propagation.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def compute_propagation_s_matrix(modes: Modes, cell_length: float) -> sax.SDictMM:
    """Return the diagonal propagation S-matrix for one cell.

    Each mode acquires a phase ``exp(2j * pi * neff / wl * cell_length)`` while
    propagating through the cell. Backward propagation is mirrored by the
    bidirectional port mapping in the returned SAX dictionary.

    Args:
        modes: Modal basis for the cell.
        cell_length: Physical length of the cell.

    Returns:
        SAX dictionary mapping port pairs to complex transmission values.
    """
    s_dict = {
        (f"left@{i}", f"right@{i}"): jnp.exp(
            2j * jnp.pi * mode.neff / mode.env.wl * cell_length
        )
        for i, mode in enumerate(modes)
    }
    return {**s_dict, **{(p2, p1): v for (p1, p2), v in s_dict.items()}}

compute_s_matrix_sax(modes, *, cells=None, cell_lengths=None, sax_backend='klu', interfaces_fn=compute_interface_s_matrices, propagations_fn=compute_propagation_s_matrices, **_)

Calculate the S-matrix for given sets of modes.

Parameters:

Name Type Description Default
modes list[Modes]

Modal basis for each cell in the stack.

required
cells list[Cell] | None

Cells from which to derive propagation lengths. Either cells or cell_lengths must be provided.

None
cell_lengths list[float] | None

Optional explicit propagation lengths per cell.

None
sax_backend Backend

SAX backend used for circuit evaluation.

'klu'
interfaces_fn Callable

Callable that computes interface S-matrices.

compute_interface_s_matrices
propagations_fn Callable

Callable that computes propagation S-matrices.

compute_propagation_s_matrices

Returns:

Type Description
SDenseMM

A tuple (S, port_map) in SAX dense multimode format.

Source code in src/meow/eme/cascade.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def compute_s_matrix_sax(
    modes: list[Modes],
    *,
    cells: list[Cell] | None = None,
    cell_lengths: list[float] | None = None,
    sax_backend: sax.Backend = "klu",
    interfaces_fn: Callable = compute_interface_s_matrices,
    propagations_fn: Callable = compute_propagation_s_matrices,
    **_: Any,
) -> sax.SDenseMM:
    """Calculate the S-matrix for given sets of modes.

    Args:
        modes: Modal basis for each cell in the stack.
        cells: Cells from which to derive propagation lengths. Either cells
            or cell_lengths must be provided.
        cell_lengths: Optional explicit propagation lengths per cell.
        sax_backend: SAX backend used for circuit evaluation.
        interfaces_fn: Callable that computes interface S-matrices.
        propagations_fn: Callable that computes propagation S-matrices.

    Returns:
        A tuple ``(S, port_map)`` in SAX dense multimode format.
    """
    propagations = propagations_fn(modes, cells, cell_lengths=cell_lengths)
    interfaces = interfaces_fn(modes)
    net = _get_netlist(propagations, interfaces)
    _, analyze_fn, evaluate_fn = circuit_backends[sax_backend]  # type: ignore[reportArgumentType]  # ty: ignore[invalid-assignment]
    # TODO: use analyze_instances instead of manually converting to scoo ?
    net["instances"] = {k: sax.scoo(v) for k, v in net["instances"].items()}
    analyzed = analyze_fn(net["instances"], net["nets"], net["ports"])
    S, port_map = sax.sdense(
        evaluate_fn(
            analyzed,
            instances=net["instances"],
        )
    )
    S = np.asarray(S)

    # final sorting of result:
    current_port_map = {
        (p, int(i)): j
        for (p, i), j in {
            tuple(pm.split("@")): idx for pm, idx in port_map.items()
        }.items()
    }
    desired_port_map = {pm: i for i, pm in enumerate(sorted(current_port_map))}
    idxs = [current_port_map[pm] for pm in desired_port_map]
    S = S[idxs, :][:, idxs]
    port_map = {f"{p}@{m}": v for (p, m), v in desired_port_map.items()}

    return cast(sax.SDenseMM, (S, port_map))

create_cells(structures, mesh, Ls, z_min=0.0)

Create multiple Cell objects with a Mesh and a collection of cell lengths.

Parameters:

Name Type Description Default
structures list[Structure3D]

the 3D structures shared by all cells.

required
mesh Mesh2D | list[Mesh2D]

a single mesh or a list of meshes, one per cell.

required
Ls Annotated[NDArray, Dim(1), DType('float64')]

a 1D array of cell lengths.

required
z_min float

the starting z-coordinate for the first cell.

0.0

Returns:

Type Description
list[Cell]

A list of Cell objects spanning the given lengths.

Source code in src/meow/cell.py
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
def create_cells(
    structures: list[Structure3D],
    mesh: Mesh2D | list[Mesh2D],
    Ls: Annotated[NDArray, Dim(1), DType("float64")],
    z_min: float = 0.0,
) -> list[Cell]:
    """Create multiple `Cell` objects with a `Mesh` and a collection of cell lengths.

    Args:
        structures: the 3D structures shared by all cells.
        mesh: a single mesh or a list of meshes, one per cell.
        Ls: a 1D array of cell lengths.
        z_min: the starting z-coordinate for the first cell.

    Returns:
        A list of Cell objects spanning the given lengths.
    """
    Ls = np.asarray(Ls, float)
    if Ls.ndim != 1:
        msg = f"Ls should be 1D. Got shape: {Ls.shape}."
        raise ValueError(msg)
    if Ls.shape[0] < 0:
        msg = f"length of Ls array should be nonzero. Got: {Ls}."
        raise ValueError(msg)

    meshes = [mesh] * Ls.shape[0] if isinstance(mesh, Mesh2D) else mesh
    if len(Ls) != len(meshes):
        msg = (
            "Number of meshes should correspond to number of lengths (length of Ls). "
            f"Got {len(meshes)} != {len(Ls)}."
        )
        raise ValueError(msg)

    z = np.cumsum(np.concatenate([np.asarray([z_min], float), Ls]))
    cells = [
        Cell(
            structures=structures,
            mesh=mesh,
            z_min=z_min,
            z_max=z_max,
        )
        for mesh, (z_min, z_max) in zip(meshes, pairwise(z), strict=False)
    ]

    return cells

create_lumerical_geometries(sim, structures, env, unit)

Create Lumerical geometries from a list of structures.

Parameters:

Name Type Description Default
sim Sim

the Lumerical simulation object.

required
structures list[Structure3D]

3-D structures to add to the simulation.

required
env Environment

the environment containing material/wavelength info.

required
unit float

unit scaling factor.

required
Source code in src/meow/fde/lumerical.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def create_lumerical_geometries(
    sim: Sim,
    structures: list[Structure3D],
    env: Environment,
    unit: float,
) -> None:
    """Create Lumerical geometries from a list of structures.

    Args:
        sim: the Lumerical simulation object.
        structures: 3-D structures to add to the simulation.
        env: the environment containing material/wavelength info.
        unit: unit scaling factor.
    """
    sim = get_sim(sim=sim)
    sim.switchtolayout()
    sim.deleteall()
    for s in structures:
        s._lumadd(sim, env, unit, "yzx")

downselect_s(S, ports)

Downselect the S-matrix to the given ports.

Parameters:

Name Type Description Default
S SDenseMM

A tuple (S_matrix, port_map) in SAX dense multimode format.

required
ports list[str]

Port names to keep.

required

Returns:

Type Description
SDenseMM

A new (S_matrix, port_map) tuple containing only the selected ports.

Source code in src/meow/eme/cascade.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def downselect_s(S: sax.SDenseMM, ports: list[str]) -> sax.SDenseMM:
    """Downselect the S-matrix to the given ports.

    Args:
        S: A tuple ``(S_matrix, port_map)`` in SAX dense multimode format.
        ports: Port names to keep.

    Returns:
        A new ``(S_matrix, port_map)`` tuple containing only the selected ports.
    """
    S_matrix, port_map = S
    idxs = [port_map[port] for port in ports]
    S_matrix = S_matrix[idxs, :][:, idxs]
    port_map = {port: i for i, port in enumerate(ports)}
    return S_matrix, port_map

electric_energy(mode)

Get the electric energy contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the electric energy of.

required

Returns:

Type Description
float

The total electric energy summed over the cross section.

Source code in src/meow/mode.py
490
491
492
493
494
495
496
497
498
499
def electric_energy(mode: Mode) -> float:
    """Get the electric energy contained in a `Mode`.

    Args:
        mode: the mode to compute the electric energy of.

    Returns:
        The total electric energy summed over the cross section.
    """
    return electric_energy_density(mode).sum()

electric_energy_density(mode)

Get the electric energy density contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the electric energy density of.

required

Returns:

Type Description
ndarray[tuple[int, int], dtype[float64]]

A 2D array of electric energy density values.

Source code in src/meow/mode.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
def electric_energy_density(
    mode: Mode,
) -> np.ndarray[tuple[int, int], np.dtype[np.float64]]:
    """Get the electric energy density contained in a `Mode`.

    Args:
        mode: the mode to compute the electric energy density of.

    Returns:
        A 2D array of electric energy density values.
    """
    epsx, epsy, epsz = mode.cs.nx**2, mode.cs.ny**2, mode.cs.nz**2
    return np.real(
        0.5
        * eps0
        * (
            epsx * np.abs(mode.Ex) ** 2
            + epsy * np.abs(mode.Ey) ** 2
            + epsz * np.abs(mode.Ez) ** 2
        )
    )

energy(mode)

Get the energy contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the energy of.

required

Returns:

Type Description
float

The total energy summed over the cross section.

Source code in src/meow/mode.py
542
543
544
545
546
547
548
549
550
551
def energy(mode: Mode) -> float:
    """Get the energy contained in a `Mode`.

    Args:
        mode: the mode to compute the energy of.

    Returns:
        The total energy summed over the cross section.
    """
    return energy_density(mode).sum()

energy_density(mode)

Get the energy density contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the energy density of.

required

Returns:

Type Description
ndarray[tuple[int, int], dtype[float64]]

A 2D array of total (electric + magnetic) energy density values.

Source code in src/meow/mode.py
530
531
532
533
534
535
536
537
538
539
def energy_density(mode: Mode) -> np.ndarray[tuple[int, int], np.dtype[np.float64]]:
    """Get the energy density contained in a `Mode`.

    Args:
        mode: the mode to compute the energy density of.

    Returns:
        A 2D array of total (electric + magnetic) energy density values.
    """
    return electric_energy_density(mode) + magnetic_energy_density(mode)

enforce_passivity(singular_values, *, method='invert')

Project singular values onto a passive interval.

This function post-processes the singular values of an S-matrix so that the resulting matrix has singular values no larger than one.

Available methods
  • "none": leave the singular values unchanged.
  • "clip": replace values larger than one by exactly one.
  • "invert": replace sigma by 1 / sigma when sigma > 1.
  • "subtract": replace sigma by max(0, 2 - sigma) when sigma > 1.
High-level interpretation

clip is the most conservative algebraic projection. invert and subtract both map modest gain above one to modest loss below one, which can be useful when the excess gain is interpreted as missing radiation channels in a truncated basis.

Sharp edge

These are heuristic corrections on top of a truncated modal solve. They are often useful, but they are not substitutes for a converged basis.

Parameters:

Name Type Description Default
singular_values ndarray

Singular values to correct.

required
method PassivityMethod

Passivity enforcement strategy.

'invert'

Returns:

Type Description
ndarray

Corrected singular values.

Raises:

Type Description
ValueError

If method is unknown.

Source code in src/meow/eme/interface.py
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
def enforce_passivity(
    singular_values: np.ndarray, *, method: PassivityMethod = "invert"
) -> np.ndarray:
    """Project singular values onto a passive interval.

    This function post-processes the singular values of an S-matrix so that the
    resulting matrix has singular values no larger than one.

    Available methods:
        - ``"none"``: leave the singular values unchanged.
        - ``"clip"``: replace values larger than one by exactly one.
        - ``"invert"``: replace ``sigma`` by ``1 / sigma`` when ``sigma > 1``.
        - ``"subtract"``: replace ``sigma`` by ``max(0, 2 - sigma)`` when
          ``sigma > 1``.

    High-level interpretation:
        ``clip`` is the most conservative algebraic projection. ``invert`` and
        ``subtract`` both map modest gain above one to modest loss below one,
        which can be useful when the excess gain is interpreted as missing
        radiation channels in a truncated basis.

    Sharp edge:
        These are heuristic corrections on top of a truncated modal solve. They
        are often useful, but they are not substitutes for a converged basis.

    Args:
        singular_values: Singular values to correct.
        method: Passivity enforcement strategy.

    Returns:
        Corrected singular values.

    Raises:
        ValueError: If ``method`` is unknown.
    """
    match method:
        case "none":
            return singular_values
        case "clip":
            return np.where(singular_values > 1.0, 1.0, singular_values)
        case "invert":
            return np.where(singular_values > 1.0, 1 / singular_values, singular_values)
        case "subtract":
            singular_values = np.where(
                singular_values > 1.0, 2.0 - singular_values, singular_values
            )
            return np.where(singular_values < 0.0, 0.0, singular_values)
        case _:
            msg = f"Unknown passivity enforcement method {method}."
            raise ValueError(msg)

extrude_gds(cell, extrusions)

Extrude a gds cell given a dictionary of extrusion rules.

Parameters:

Name Type Description Default
cell Any

a gdspy or gdstk Cell to extrude.

required
extrusions dict[tuple[int, int], list[GdsExtrusionRule]]

the extrusion rules to use (if not given, the example extrusions will be used).

required

Returns:

Type Description
list[Structure3D]

A list of Structure3D objects created from the extruded polygons.

Source code in src/meow/gds_structures.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def extrude_gds(
    cell: Any,  # gf.Component | gdspy.Cell | gdstk.Cell
    extrusions: dict[tuple[int, int], list[GdsExtrusionRule]],
) -> list[Structure3D]:
    """Extrude a gds cell given a dictionary of extrusion rules.

    Args:
        cell: a gdspy or gdstk Cell to extrude.
        extrusions: the extrusion rules to use
            (if not given, the example extrusions will be used).

    Returns:
        A list of Structure3D objects created from the extruded polygons.
    """
    structs = []
    for layer, polys in _get_polygons(cell).items():
        for poly in polys:
            if layer not in extrusions:
                continue
            structs.extend(extrusion(poly) for extrusion in extrusions[layer])
    return structs

filter_modes(modes, conditions=(is_pml_mode, is_lossy_mode))

Filter a set of modes according to certain criteria.

Parameters:

Name Type Description Default
modes Modes

the list of modes to filter

required
conditions Iterable[Callable]

the conditions to filter the modes with

(is_pml_mode, is_lossy_mode)

Returns:

Type Description
Modes

the filtered modes

Source code in src/meow/fde/post_process.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def filter_modes(
    modes: Modes,
    conditions: Iterable[Callable] = (is_pml_mode, is_lossy_mode),
) -> Modes:
    """Filter a set of modes according to certain criteria.

    Args:
        modes: the list of modes to filter
        conditions: the conditions to filter the modes with

    Returns:
        the filtered modes
    """
    kept = []
    for mode in modes:
        for condition_fn in conditions:
            if condition_fn(mode):
                break
        else:
            kept.append(mode)
    return kept

get_sim(**kwargs)

Get the Lumerical simulation object.

Parameters:

Name Type Description Default
**kwargs Any

keyword arguments; pass sim to set and return a specific simulation object.

{}

Returns:

Type Description
Sim

The active Lumerical simulation object.

Source code in src/meow/fde/lumerical.py
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
def get_sim(**kwargs: Any) -> Sim:
    """Get the Lumerical simulation object.

    Args:
        **kwargs: keyword arguments; pass ``sim`` to set and return a specific
            simulation object.

    Returns:
        The active Lumerical simulation object.
    """
    global _sim  # noqa: PLW0603
    sim = kwargs.get("sim", None)
    if sim is not None:
        _sim = sim
        return sim
    sim = _sim
    if sim is None:
        msg = (
            "Could not start Lumerical simulation. Please either pass the "
            "`lumapi.MODE` simulation object as an argument to "
            "`compute_modes_lumerical` to globally "
            "set the lumapi.MODE simulation object."
        )
        raise ValueError(msg)
    return sim

inner_product(mode1, mode2, *, symmetric=False, conjugate=False, ignore_pml=True, interpolation=None)

The modal inner product for z-normal mode planes.

Parameters:

Name Type Description Default
mode1 Mode

the left mode

required
mode2 Mode

the right mode

required
symmetric bool

use the symmetric inner product definition

False
conjugate bool

use the conjugage inner product definition (power normalization)

False
ignore_pml bool

ignore PML region while taking the inner product

True
interpolation Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'] | None

interpolate both modes to a certain field-grid position before taking the inner product.

None

Returns:

Type Description
complex

the inner product

Source code in src/meow/mode.py
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
def inner_product(
    mode1: Mode,
    mode2: Mode,
    *,
    symmetric: bool = False,
    conjugate: bool = False,
    ignore_pml: bool = True,
    interpolation: Literal["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] | None = None,
) -> complex:
    """The modal inner product for z-normal mode planes.

    Args:
        mode1: the left mode
        mode2: the right mode
        symmetric: use the symmetric inner product definition
        conjugate: use the conjugage inner product definition (power normalization)
        ignore_pml: ignore PML region while taking the inner product
        interpolation: interpolate both modes to a certain field-grid position
            before taking the inner product.

    Returns:
        the inner product
    """
    mode1 = mode1.interpolate(interpolation)
    mode2 = mode2.interpolate(interpolation)

    mesh = mode1.mesh
    x = np.asarray(mesh.x_)
    y = np.asarray(mesh.y_)

    if conjugate:
        ex1, ey1 = mode1.Ex.conj(), mode1.Ey.conj()
        hx1, hy1 = mode1.Hx.conj(), mode1.Hy.conj()
    else:
        ex1, ey1 = mode1.Ex, mode1.Ey
        hx1, hy1 = mode1.Hx, mode1.Hy

    e1_cross_h2 = ex1 * mode2.Hy - ey1 * mode2.Hx
    if symmetric:
        h1_cross_e2 = hx1 * mode2.Ey - hy1 * mode2.Ex
        integrand = 0.25 * (e1_cross_h2 - h1_cross_e2)
    else:
        integrand = 0.5 * e1_cross_h2

    if ignore_pml:
        integrand, x, y = _crop_non_pml(mesh, integrand, x, y)

    arr = np.trapezoid(np.trapezoid(integrand, y), x)
    return float(np.real(arr)) + float(np.imag(arr)) * 1j

invert_mode(mode)

Invert a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to invert.

required

Returns:

Type Description
Mode

A new mode with all field components negated.

Source code in src/meow/mode.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def invert_mode(mode: Mode) -> Mode:
    """Invert a `Mode`.

    Args:
        mode: the mode to invert.

    Returns:
        A new mode with all field components negated.
    """
    return Mode(
        neff=mode.neff,
        cs=mode.cs,
        Ex=-mode.Ex,
        Ey=-mode.Ey,
        Ez=-mode.Ez,
        Hx=-mode.Hx,
        Hy=-mode.Hy,
        Hz=-mode.Hz,
    )

is_lossy_mode(mode, *, threshold=1.0)

Check whether a mode can be considered lossy.

Parameters:

Name Type Description Default
mode Mode

the mode to check.

required
threshold float

imaginary neff value above which the mode is considered lossy.

1.0

Returns:

Type Description
bool

True if the absolute imaginary part of neff exceeds the threshold.

Source code in src/meow/mode.py
620
621
622
623
624
625
626
627
628
629
630
def is_lossy_mode(mode: Mode, *, threshold: float = 1.0) -> bool:
    """Check whether a mode can be considered lossy.

    Args:
        mode: the mode to check.
        threshold: imaginary neff value above which the mode is considered lossy.

    Returns:
        True if the absolute imaginary part of neff exceeds the threshold.
    """
    return bool(abs(np.imag(mode.neff)) > threshold)

is_pml_mode(mode, *, threshold=0.1)

Check whether a mode can be considered a PML mode.

Parameters:

Name Type Description Default
mode Mode

the mode to check.

required
threshold float

PML energy fraction above which the mode is considered a PML mode.

0.1

Returns:

Type Description
bool

True if the PML energy fraction exceeds the threshold.

Source code in src/meow/mode.py
604
605
606
607
608
609
610
611
612
613
614
615
616
617
def is_pml_mode(mode: Mode, *, threshold: float = 0.1) -> bool:
    """Check whether a mode can be considered a PML mode.

    Args:
        mode: the mode to check.
        threshold: PML energy fraction above which the mode is considered a PML mode.

    Returns:
        True if the PML energy fraction exceeds the threshold.
    """
    threshold = min(max(float(threshold), 0.0), 1.0)
    if threshold > 0.999:
        return False
    return abs(pml_fraction(mode)) > threshold

l2r_matrices(pairs, identity, sax_backend)

Return cumulative left-to-right S-matrices.

Parameters:

Name Type Description Default
pairs list[STypeMM]

Propagation-interface pair S-matrices from pi_pairs.

required
identity SDenseMM

Identity-like S-matrix used as the initial accumulator.

required
sax_backend Backend

SAX backend used for cascading.

required

Returns:

Type Description
list[STypeMM]

List of cumulative S-matrices from the left boundary up to each cell.

Source code in src/meow/eme/propagation.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def l2r_matrices(
    pairs: list[sax.STypeMM], identity: sax.SDenseMM, sax_backend: sax.Backend
) -> list[sax.STypeMM]:
    """Return cumulative left-to-right S-matrices.

    Args:
        pairs: Propagation-interface pair S-matrices from ``pi_pairs``.
        identity: Identity-like S-matrix used as the initial accumulator.
        sax_backend: SAX backend used for cascading.

    Returns:
        List of cumulative S-matrices from the left boundary up to each cell.
    """
    matrices: list[sax.STypeMM] = [identity]
    for pair in pairs[:-1]:
        matrices.append(_connect_two(matrices[-1], pair, sax_backend))
    return matrices

magnetic_energy(mode)

Get the magnetic energy contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the magnetic energy of.

required

Returns:

Type Description
float

The total magnetic energy summed over the cross section.

Source code in src/meow/mode.py
518
519
520
521
522
523
524
525
526
527
def magnetic_energy(mode: Mode) -> float:
    """Get the magnetic energy contained in a `Mode`.

    Args:
        mode: the mode to compute the magnetic energy of.

    Returns:
        The total magnetic energy summed over the cross section.
    """
    return magnetic_energy_density(mode).sum()

magnetic_energy_density(mode)

Get the magnetic energy density contained in a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the magnetic energy density of.

required

Returns:

Type Description
ndarray[tuple[int, int], dtype[float64]]

A 2D array of magnetic energy density values.

Source code in src/meow/mode.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
def magnetic_energy_density(
    mode: Mode,
) -> np.ndarray[tuple[int, int], np.dtype[np.float64]]:
    """Get the magnetic energy density contained in a `Mode`.

    Args:
        mode: the mode to compute the magnetic energy density of.

    Returns:
        A 2D array of magnetic energy density values.
    """
    return np.real(
        0.5 * mu0 * (np.abs(mode.Hx) ** 2 + np.abs(mode.Hy) ** 2 + np.abs(mode.Hz) ** 2)
    )

normalize(mode, inner_product)

Normalize a Mode according to the inner_product with itself.

Parameters:

Name Type Description Default
mode Mode

the mode to normalize.

required
inner_product Callable

a callable computing the inner product of two modes.

required

Returns:

Type Description
Mode

A new mode normalized such that its self-inner-product is unity.

Source code in src/meow/mode.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
def normalize(mode: Mode, inner_product: Callable) -> Mode:
    """Normalize a `Mode` according to the `inner_product` with itself.

    Args:
        mode: the mode to normalize.
        inner_product: a callable computing the inner product of two modes.

    Returns:
        A new mode normalized such that its self-inner-product is unity.
    """
    factor = np.sqrt(inner_product(mode, mode))
    return Mode(
        neff=mode.neff,
        cs=mode.cs,
        Ex=mode.Ex / factor,
        Ey=mode.Ey / factor,
        Ez=mode.Ez / factor,
        Hx=mode.Hx / factor,
        Hy=mode.Hy / factor,
        Hz=mode.Hz / factor,
    )

normalize_energy(mode)

Normalize a mode according to the energy it contains.

Parameters:

Name Type Description Default
mode Mode

the mode to normalize.

required

Returns:

Type Description
Mode

A new mode with fields scaled so that electric and magnetic energies are 0.5.

Source code in src/meow/mode.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def normalize_energy(mode: Mode) -> Mode:
    """Normalize a mode according to the energy it contains.

    Args:
        mode: the mode to normalize.

    Returns:
        A new mode with fields scaled so that electric and magnetic energies are 0.5.
    """
    e = np.sqrt(2 * electric_energy(mode))
    h = np.sqrt(2 * magnetic_energy(mode))

    return Mode(
        neff=mode.neff,
        cs=mode.cs,
        Ex=mode.Ex / e,
        Ey=mode.Ey / e,
        Ez=mode.Ez / e,
        Hx=mode.Hx / h,
        Hy=mode.Hy / h,
        Hz=mode.Hz / h,
    )

normalize_modes(modes, inner_product)

Self-normalize a set of modes.

This only fixes the norm of each mode individually. It does not make the mode set mutually orthogonal. For overlap-based interface formulas, use :func:orthonormalize_modes if you need the simplified G = I metric.

Parameters:

Name Type Description Default
modes Modes

the modes to normalize.

required
inner_product Callable

callable computing the inner product between two modes.

required

Returns:

Type Description
Modes

The normalized (and zero-phased) modes.

Source code in src/meow/fde/post_process.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def normalize_modes(modes: Modes, inner_product: Callable) -> Modes:
    """Self-normalize a set of modes.

    This only fixes the norm of each mode individually. It does not make the
    mode set mutually orthogonal. For overlap-based interface formulas, use
    :func:`orthonormalize_modes` if you need the simplified ``G = I`` metric.

    Args:
        modes: the modes to normalize.
        inner_product: callable computing the inner product between two modes.

    Returns:
        The normalized (and zero-phased) modes.
    """
    return [zero_phase(normalize(m, inner_product)) for m in modes]

orthonormalize_modes(modes, inner_product, *, tolerance=0.01)

Gram-Schmidt orthonormalization with drop tolerance.

Parameters:

Name Type Description Default
modes Modes

the modes to orthonormalize

required
inner_product Callable

the inner product to orthonormalize them under

required
tolerance float

any mode that can't expand the basis beyond this tolerance will be dropped.

0.01

Returns:

Type Description
Modes

Orthonormalized mode basis.

Notes

This routine first self-normalizes each mode and then applies Gram-Schmidt. The inner_product passed here should generally be the same one used later to build interface overlaps; otherwise the final basis is orthonormal in the wrong metric.

Source code in src/meow/fde/post_process.py
 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
def orthonormalize_modes(
    modes: Modes,
    inner_product: Callable,
    *,
    tolerance: float = 0.01,
) -> Modes:
    """Gram-Schmidt orthonormalization with drop tolerance.

    Args:
        modes: the modes to orthonormalize
        inner_product: the inner product to orthonormalize them under
        tolerance: any mode that can't expand the basis beyond this tolerance
            will be dropped.

    Returns:
        Orthonormalized mode basis.

    Notes:
        This routine first self-normalizes each mode and then applies
        Gram-Schmidt. The ``inner_product`` passed here should generally be the
        same one used later to build interface overlaps; otherwise the final
        basis is orthonormal in the wrong metric.
    """
    if not modes:
        return []
    modes = normalize_modes(modes, inner_product)
    basis = []
    n_dropped = 0
    for mode in modes:
        current = mode
        for b in basis:
            current = current - (inner_product(b, current) / inner_product(b, b)) * b
        norm_sq = inner_product(current, current)
        if abs(norm_sq) < tolerance:
            n_dropped += 1
            continue
        basis.append(current / np.sqrt(norm_sq))
    return basis

overlap_matrix(modes1, modes2, inner_product)

Build the modal overlap matrix between two mode sets.

The entry M[i, j] is inner_product(modes1[i], modes2[j]). In the interface derivation this is used both for self-overlaps (the metric G) and cross-overlaps between the left and right waveguide bases.

Sharp edge

The meaning of the overlap matrix is entirely determined by the inner_product callable. If modes were orthonormalized with one inner product but overlaps are formed with another, the simplified G = I interface formulas are no longer valid.

Parameters:

Name Type Description Default
modes1 Modes

Left/test mode set.

required
modes2 Modes

Right/expansion mode set.

required
inner_product Callable

Modal inner-product callable.

required

Returns:

Type Description
ndarray[tuple[int, int], dtype[complex128]]

Complex overlap matrix of shape (len(modes1), len(modes2)).

Source code in src/meow/eme/interface.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def overlap_matrix(
    modes1: Modes, modes2: Modes, inner_product: Callable
) -> np.ndarray[tuple[int, int], np.dtype[np.complex128]]:
    """Build the modal overlap matrix between two mode sets.

    The entry ``M[i, j]`` is ``inner_product(modes1[i], modes2[j])``. In the
    interface derivation this is used both for self-overlaps (the metric ``G``)
    and cross-overlaps between the left and right waveguide bases.

    Sharp edge:
        The meaning of the overlap matrix is entirely determined by the
        ``inner_product`` callable. If modes were orthonormalized with one
        inner product but overlaps are formed with another, the simplified
        ``G = I`` interface formulas are no longer valid.

    Args:
        modes1: Left/test mode set.
        modes2: Right/expansion mode set.
        inner_product: Modal inner-product callable.

    Returns:
        Complex overlap matrix of shape ``(len(modes1), len(modes2))``.
    """
    M = np.zeros((len(modes1), len(modes2)), dtype=np.complex128)
    for i, ma in enumerate(modes1):
        for j, mb in enumerate(modes2):
            M[i, j] = inner_product(ma, mb)
    return M

pi_pairs(propagations, interfaces, sax_backend)

Return propagation-interface pairs for a full stack.

Parameters:

Name Type Description Default
propagations dict[str, SDictMM]

Propagation S-matrices keyed by cell index.

required
interfaces dict[str, SDenseMM]

Interface S-matrices keyed by adjacent cell pairs.

required
sax_backend Backend

SAX backend used for cascading.

required

Returns:

Type Description
list[STypeMM]

List of cascaded propagation-interface S-matrices, one per cell.

Source code in src/meow/eme/propagation.py
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
def pi_pairs(
    propagations: dict[str, sax.SDictMM],
    interfaces: dict[str, sax.SDenseMM],
    sax_backend: sax.Backend,
) -> list[sax.STypeMM]:
    """Return propagation-interface pairs for a full stack.

    Args:
        propagations: Propagation S-matrices keyed by cell index.
        interfaces: Interface S-matrices keyed by adjacent cell pairs.
        sax_backend: SAX backend used for cascading.

    Returns:
        List of cascaded propagation-interface S-matrices, one per cell.
    """
    pairs: list[sax.STypeMM] = []
    for i in range(len(propagations)):
        propagation = propagations[f"p_{i}"]
        if i == len(interfaces):
            pairs.append(propagation)
        else:
            pairs.append(
                _connect_two(propagation, interfaces[f"i_{i}_{i + 1}"], sax_backend)
            )
    return pairs

plot_fields(modes, cells, forwards, backwards, y, z)

Reconstruct an Ex(x, z) field slice from propagated modal amplitudes.

Parameters:

Name Type Description Default
modes list[list[Mode]]

Mode sets for each cell.

required
cells list[Cell]

Cells defining the geometry and mesh.

required
forwards list[ComplexArray1D]

Forward amplitude vectors per cell.

required
backwards list[ComplexArray1D]

Backward amplitude vectors per cell.

required
y float

Transverse y-coordinate at which to sample the field.

required
z FloatArray1D

Global z-grid on which to reconstruct the field.

required

Returns:

Type Description
ComplexArray2D

Tuple (field, x) where field is the complex Ex(z, x)

FloatArray1D

array and x is the transverse sampling grid.

Source code in src/meow/eme/propagation.py
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
def plot_fields(
    modes: list[list[Mode]],
    cells: list[Cell],
    forwards: list[ComplexArray1D],
    backwards: list[ComplexArray1D],
    y: float,
    z: FloatArray1D,
) -> tuple[ComplexArray2D, FloatArray1D]:
    """Reconstruct an ``Ex(x, z)`` field slice from propagated modal amplitudes.

    Args:
        modes: Mode sets for each cell.
        cells: Cells defining the geometry and mesh.
        forwards: Forward amplitude vectors per cell.
        backwards: Backward amplitude vectors per cell.
        y: Transverse y-coordinate at which to sample the field.
        z: Global z-grid on which to reconstruct the field.

    Returns:
        Tuple ``(field, x)`` where ``field`` is the complex ``Ex(z, x)``
        array and ``x`` is the transverse sampling grid.
    """
    mesh_y = cells[0].mesh.y
    mesh_x = cells[0].mesh.x
    mesh_x = mesh_x[:-1] + np.diff(mesh_x) / 2
    i_y = np.argmin(np.abs(mesh_y - y))

    e_tot = np.zeros((len(z), len(mesh_x)), dtype=complex)
    for mode_set, forward, backward, cell in zip(
        modes, forwards, backwards, cells, strict=False
    ):
        ex = np.array(0 + 0j)
        i_min = np.argmax(z >= cell.z_min)
        i_max = np.argmax(z > cell.z_max)
        z_ = z[i_min:] if i_max == 0 else z[i_min:i_max]
        z_local = z_ - cell.z_min
        for mode, fwd, bwd in zip(mode_set, forward, backward, strict=False):
            e_slice = mode.Ex[:, i_y]
            ex += jnp.outer(
                fwd * e_slice.T, jnp.exp(2j * np.pi * mode.neff / mode.env.wl * z_local)
            )
            ex += jnp.outer(
                bwd * e_slice.T,
                jnp.exp(-2j * np.pi * mode.neff / mode.env.wl * z_local),
            )

        if i_max == 0:
            e_tot[i_min:] = ex.T
        else:
            e_tot[i_min:i_max] = ex.T

    return e_tot, mesh_x

pml_fraction(mode)

Fraction of energy density in the PML region.

Parameters:

Name Type Description Default
mode Mode

the mode to evaluate.

required

Returns:

Type Description
float

The fraction of total energy density located in the PML region (0.0 to 1.0).

Source code in src/meow/mode.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
def pml_fraction(mode: Mode) -> float:
    """Fraction of energy density in the PML region.

    Args:
        mode: the mode to evaluate.

    Returns:
        The fraction of total energy density located in the PML region (0.0 to 1.0).
    """
    numx, numy = mode.mesh.num_pml
    if numx == numy == 0:
        return 0.0
    ed = energy_density(mode)
    m, n = ed.shape
    lft = ed[:numx, :]
    rgt = ed[m - numx :, :]
    top = ed[numx : m - numx, :numy]
    btm = ed[numx : m - numx, n - numy :]
    rest = ed[numx : m - numx, numy : n - numy]
    pml_sum = lft.sum() + rgt.sum() + top.sum() + btm.sum()
    total = pml_sum + rest.sum()
    # probably propper integration considering
    # the size of the mesh cells would be better here
    return float(pml_sum / total) if total > 0 else 1.0

post_process_modes(modes, inner_product=inner_product, gm_tolerance=0.01)

Default post-processing pipeline after FDE.

Parameters:

Name Type Description Default
modes Modes

the modes to post process

required
inner_product Callable

the inner product with which to post-process the modes

inner_product
gm_tolerance float

Gramm-Schmidt orthonormalization tolerance

0.01

Returns:

Type Description
Modes

Filtered and orthonormalized modes.

Notes

This is the default post_process used by compute_modes. The choice of inner_product here matters downstream: if interface overlaps are later built with a different inner product, the resulting mode basis is no longer orthonormal in the interface metric.

Source code in src/meow/fde/post_process.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def post_process_modes(
    modes: Modes,
    inner_product: Callable = inner_product,
    gm_tolerance: float = 0.01,
) -> Modes:
    """Default post-processing pipeline after FDE.

    Args:
        modes: the modes to post process
        inner_product: the inner product with which to post-process the modes
        gm_tolerance: Gramm-Schmidt orthonormalization tolerance

    Returns:
        Filtered and orthonormalized modes.

    Notes:
        This is the default ``post_process`` used by ``compute_modes``. The
        choice of ``inner_product`` here matters downstream: if interface
        overlaps are later built with a different inner product, the resulting
        mode basis is no longer orthonormal in the interface metric.
    """
    return orthonormalize_modes(
        filter_modes(modes),
        inner_product,
        tolerance=gm_tolerance,
    )

propagate(l2rs, r2ls, excitation_l, excitation_r)

Propagate boundary excitations through cumulative S-matrices.

Parameters:

Name Type Description Default
l2rs list[STypeMM]

Cumulative left-to-right S-matrices for each cell.

required
r2ls list[STypeMM]

Cumulative right-to-left S-matrices for each cell.

required
excitation_l ComplexArray1D

Left boundary excitation vector.

required
excitation_r ComplexArray1D

Right boundary excitation vector.

required

Returns:

Type Description
list[ComplexArray1D]

Tuple (forwards, backwards) where each element is a list of

list[ComplexArray1D]

complex amplitude vectors, one per cell.

Source code in src/meow/eme/propagation.py
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
def propagate(
    l2rs: list[sax.STypeMM],
    r2ls: list[sax.STypeMM],
    excitation_l: ComplexArray1D,
    excitation_r: ComplexArray1D,
) -> tuple[list[ComplexArray1D], list[ComplexArray1D]]:
    """Propagate boundary excitations through cumulative S-matrices.

    Args:
        l2rs: Cumulative left-to-right S-matrices for each cell.
        r2ls: Cumulative right-to-left S-matrices for each cell.
        excitation_l: Left boundary excitation vector.
        excitation_r: Right boundary excitation vector.

    Returns:
        Tuple ``(forwards, backwards)`` where each element is a list of
        complex amplitude vectors, one per cell.
    """
    forwards = []
    backwards = []
    for l2r, r2l in zip(l2rs, r2ls, strict=False):
        s_l2r, pm_l2r = sax.sdense(l2r)
        s_r2l, pm_r2l = sax.sdense(r2l)
        ports_l2r = _sorted_ports(pm_l2r)
        ports_r2l = _sorted_ports(pm_r2l)
        s_l2r, _ = select_ports((s_l2r, pm_l2r), ports_l2r)
        s_r2l, _ = select_ports((s_r2l, pm_r2l), ports_r2l)
        n_right = len([key for key in ports_l2r if "right" in key])
        fwd, bwd = compute_mode_amplitudes(
            np.asarray(s_l2r), np.asarray(s_r2l), n_right, excitation_l, excitation_r
        )
        forwards.append(fwd)
        backwards.append(bwd)
    return forwards, backwards

propagate_modes(modes, cells, *, excitation_l=None, excitation_r=None, excite_mode_l=0, excite_mode_r=None, y=None, z=None, num_z=1000, sax_backend='default', interface_kwargs=None, track=True, tracking_inner_product=inner_product, interfaces_fn=compute_interface_s_matrices, interface_fn=compute_interface_s_matrix)

Propagate modal excitations through a stack of cells.

This is a convenience wrapper around the propagation and interface S-matrix machinery. The only required positional inputs are the modal bases and the cells. Everything else has defaults that are inferred from the stack:

  • by default the left boundary excites mode 0 with unit amplitude;
  • by default there is no right-side excitation;
  • by default y is the center of the transverse mesh;
  • by default z spans the full device with num_z samples.

Parameters:

Name Type Description Default
modes list[list[Mode]]

Mode set for each cell.

required
cells list[Cell]

Cells associated with modes.

required
excitation_l ComplexArray1D | None

Optional explicit left excitation vector.

None
excitation_r ComplexArray1D | None

Optional explicit right excitation vector.

None
excite_mode_l int

Left mode to excite if excitation_l is omitted.

0
excite_mode_r int | None

Right mode to excite if excitation_r is omitted. If omitted, no right-side excitation is applied.

None
y float | None

Transverse y-coordinate at which to reconstruct Ex.

None
z FloatArray1D | None

Global z-grid on which to reconstruct the field.

None
num_z int

Number of z samples if z is omitted.

1000
sax_backend BackendLike

SAX backend used for cascading.

'default'
interface_kwargs dict[str, Any] | None

Optional keyword arguments forwarded to both interfaces_fn and interface_fn.

None
track bool

Whether to reorder and phase-align modes between neighboring cells before propagation.

True
tracking_inner_product Callable

Inner product used for mode tracking.

inner_product
interfaces_fn Callable

Factory for interface S-matrices across the stack.

compute_interface_s_matrices
interface_fn Callable

Factory for the identity-like same-basis interface used to seed the left-to-right accumulation.

compute_interface_s_matrix

Returns:

Type Description
ComplexArray2D

(field, x) where field is the reconstructed Ex(z, x) slice

FloatArray1D

and x is the transverse sampling grid.

Source code in src/meow/eme/propagation.py
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
def propagate_modes(
    modes: list[list[Mode]],
    cells: list[Cell],
    *,
    excitation_l: ComplexArray1D | None = None,
    excitation_r: ComplexArray1D | None = None,
    excite_mode_l: int = 0,
    excite_mode_r: int | None = None,
    y: float | None = None,
    z: FloatArray1D | None = None,
    num_z: int = 1000,
    sax_backend: sax.BackendLike = "default",
    interface_kwargs: dict[str, Any] | None = None,
    track: bool = True,
    tracking_inner_product: Callable = inner_product,
    interfaces_fn: Callable = compute_interface_s_matrices,
    interface_fn: Callable = compute_interface_s_matrix,
) -> tuple[ComplexArray2D, FloatArray1D]:
    """Propagate modal excitations through a stack of cells.

    This is a convenience wrapper around the propagation and interface S-matrix
    machinery. The only required positional inputs are the modal bases and the
    cells. Everything else has defaults that are inferred from the stack:

    - by default the left boundary excites mode 0 with unit amplitude;
    - by default there is no right-side excitation;
    - by default ``y`` is the center of the transverse mesh;
    - by default ``z`` spans the full device with ``num_z`` samples.

    Args:
        modes: Mode set for each cell.
        cells: Cells associated with ``modes``.
        excitation_l: Optional explicit left excitation vector.
        excitation_r: Optional explicit right excitation vector.
        excite_mode_l: Left mode to excite if ``excitation_l`` is omitted.
        excite_mode_r: Right mode to excite if ``excitation_r`` is omitted. If
            omitted, no right-side excitation is applied.
        y: Transverse y-coordinate at which to reconstruct ``Ex``.
        z: Global z-grid on which to reconstruct the field.
        num_z: Number of z samples if ``z`` is omitted.
        sax_backend: SAX backend used for cascading.
        interface_kwargs: Optional keyword arguments forwarded to both
            ``interfaces_fn`` and ``interface_fn``.
        track: Whether to reorder and phase-align modes between neighboring
            cells before propagation.
        tracking_inner_product: Inner product used for mode tracking.
        interfaces_fn: Factory for interface S-matrices across the stack.
        interface_fn: Factory for the identity-like same-basis interface used to
            seed the left-to-right accumulation.

    Returns:
        ``(field, x)`` where ``field`` is the reconstructed ``Ex(z, x)`` slice
        and ``x`` is the transverse sampling grid.
    """
    if len(cells) != len(modes):
        msg = f"len(cells) != len(modes): {len(cells)} != {len(modes)}"
        raise ValueError(msg)
    if not cells:
        msg = "At least one cell is required."
        raise ValueError(msg)

    actual_sax_backend = sax.into[sax.Backend](sax_backend)
    interface_kwargs = dict(interface_kwargs or {})
    interface_kwargs.setdefault("enforce_reciprocity", False)

    if excitation_l is None:
        excitation_l = _default_excitation(len(modes[0]), excite_mode_l)
    if excitation_r is None:
        if excite_mode_r is None:
            excitation_r = np.zeros(len(modes[-1]), dtype=complex)
        else:
            excitation_r = _default_excitation(len(modes[-1]), excite_mode_r)

    if y is None:
        y = float(0.5 * (cells[0].mesh.y.min() + cells[0].mesh.y.max()))
    if z is None:
        z = _default_z(cells, num_z)

    tracked_modes = (
        track_modes(modes, inner_product_fn=tracking_inner_product) if track else modes
    )

    propagations = compute_propagation_s_matrices(tracked_modes, cells)
    interfaces = interfaces_fn(tracked_modes, **interface_kwargs)
    identity = interface_fn(tracked_modes[0], tracked_modes[0], **interface_kwargs)

    pairs = pi_pairs(propagations, interfaces, actual_sax_backend)
    l2rs = l2r_matrices(pairs, identity, actual_sax_backend)
    r2ls = r2l_matrices(pairs, actual_sax_backend)

    forwards, backwards = propagate(l2rs, r2ls, excitation_l, excitation_r)
    return plot_fields(tracked_modes, cells, forwards, backwards, y, z)

r2l_matrices(pairs, sax_backend)

Return cumulative right-to-left S-matrices.

Parameters:

Name Type Description Default
pairs list[STypeMM]

Propagation-interface pair S-matrices from pi_pairs.

required
sax_backend Backend

SAX backend used for cascading.

required

Returns:

Type Description
list[STypeMM]

List of cumulative S-matrices from each cell to the right boundary.

Source code in src/meow/eme/propagation.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def r2l_matrices(
    pairs: list[sax.STypeMM], sax_backend: sax.Backend
) -> list[sax.STypeMM]:
    """Return cumulative right-to-left S-matrices.

    Args:
        pairs: Propagation-interface pair S-matrices from ``pi_pairs``.
        sax_backend: SAX backend used for cascading.

    Returns:
        List of cumulative S-matrices from each cell to the right boundary.
    """
    matrices = [pairs[-1]]
    for pair in pairs[-2::-1]:
        matrices.append(_connect_two(pair, matrices[-1], sax_backend))
    return matrices[::-1]

select_ports(S, ports)

Keep a subset of ports from an S-matrix.

Parameters:

Name Type Description Default
S SDenseMM

A tuple (s_matrix, port_map) in SAX dense multimode format.

required
ports list[str]

Port names to retain.

required

Returns:

Type Description
SDenseMM

A new (s_matrix, port_map) tuple with only the selected ports.

Source code in src/meow/eme/propagation.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def select_ports(S: sax.SDenseMM, ports: list[str]) -> sax.SDenseMM:
    """Keep a subset of ports from an S-matrix.

    Args:
        S: A tuple ``(s_matrix, port_map)`` in SAX dense multimode format.
        ports: Port names to retain.

    Returns:
        A new ``(s_matrix, port_map)`` tuple with only the selected ports.
    """
    s, pm = S
    idxs = jnp.array([pm[port] for port in ports], dtype=jnp.int32)
    s = s[idxs, :][:, idxs]
    new_port_map = {p: i for i, p in enumerate(ports)}
    return s, new_port_map

sort_structures(structures)

sort_structures(
    structures: list[Structure3D],
) -> list[Structure3D]
sort_structures(
    structures: list[Structure2D],
) -> list[Structure2D]

Sort structures by mesh order, then by order of definition.

Parameters:

Name Type Description Default
structures list[Structure3D] | list[Structure2D]

the list of structures to sort.

required

Returns:

Type Description
list[Structure2D] | list[Structure3D]

A new list of structures sorted by descending mesh order.

Source code in src/meow/structures.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def sort_structures(
    structures: list[Structure3D] | list[Structure2D],
) -> list[Structure2D] | list[Structure3D]:
    """Sort structures by mesh order, then by order of definition.

    Args:
        structures: the list of structures to sort.

    Returns:
        A new list of structures sorted by descending mesh order.
    """
    struct_info = [(s.mesh_order, -i, s) for i, s in enumerate(structures)]
    sorted_struct_info = sorted(struct_info, key=lambda I: (I[0], I[1]), reverse=True)
    return cast(
        list[Structure2D] | list[Structure3D], [s for _, _, s in sorted_struct_info]
    )

split_square_matrix(matrix, idx)

Split a square matrix into its four block submatrices.

Parameters:

Name Type Description Default
matrix ComplexArray2D

Square matrix to split.

required
idx int

Row/column index at which to split.

required

Returns:

Type Description
tuple[tuple[ComplexArray2D, ComplexArray2D], tuple[ComplexArray2D, ComplexArray2D]]

Nested tuple ((top_left, top_right), (bottom_left, bottom_right)).

Source code in src/meow/eme/propagation.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def split_square_matrix(
    matrix: ComplexArray2D, idx: int
) -> tuple[
    tuple[ComplexArray2D, ComplexArray2D], tuple[ComplexArray2D, ComplexArray2D]
]:
    """Split a square matrix into its four block submatrices.

    Args:
        matrix: Square matrix to split.
        idx: Row/column index at which to split.

    Returns:
        Nested tuple ``((top_left, top_right), (bottom_left, bottom_right))``.
    """
    if matrix.shape[0] != matrix.shape[1]:
        msg = "Matrix has to be square."
        raise ValueError(msg)
    return (matrix[:idx, :idx], matrix[:idx, idx:]), (
        matrix[idx:, :idx],
        matrix[idx:, idx:],
    )

te_fraction(mode)

The TE polarization fraction of the Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to compute the TE fraction of.

required

Returns:

Type Description
float

The TE fraction (0.0 for pure TM, 1.0 for pure TE).

Source code in src/meow/mode.py
633
634
635
636
637
638
639
640
641
642
643
644
645
def te_fraction(mode: Mode) -> float:
    """The TE polarization fraction of the `Mode`.

    Args:
        mode: the mode to compute the TE fraction of.

    Returns:
        The TE fraction (0.0 for pure TM, 1.0 for pure TE).
    """
    epsx = np.real(mode.cs.nx**2)
    e = np.sum(0.5 * eps0 * epsx * np.abs(mode.Ex) ** 2)
    h = np.sum(0.5 * mu0 * np.abs(mode.Hx) ** 2)
    return float(e / (e + h))

track_modes(modes, *, inner_product_fn=inner_product)

Track modal order and phase continuously across a cell stack.

Modes are solved independently in each cell, so neighboring cells may differ by permutation and arbitrary complex phase. This helper reorders each mode set by maximum overlap with the previous cell and phase-aligns matched modes so that the tracking overlap is real-positive.

Unmatched modes are appended after the matched subset in their original order. This function does not change the physics of the interface solve; it only makes the local basis more continuous for reconstruction and plotting.

Parameters:

Name Type Description Default
modes list[Modes]

Ordered list of modal bases across the stack.

required
inner_product_fn Callable

Inner-product callable used to compute overlaps between neighboring mode sets.

inner_product

Returns:

Type Description
list[Modes]

Reordered and phase-aligned list of modal bases.

Source code in src/meow/eme/propagation.py
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
def track_modes(
    modes: list[Modes],
    *,
    inner_product_fn: Callable = inner_product,
) -> list[Modes]:
    """Track modal order and phase continuously across a cell stack.

    Modes are solved independently in each cell, so neighboring cells may differ
    by permutation and arbitrary complex phase. This helper reorders each mode
    set by maximum overlap with the previous cell and phase-aligns matched modes
    so that the tracking overlap is real-positive.

    Unmatched modes are appended after the matched subset in their original
    order. This function does not change the physics of the interface solve; it
    only makes the local basis more continuous for reconstruction and plotting.

    Args:
        modes: Ordered list of modal bases across the stack.
        inner_product_fn: Inner-product callable used to compute overlaps
            between neighboring mode sets.

    Returns:
        Reordered and phase-aligned list of modal bases.
    """
    if not modes:
        return []

    tracked: list[Modes] = [list(modes[0])]
    for current in modes[1:]:
        previous = tracked[-1]
        n_prev = len(previous)
        n_curr = len(current)
        if n_prev == 0 or n_curr == 0:
            tracked.append(list(current))
            continue

        overlaps = np.zeros((n_prev, n_curr), dtype=np.complex128)
        for i, mode_prev in enumerate(previous):
            for j, mode_curr in enumerate(current):
                overlaps[i, j] = inner_product_fn(mode_prev, mode_curr)

        row_ind, col_ind = linear_sum_assignment(-np.abs(overlaps))
        matched_cols = set(col_ind.tolist())

        reordered: list[Mode] = []
        for i, j in sorted(
            zip(row_ind, col_ind, strict=True), key=lambda pair: pair[0]
        ):
            overlap = overlaps[i, j]
            if np.abs(overlap) > 0:
                phase = np.exp(-1j * np.angle(overlap))
                reordered.append(current[j] * phase)
            else:
                reordered.append(current[j])

        reordered.extend(
            mode for j, mode in enumerate(current) if j not in matched_cols
        )
        tracked.append(reordered)

    return tracked

tsvd_solve(A, B, rcond=0.001)

Solve A X = B with a truncated-SVD pseudoinverse.

This is the regularized solve used by the EME interface construction. The smallest singular directions of A are the first place where truncated mode sets become numerically unstable, so the solve is performed as

X = pinv_tsvd(A) @ B

with a relative cutoff

s_cut = rcond * s_max.

Singular values below s_cut are discarded entirely.

High-level behavior
  • small rcond keeps more singular directions and is closer to the exact inverse, but is more sensitive to ill-conditioning;
  • large rcond drops more singular directions and is more stable, but biases the result towards a lower-rank approximation.

This routine does not enforce passivity by itself. It only stabilizes the linear inversion. Passivity is handled afterwards on the assembled interface S-matrix.

Parameters:

Name Type Description Default
A ndarray

System matrix to invert approximately.

required
B ndarray

Right-hand side.

required
rcond float

Relative singular-value cutoff. Singular values smaller than rcond * max(s) are discarded.

0.001

Returns:

Type Description
ndarray

A tuple (X, s, s_cut, rank_kept) containing the solved result,

ndarray

the singular values of A, the absolute cutoff, and the number of

float

retained singular directions.

Raises:

Type Description
RuntimeError

If all singular values are rejected.

Source code in src/meow/eme/solve.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def tsvd_solve(
    A: np.ndarray, B: np.ndarray, rcond: float = 1e-3
) -> tuple[np.ndarray, np.ndarray, float, float]:
    """Solve ``A X = B`` with a truncated-SVD pseudoinverse.

    This is the regularized solve used by the EME interface construction. The
    smallest singular directions of ``A`` are the first place where truncated
    mode sets become numerically unstable, so the solve is performed as

    ``X = pinv_tsvd(A) @ B``

    with a relative cutoff

    ``s_cut = rcond * s_max``.

    Singular values below ``s_cut`` are discarded entirely.

    High-level behavior:
        - small ``rcond`` keeps more singular directions and is closer to the
          exact inverse, but is more sensitive to ill-conditioning;
        - large ``rcond`` drops more singular directions and is more stable,
          but biases the result towards a lower-rank approximation.

    This routine does not enforce passivity by itself. It only stabilizes the
    linear inversion. Passivity is handled afterwards on the assembled
    interface S-matrix.

    Args:
        A: System matrix to invert approximately.
        B: Right-hand side.
        rcond: Relative singular-value cutoff. Singular values smaller than
            ``rcond * max(s)`` are discarded.

    Returns:
        A tuple ``(X, s, s_cut, rank_kept)`` containing the solved result,
        the singular values of ``A``, the absolute cutoff, and the number of
        retained singular directions.

    Raises:
        RuntimeError: If all singular values are rejected.
    """
    U, s, Vh = np.linalg.svd(A, full_matrices=False)
    s_cut = rcond * float(s[0]) if s.size else 0.0
    keep = s >= s_cut
    if not np.any(keep):
        msg = "TSVD rejected all singular values; lower rcond."
        raise RuntimeError(msg)
    A_pinv = (Vh.conj().T[:, keep] * (1.0 / s[keep])) @ U.conj().T[keep, :]
    X = A_pinv @ B
    return X, s, s_cut, int(np.count_nonzero(keep))

visualize(obj, **kwargs)

Visualize any meow object.

Parameters:

Name Type Description Default
obj Any

the meow object to visualize

required
**kwargs Any

extra configuration to visualize the object

{}

Returns:

Type Description
Any

The result of the visualization function for the given object.

Note

Most meow objects have a ._visualize method. Check out its help to see which kwargs are accepted.

Source code in src/meow/visualization.py
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
def visualize(obj: Any, **kwargs: Any) -> Any:
    """Visualize any meow object.

    Args:
        obj: the meow object to visualize
        **kwargs: extra configuration to visualize the object

    Returns:
        The result of the visualization function for the given object.

    Note:
        Most meow objects have a `._visualize` method.
        Check out its help to see which kwargs are accepted.
    """
    try:
        is_empty = bool(not obj)
    except ValueError:
        is_empty = isinstance(obj, np.ndarray) and obj.size == 0

    if is_empty:
        msg = "Nothing to visualize!"
        raise ValueError(msg)

    vis_func = _get_vis_func(obj)
    vis_func(obj, **kwargs)

zero_phase(mode)

Normalize (zero out) the phase of a Mode.

Parameters:

Name Type Description Default
mode Mode

the mode to normalize the phase of.

required

Returns:

Type Description
Mode

A new mode with zeroed-out phase.

Source code in src/meow/mode.py
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
def zero_phase(mode: Mode) -> Mode:
    """Normalize (zero out) the phase of a `Mode`.

    Args:
        mode: the mode to normalize the phase of.

    Returns:
        A new mode with zeroed-out phase.
    """
    e = np.abs(energy_density(mode))
    m, n = np.array(np.where(e == e.max()))[:, 0]
    phase = np.exp(-1j * np.angle(np.array(mode.Hx))[m][n])
    new_mode = Mode(
        neff=mode.neff,
        cs=mode.cs,
        Ex=mode.Ex * phase,
        Ey=mode.Ey * phase,
        Ez=mode.Ez * phase,
        Hx=mode.Hx * phase,
        Hy=mode.Hy * phase,
        Hz=mode.Hz * phase,
    )
    if _sum_around(np.real(new_mode.Hx), int(m), int(n)) < 0:
        new_mode = invert_mode(new_mode)
    return new_mode