"""Rectangular dielectric waveguide utilities."""
from typing import List, Any
import numpy
import pydantic.v1 as pydantic
from ...components.base import Tidy3dBaseModel, cached_property, skip_if_fields_missing
from ...components.boundary import BoundarySpec, Periodic
from ...components.data.data_array import ModeIndexDataArray, FreqModeDataArray
from ...components.geometry.base import Box
from ...components.geometry.polyslab import PolySlab
from ...components.grid.grid_spec import GridSpec
from ...components.medium import Medium, MediumType
from ...components.mode import ModeSpec
from ...components.simulation import Simulation
from ...components.source import ModeSource, GaussianPulse
from ...components.structure import Structure
from ...components.types import ArrayFloat1D, Ax, Axis, Coordinate, Literal, Size1D, Union
from ...components.types import TYPE_TAG_STR
from ...constants import C_0, inf, MICROMETER, RADIAN
from ...exceptions import Tidy3dError, ValidationError
from ...log import log
from ..mode.mode_solver import ModeSolver
[docs]
class RectangularDielectric(Tidy3dBaseModel):
    """General rectangular dielectric waveguide
    Supports:
    - Strip and rib geometries
    - Angled sidewalls
    - Modes in waveguide bends
    - Surface and sidewall loss models
    - Coupled waveguides
    """
    wavelength: Union[float, ArrayFloat1D] = pydantic.Field(
        ...,
        title="Wavelength",
        description="Wavelength(s) at which to calculate modes (in ΞΌm).",
        units=MICROMETER,
    )
    core_width: Union[Size1D, ArrayFloat1D] = pydantic.Field(
        ...,
        title="Core width",
        description="Core width at the top of the waveguide.  If set to an array, defines "
        "the widths of adjacent waveguides.",
        units=MICROMETER,
    )
    core_thickness: Size1D = pydantic.Field(
        ...,
        title="Core Thickness",
        description="Thickness of the core layer.",
        units=MICROMETER,
    )
    core_medium: MediumType = pydantic.Field(
        ...,
        title="Core Medium",
        description="Medium associated with the core layer.",
        discriminator=TYPE_TAG_STR,
    )
    clad_medium: MediumType = pydantic.Field(
        ...,
        title="Clad Medium",
        description="Medium associated with the upper cladding layer.",
        discriminator=TYPE_TAG_STR,
    )
    box_medium: MediumType = pydantic.Field(
        None,
        title="Box Medium",
        description="Medium associated with the lower cladding layer.",
        discriminator=TYPE_TAG_STR,
    )
    slab_thickness: Size1D = pydantic.Field(
        0.0,
        title="Slab Thickness",
        description="Thickness of the slab for rib geometry.",
        units=MICROMETER,
    )
    clad_thickness: Size1D = pydantic.Field(
        None,
        title="Clad Thickness",
        description="Domain size above the core layer.",
        units=MICROMETER,
    )
    box_thickness: Size1D = pydantic.Field(
        None,
        title="Box Thickness",
        description="Domain size below the core layer.",
        units=MICROMETER,
    )
    side_margin: Size1D = pydantic.Field(
        None,
        title="Side Margin",
        description="Domain size to the sides of the waveguide core.",
        units=MICROMETER,
    )
    sidewall_angle: float = pydantic.Field(
        0.0,
        title="Sidewall Angle",
        description="Angle of the core sidewalls measured from the vertical direction (in "
        "radians).  Positive (negative) values create waveguides with bases wider (narrower) "
        "than their tops.",
        units=RADIAN,
    )
    gap: Union[float, ArrayFloat1D] = pydantic.Field(
        0.0,
        title="Gap",
        description="Distance between adjacent waveguides, measured at the top core edges.  "
        "An array can be used to define one gap per pair of adjacent waveguides.",
        units=MICROMETER,
    )
    sidewall_thickness: Size1D = pydantic.Field(
        0.0,
        title="Sidewall Thickness",
        description="Sidewall layer thickness (within core).",
        units=MICROMETER,
    )
    sidewall_medium: MediumType = pydantic.Field(
        None,
        title="Sidewall medium",
        description="Medium associated with the sidewall layer to model sidewall losses.",
        discriminator=TYPE_TAG_STR,
    )
    surface_thickness: Size1D = pydantic.Field(
        0.0,
        title="Surface Thickness",
        description="Thickness of the surface layers defined on the top of the waveguide and  "
        "slab regions (if any).",
        units=MICROMETER,
    )
    surface_medium: MediumType = pydantic.Field(
        None,
        title="Surface Medium",
        description="Medium associated with the surface layer to model surface losses.",
        discriminator=TYPE_TAG_STR,
    )
    origin: Coordinate = pydantic.Field(
        (0, 0, 0),
        title="Origin",
        description="Center of the waveguide geometry.  This coordinate represents the base "
        "of the waveguides (substrate surface) in the normal axis, and center of the geometry "
        "in the remaining axes.",
        units=MICROMETER,
    )
    length: Size1D = pydantic.Field(
        1e30,
        title="Length",
        description="Length of the waveguides in the propagation direction",
        units=MICROMETER,
    )
    propagation_axis: Axis = pydantic.Field(
        0,
        title="Propagation Axis",
        description="Axis of propagation of the waveguide",
    )
    normal_axis: Axis = pydantic.Field(
        2,
        title="Normal Axis",
        description="Axis normal to the substrate surface",
    )
    mode_spec: ModeSpec = pydantic.Field(
        ModeSpec(num_modes=2),
        title="Mode Specification",
        description=":class:`ModeSpec` defining waveguide mode properties.",
    )
    grid_resolution: int = pydantic.Field(
        15,
        title="Grid Resolution",
        description="Solver grid resolution per wavelength.",
    )
    max_grid_scaling: float = pydantic.Field(
        1.2,
        title="Maximal Grid Scaling",
        description="Maximal size increase between adjacent grid boundaries.",
    )
    @pydantic.validator("wavelength", "core_width", "gap", always=True)
    def _set_array(cls, val):
        """Ensure values are not negative and convert to numpy arrays."""
        result = numpy.array(val, ndmin=1)
        if any(result < 0):
            raise ValidationError("Values may not be negative.")
        return result
    @pydantic.validator("box_medium", always=True)
    @skip_if_fields_missing(["clad_medium"])
    def _set_box_medium(cls, val, values):
        """Set BOX medium same as cladding as default value."""
        return values["clad_medium"] if val is None else val
    @pydantic.validator("clad_thickness", always=True)
    @skip_if_fields_missing(["clad_medium", "wavelength"])
    def _set_clad_thickness(cls, val, values):
        """Set default cladding thickness based on the max wavelength in the cladding medium."""
        if val is None:
            wavelength = values["wavelength"]
            medium = values["clad_medium"]
            n = numpy.array([medium.nk_model(f)[0] for f in C_0 / wavelength])
            lda = wavelength / n
            return 1.5 * lda.max()
        return val
    @pydantic.validator("box_thickness", always=True)
    @skip_if_fields_missing(["clad_medium", "wavelength"])
    def _set_box_thickness(cls, val, values):
        """Set default BOX thickness based on the max wavelength in the BOX medium."""
        if val is None:
            wavelength = values["wavelength"]
            medium = values["box_medium"]
            n = numpy.array([medium.nk_model(f)[0] for f in C_0 / wavelength])
            lda = wavelength / n
            return 1.5 * lda.max()
        return val
    @pydantic.validator("side_margin", always=True)
    @skip_if_fields_missing(["clad_thickness", "box_thickness"])
    def _set_side_margin(cls, val, values):
        """Set default side margin based on BOX and cladding thicknesses."""
        return max(values["clad_thickness"], values["box_thickness"]) if val is None else val
    @pydantic.validator("gap", always=True)
    @skip_if_fields_missing(["core_width"])
    def _validate_gaps(cls, val, values):
        """Ensure the number of gaps is compatible with the number of cores supplied."""
        if val.size == 1 and values["core_width"].size != 2:
            # If a single value is defined, use it for all gaps
            return numpy.array([val[0]] * (values["core_width"].size - 1))
        if val.size != values["core_width"].size - 1:
            raise ValidationError("Number of gaps must be 1 less than number of core widths.")
        return val
    @pydantic.root_validator
    def _ensure_consistency(cls, values):
        """Ensure consistency in setting surface/sidewall models and propagation/normal axes."""
        sidewall_thickness = values["sidewall_thickness"]
        sidewall_medium = values["sidewall_medium"]
        surface_thickness = values["surface_thickness"]
        surface_medium = values["surface_medium"]
        propagation_axis = values["propagation_axis"]
        normal_axis = values["normal_axis"]
        if sidewall_thickness > 0 and sidewall_medium is None:
            raise ValidationError(
                "Sidewall medium must be provided when sidewall thickness is greater than 0."
            )
        if sidewall_thickness == 0 and sidewall_medium is not None:
            log.warning("Sidewall medium not used because sidewall thickness is zero.")
        if surface_thickness > 0 and surface_medium is None:
            raise ValidationError(
                "Surface medium must be provided when surface thickness is greater than 0."
            )
        if surface_thickness == 0 and surface_medium is not None:
            log.warning("Surface medium not used because surface thickness is zero.")
        if propagation_axis == normal_axis:
            raise ValidationError("Propagation and normal axes must be different.")
        return values
    @cached_property
    def lateral_axis(self) -> Axis:
        """Lateral direction axis."""
        return 3 - self.propagation_axis - self.normal_axis
    def _swap_axis(
        self, lateral_coord: Any, normal_coord: Any, propagation_coord: Any
    ) -> List[Any]:
        """Swap the model coordinates to desired axes."""
        result = [None, None, None]
        result[self.lateral_axis] = lateral_coord
        result[self.propagation_axis] = propagation_coord
        result[self.normal_axis] = normal_coord
        return result
    def _translate(
        self, lateral_coord: float, normal_coord: float, propagation_coord: float
    ) -> List[float]:
        """Swap the model coordinates to desired axes and translate to origin."""
        coordinates = self._swap_axis(lateral_coord, normal_coord, propagation_coord)
        result = [a + b for a, b in zip(self.origin, coordinates)]
        return result
    def _transform_in_plane(self, lateral_coord: float, propagation_coord: float) -> List[float]:
        """Swap the model coordinates to desired axes in the substrate plane."""
        result = self._translate(lateral_coord, 0, propagation_coord)
        _, result = Box.pop_axis(result, self.normal_axis)
        return result
    @cached_property
    def height(self) -> Size1D:
        """Domain height (size in the normal direction)."""
        return self.box_thickness + self.core_thickness + self.clad_thickness
    @cached_property
    def width(self) -> Size1D:
        """Domain width (size in the lateral direction)."""
        w = self.core_width.sum() + self.gap.sum() + 2 * self.side_margin
        if self.sidewall_angle > 0:
            w += 2 * self.core_thickness * numpy.tan(self.sidewall_angle)
        return w
    @property
    def _core_starts(self) -> List[float]:
        """Starting positions of each waveguide (x is the position in the lateral direction)."""
        core_x = [-0.5 * (self.core_width.sum() + self.gap.sum())]
        core_x.extend(core_x[0] + numpy.cumsum(self.core_width[:-1]) + numpy.cumsum(self.gap))
        return core_x
    @property
    def _override_structures(self) -> List[Structure]:
        """Build override structures to define the simulation grid."""
        # Grid resolution factor applied to the materials (increase for waveguide corners
        # and decrease for evanescent tail regions).
        scale_factor = 1.5
        freqs = C_0 / self.wavelength
        nk_core = numpy.array([self.core_medium.nk_model(f) for f in freqs])
        nk_clad = numpy.array([self.clad_medium.nk_model(f) for f in freqs])
        nk_box = numpy.array([self.box_medium.nk_model(f) for f in freqs])
        lda_core = self.wavelength / nk_core[:, 0]
        lda_clad = self.wavelength / nk_clad[:, 0]
        lda_box = self.wavelength / nk_box[:, 0]
        core_x = self._core_starts
        i = numpy.argmin(lda_core)
        hi_index = Medium.from_nk(n=nk_core[i, 0] * scale_factor, k=0, freq=freqs[i])
        i = numpy.argmin(lda_box)
        lo_index_box = Medium.from_nk(n=max(1.0, nk_box[i, 0] / scale_factor), k=0, freq=freqs[i])
        i = numpy.argmin(lda_clad)
        lo_index_clad = Medium.from_nk(n=max(1.0, nk_clad[i, 0] / scale_factor), k=0, freq=freqs[i])
        # Gather all waveguide edge intervals into `hi_res` list
        corner_margin = max(lda_box.max(), lda_clad.max()) / self.grid_resolution
        if self.sidewall_angle > 0:
            dx = (self.core_thickness - self.slab_thickness) * numpy.tan(self.sidewall_angle)
            hi_res = [
                pair
                for x, w in zip(core_x, self.core_width)
                for pair in [
                    [x - dx - corner_margin, x + corner_margin],
                    [x + w - corner_margin, x + w + dx + corner_margin],
                ]
            ]
        elif self.sidewall_angle < 0:
            dx = (self.core_thickness - self.slab_thickness) * numpy.tan(self.sidewall_angle)
            hi_res = [
                pair
                for x, w in zip(core_x, self.core_width)
                for pair in [
                    [x - corner_margin, x - dx + corner_margin],
                    [x + w + dx - corner_margin, x + w + corner_margin],
                ]
            ]
        else:
            hi_res = [
                pair
                for x, w in zip(core_x, self.core_width)
                for pair in [
                    [x - corner_margin, x + corner_margin],
                    [x + w - corner_margin, x + w + corner_margin],
                ]
            ]
        # The gaps between waveguides can be small enough to merge adjacent high
        # resolution intervals (specially with angled sidewalls), so we merge
        # intervals that overlap
        i = 0
        while i < len(hi_res) - 1:
            if hi_res[i][1] >= hi_res[i + 1][0]:
                hi_res[i][1] = hi_res.pop(i + 1)[1]
            else:
                i += 1
        # Create override structures to improve the mode solver grid.  We want
        # high resolution around all core edges, but not along the whole slab
        # in case of rib geometry.  Start with the 4 corners:
        override_structures = [
            Structure(
                geometry=Box(
                    center=self._translate(0.5 * (a + b), y, 0),
                    size=self._swap_axis(b - a, 2 * corner_margin, inf),
                ),
                medium=hi_index,
            )
            for (a, b) in hi_res
            for y in (self.slab_thickness, self.core_thickness)
        ]
        # Low resolution on the sides:
        override_structures.extend(
            Structure(
                geometry=Box(
                    center=self._translate(0.5 * (a + b), 0, 0),
                    size=self._swap_axis(b - a, inf, inf),
                ),
                medium=lo_index_clad,
            )
            for (a, b) in ((-self.width, hi_res[0][0]), (hi_res[-1][1], self.width))
        )
        # Low resolution above and below:
        override_structures.extend(
            Structure(
                geometry=Box(
                    center=self._translate(0, 0.5 * (a + b), 0),
                    size=self._swap_axis(inf, b - a, inf),
                ),
                medium=lo_index,
            )
            for (a, b, lo_index) in (
                (-2 * self.box_thickness, -corner_margin, lo_index_box),
                (
                    self.core_thickness + corner_margin,
                    self.core_thickness + 2 * self.clad_thickness,
                    lo_index_clad,
                ),
            )
        )
        return override_structures
    @cached_property
    def grid_spec(self) -> GridSpec:
        """Waveguide grid specification with overriding geometry."""
        grid_spec = GridSpec.auto(
            min_steps_per_wvl=self.grid_resolution,
            wavelength=self.wavelength.min(),
            override_structures=self._override_structures,
            max_scale=self.max_grid_scaling,
        )
        return grid_spec
    @cached_property
    def structures(self) -> List[Structure]:
        """Waveguide structures for simulation, including the core(s), slabs (if any), and bottom
        cladding, if different from the top. For bend modes, the structure is a 270 degree bend
        regardless of :attr:`length`."""
        # Create a local copy of these values, as they will be modified
        # according to the desired geometry
        core_w = numpy.array(self.core_width, copy=True)
        core_t = self.core_thickness
        slab_t = self.slab_thickness
        core_x = self._core_starts
        if self.mode_spec.bend_radius is None or self.mode_spec.bend_radius == 0.0:
            half_length = 0.5 * self.length
            def polyslab_vertices(x, w):
                return (
                    self._transform_in_plane(x, -half_length),
                    self._transform_in_plane(x + w, -half_length),
                    self._transform_in_plane(x + w, half_length),
                    self._transform_in_plane(x, half_length),
                )
        else:
            if (self.normal_axis > self.lateral_axis) != (self.mode_spec.bend_axis == 1):
                raise Tidy3dError(
                    "Waveguide band axis must be the substrate normal "
                    f"(mode_spec.bend_axis = {1 - self.mode_spec.bend_axis})"
                )
            bend_radius = self.mode_spec.bend_radius
            # 10 nm resolution (at center)
            num_points = 1 + int(0.5 + 1.5 * numpy.pi * abs(bend_radius) / 0.01)
            angles = numpy.linspace(-0.75 * numpy.pi, 0.75 * numpy.pi, num_points)
            if bend_radius < 0:
                angles = -angles
            sin = numpy.sin(angles)
            cos = numpy.cos(angles)
            def polyslab_vertices(x, w):
                r_in = bend_radius + x
                v_in = numpy.vstack((-bend_radius + r_in * cos, r_in * sin)).T
                r_out = r_in + w
                v_out = numpy.vstack((-bend_radius + r_out * cos, r_out * sin)).T
                return [self._transform_in_plane(*v) for v in list(v_out) + list(v_in[::-1])]
        # Create the actual waveguide geometry
        structures = []
        normal_origin = self.origin[self.normal_axis]
        # Surface and sidewall loss regions are created first, so that the core
        # can be applied on top.
        if self.surface_thickness > 0:
            structures.extend(
                Structure(
                    geometry=PolySlab(
                        vertices=polyslab_vertices(x, w),
                        slab_bounds=(
                            normal_origin + core_t - self.surface_thickness,
                            normal_origin + core_t,
                        ),
                        sidewall_angle=self.sidewall_angle,
                        reference_plane="top",
                        axis=self.normal_axis,
                    ),
                    medium=self.surface_medium,
                )
                for x, w in zip(core_x, core_w)
            )
            # Add loss region over slab surface, if rib geometry
            if slab_t > 0:
                structures.append(
                    Structure(
                        geometry=Box(
                            center=self._translate(0, 0.5 * slab_t, 0),
                            size=self._swap_axis(inf, slab_t, self.length),
                        ),
                        medium=self.surface_medium,
                    )
                )
            # Correct core geometry to leave the lossy regions with their
            # specified thickness
            dx = self.surface_thickness * numpy.tan(self.sidewall_angle)
            core_x -= dx
            core_w += 2 * dx
            core_t -= self.surface_thickness
            slab_t = max(0, slab_t - self.surface_thickness)
        if self.sidewall_thickness > 0:
            structures.extend(
                Structure(
                    geometry=PolySlab(
                        vertices=polyslab_vertices(x, w),
                        slab_bounds=(normal_origin, normal_origin + core_t),
                        sidewall_angle=self.sidewall_angle,
                        reference_plane="top",
                        axis=self.normal_axis,
                    ),
                    medium=self.sidewall_medium,
                )
                for x, w in zip(core_x, core_w)
            )
            # Core position offset and width reduction to accommodate lossy
            # regions
            dx = self.sidewall_thickness / numpy.cos(self.sidewall_angle)
            core_x += dx
            core_w -= 2 * dx
        # Waveguide cores
        structures.extend(
            Structure(
                geometry=PolySlab(
                    vertices=polyslab_vertices(x, w),
                    slab_bounds=(normal_origin, normal_origin + core_t),
                    sidewall_angle=self.sidewall_angle,
                    reference_plane="top",
                    axis=self.normal_axis,
                ),
                medium=self.core_medium,
            )
            for x, w in zip(core_x, core_w)
        )
        # Slab for rib geometry
        if slab_t > 0:
            structures.append(
                Structure(
                    geometry=Box(
                        center=self._translate(0, 0.5 * slab_t, 0),
                        size=self._swap_axis(inf, slab_t, self.length),
                    ),
                    medium=self.core_medium,
                )
            )
        # Lower cladding
        if self.box_medium != self.clad_medium:
            structures.append(
                Structure(
                    geometry=Box(
                        center=self._translate(0, -self.box_thickness, 0),
                        size=self._swap_axis(inf, 2 * self.box_thickness, self.length),
                    ),
                    medium=self.box_medium,
                )
            )
        return structures
    @cached_property
    def mode_solver(self) -> ModeSolver:
        """Create a mode solver based on this waveguide structure
        Returns
        -------
        :class:`ModeSolver`
        Example
        -------
        >>> wg = waveguide.RectangularDielectric(
        ...     wavelength=1.55,
        ...     core_width=0.5,
        ...     core_thickness=0.22,
        ...     core_medium=Medium(permittivity=3.48**2),
        ...     clad_medium=Medium(permittivity=1.45**2),
        ...     num_modes=2,
        ... )
        >>> mode_data = wg.mode_solver.solve()
        >>> mode_data.n_eff.values
        array([[2.4536054 1.7850305]], dtype=float32)
        """
        freqs = C_0 / self.wavelength
        f_max = freqs.max()
        f_min = freqs.min()
        freq0 = 0.5 * (f_max + f_min)
        fwidth = max(0.1 * freq0, f_max - f_min)
        plane = Box(
            center=self._translate(0, 0.5 * self.height - self.box_thickness, 0),
            size=self._swap_axis(self.width, self.height, 0),
        )
        # Source used only to silence warnings
        mode_source = ModeSource(
            center=plane.center,
            size=plane.size,
            source_time=GaussianPulse(freq0=freq0, fwidth=fwidth),
            direction="+",
            mode_spec=self.mode_spec,
        )
        simulation = Simulation(
            center=plane.center,
            size=plane.size,
            medium=self.clad_medium,
            structures=self.structures,
            boundary_spec=BoundarySpec.all_sides(Periodic()),
            grid_spec=self.grid_spec,
            sources=[mode_source],
            run_time=1e-12,
        )
        mode_solver = ModeSolver(
            simulation=simulation,
            plane=plane,
            mode_spec=self.mode_spec,
            freqs=freqs,
        )
        return mode_solver
    @property
    def n_eff(self) -> ModeIndexDataArray:
        """Calculate the effective index."""
        return self.mode_solver.data.n_eff
    @property
    def n_complex(self) -> ModeIndexDataArray:
        """Calculate the complex effective index."""
        return self.mode_solver.data.n_complex
    @property
    def n_group(self) -> ModeIndexDataArray:
        r"""Calculate the group index."""
        return self.mode_solver.data.n_group
    @property
    def mode_area(self) -> FreqModeDataArray:
        """Calculate the effective mode area."""
        return self.mode_solver.data.mode_area
    # plot wrappers
[docs]
    def plot(
        self,
        x: float = None,
        y: float = None,
        z: float = None,
        ax: Ax = None,
        source_alpha: float = None,
        monitor_alpha: float = None,
        **patch_kwargs,
    ) -> Ax:
        """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
        Parameters
        ----------
        x : float = None
            position of plane in x direction, only one of x, y, z must be specified to define plane.
        y : float = None
            position of plane in y direction, only one of x, y, z must be specified to define plane.
        z : float = None
            position of plane in z direction, only one of x, y, z must be specified to define plane.
        source_alpha : float = None
            Opacity of the sources. If ``None``, uses Tidy3d default.
        monitor_alpha : float = None
            Opacity of the monitors. If ``None``, uses Tidy3d default.
        ax : matplotlib.axes._subplots.Axes = None
            Matplotlib axes to plot on, if not specified, one is created.
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.simulation.plot(
            x=x,
            y=y,
            z=z,
            ax=ax,
            source_alpha=source_alpha,
            monitor_alpha=monitor_alpha,
            **patch_kwargs,
        ) 
[docs]
    def plot_eps(
        self,
        x: float = None,
        y: float = None,
        z: float = None,
        freq: float = None,
        alpha: float = None,
        source_alpha: float = None,
        monitor_alpha: float = None,
        ax: Ax = None,
    ) -> Ax:
        """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
        The permittivity is plotted in grayscale based on its value at the specified frequency.
        Parameters
        ----------
        x : float = None
            position of plane in x direction, only one of x, y, z must be specified to define plane.
        y : float = None
            position of plane in y direction, only one of x, y, z must be specified to define plane.
        z : float = None
            position of plane in z direction, only one of x, y, z must be specified to define plane.
        freq : float = None
            Frequency to evaluate the relative permittivity of all mediums.
            If not specified, evaluates at infinite frequency.
        alpha : float = None
            Opacity of the structures being plotted.
            Defaults to the structure default alpha.
        source_alpha : float = None
            Opacity of the sources. If ``None``, uses Tidy3d default.
        monitor_alpha : float = None
            Opacity of the monitors. If ``None``, uses Tidy3d default.
        ax : matplotlib.axes._subplots.Axes = None
            Matplotlib axes to plot on, if not specified, one is created.
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.simulation.plot_eps(
            x=x,
            y=y,
            z=z,
            freq=freq,
            alpha=alpha,
            source_alpha=source_alpha,
            monitor_alpha=monitor_alpha,
            ax=ax,
        ) 
[docs]
    def plot_structures(
        self, x: float = None, y: float = None, z: float = None, ax: Ax = None
    ) -> Ax:
        """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate.
        Parameters
        ----------
        x : float = None
            position of plane in x direction, only one of x, y, z must be specified to define plane.
        y : float = None
            position of plane in y direction, only one of x, y, z must be specified to define plane.
        z : float = None
            position of plane in z direction, only one of x, y, z must be specified to define plane.
        ax : matplotlib.axes._subplots.Axes = None
            Matplotlib axes to plot on, if not specified, one is created.
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.simulation.plot_structures(
            x=x,
            y=y,
            z=z,
            ax=ax,
        ) 
[docs]
    def plot_structures_eps(
        self,
        x: float = None,
        y: float = None,
        z: float = None,
        freq: float = None,
        alpha: float = None,
        cbar: bool = True,
        reverse: bool = False,
        ax: Ax = None,
    ) -> Ax:
        """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate.
        The permittivity is plotted in grayscale based on its value at the specified frequency.
        Parameters
        ----------
        x : float = None
            position of plane in x direction, only one of x, y, z must be specified to define plane.
        y : float = None
            position of plane in y direction, only one of x, y, z must be specified to define plane.
        z : float = None
            position of plane in z direction, only one of x, y, z must be specified to define plane.
        freq : float = None
            Frequency to evaluate the relative permittivity of all mediums.
            If not specified, evaluates at infinite frequency.
        reverse : bool = False
            If ``False``, the highest permittivity is plotted in black.
            If ``True``, it is plotteed in white (suitable for black backgrounds).
        cbar : bool = True
            Whether to plot a colorbar for the relative permittivity.
        alpha : float = None
            Opacity of the structures being plotted.
            Defaults to the structure default alpha.
        ax : matplotlib.axes._subplots.Axes = None
            Matplotlib axes to plot on, if not specified, one is created.
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.simulation.plot_structures_eps(
            x=x,
            y=y,
            z=z,
            freq=freq,
            alpha=alpha,
            cbar=cbar,
            reverse=reverse,
            ax=ax,
        ) 
[docs]
    def plot_grid(
        self,
        x: float = None,
        y: float = None,
        z: float = None,
        ax: Ax = None,
        **kwargs,
    ) -> Ax:
        """Plot the cell boundaries as lines on a plane defined by one nonzero x,y,z coordinate.
        Parameters
        ----------
        x : float = None
            position of plane in x direction, only one of x, y, z must be specified to define plane.
        y : float = None
            position of plane in y direction, only one of x, y, z must be specified to define plane.
        z : float = None
            position of plane in z direction, only one of x, y, z must be specified to define plane.
        ax : matplotlib.axes._subplots.Axes = None
            Matplotlib axes to plot on, if not specified, one is created.
        **kwargs
            Optional keyword arguments passed to the matplotlib ``LineCollection``.
            For details on accepted values, refer to
            `Matplotlib's documentation <https://tinyurl.com/2p97z4cn>`_.
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.simulation.plot_grid(
            x=x,
            y=y,
            z=z,
            ax=ax,
            **kwargs,
        ) 
[docs]
    def plot_field(
        self,
        field_name: str,
        val: Literal["real", "imag", "abs"] = "real",
        eps_alpha: float = 0.2,
        robust: bool = True,
        vmin: float = None,
        vmax: float = None,
        ax: Ax = None,
        **sel_kwargs,
    ) -> Ax:
        """Plot the field for a :class:`.ModeSolverData` with :class:`.Simulation` plot overlaid.
        Parameters
        ----------
        field_name : str
            Name of ``field`` component to plot (eg. ``'Ex'``).
            Also accepts ``'E'`` and ``'H'`` to plot the vector magnitudes of the electric and
            magnetic fields, and ``'S'`` for the Poynting vector.
        val : Literal['real', 'imag', 'abs', 'abs^2', 'dB'] = 'real'
            Which part of the field to plot.
        eps_alpha : float = 0.2
            Opacity of the structure permittivity.
            Must be between 0 and 1 (inclusive).
        robust : bool = True
            If True and vmin or vmax are absent, uses the 2nd and 98th percentiles of the data
            to compute the color limits. This helps in visualizing the field patterns especially
            in the presence of a source.
        vmin : float = None
            The lower bound of data range that the colormap covers. If ``None``, they are
            inferred from the data and other keyword arguments.
        vmax : float = None
            The upper bound of data range that the colormap covers. If ``None``, they are
            inferred from the data and other keyword arguments.
        ax : matplotlib.axes._subplots.Axes = None
            matplotlib axes to plot on, if not specified, one is created.
        sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data.
            These kwargs can select over the spatial dimensions (``x``, ``y``, ``z``),
            frequency or time dimensions (``f``, ``t``) or `mode_index`, if applicable.
            For the plotting to work appropriately, the resulting data after selection must contain
            only two coordinates with len > 1.
            Furthermore, these should be spatial coordinates (``x``, ``y``, or ``z``).
        Returns
        -------
        matplotlib.axes._subplots.Axes
            The supplied or created matplotlib axes.
        """
        return self.mode_solver.plot_field(
            field_name=field_name,
            val=val,
            eps_alpha=eps_alpha,
            robust=robust,
            vmin=vmin,
            vmax=vmax,
            ax=ax,
            **sel_kwargs,
        )