From 287892fc296204c5c4ef98c19cde6888eec80e84 Mon Sep 17 00:00:00 2001 From: dmarek Date: Thu, 4 Jul 2024 15:37:29 -0400 Subject: [PATCH] add plotting to path integrals --- tidy3d/components/viz.py | 41 ++- .../microwave/custom_path_integrals.py | 114 ++++++++- tidy3d/plugins/microwave/path_integrals.py | 240 +++++++++--------- tidy3d/plugins/microwave/viz.py | 54 ++++ .../smatrix/component_modelers/base.py | 8 +- .../smatrix/component_modelers/terminal.py | 4 +- .../plugins/smatrix/ports/coaxial_lumped.py | 1 + tidy3d/plugins/smatrix/ports/wave.py | 2 + 8 files changed, 322 insertions(+), 142 deletions(-) create mode 100644 tidy3d/plugins/microwave/viz.py diff --git a/tidy3d/components/viz.py b/tidy3d/components/viz.py index b26db20f8f..cc341d9eb5 100644 --- a/tidy3d/components/viz.py +++ b/tidy3d/components/viz.py @@ -4,7 +4,7 @@ from functools import wraps from html import escape -from typing import Any +from typing import Any, Self import matplotlib.pyplot as plt import pydantic.v1 as pd @@ -73,18 +73,15 @@ def _plot(*args, **kwargs) -> Ax: """ plot parameters """ -class PlotParams(Tidy3dBaseModel): - """Stores plotting parameters / specifications for a given model.""" +class AbstractPlotParams(Tidy3dBaseModel): + """Abstract class for storing plotting parameters. + Corresponds with select properties of ``matplotlib.artist.Artist``. + """ alpha: Any = pd.Field(1.0, title="Opacity") - edgecolor: Any = pd.Field(None, title="Edge Color", alias="ec") - facecolor: Any = pd.Field(None, title="Face Color", alias="fc") - fill: bool = pd.Field(True, title="Is Filled") - hatch: str = pd.Field(None, title="Hatch Style") zorder: float = pd.Field(None, title="Display Order") - linewidth: pd.NonNegativeFloat = pd.Field(1, title="Line Width", alias="lw") - def include_kwargs(self, **kwargs) -> PlotParams: + def include_kwargs(self, **kwargs) -> Self: """Update the plot params with supplied kwargs.""" update_dict = { key: value @@ -101,6 +98,32 @@ def to_kwargs(self) -> dict: return kwarg_dict +class PathPlotParams(AbstractPlotParams): + """Stores plotting parameters / specifications for a path. + Corresponds with select properties of ``matplotlib.lines.Line2D``. + """ + + color: Any = pd.Field(None, title="Color", alias="c") + linewidth: pd.NonNegativeFloat = pd.Field(2, title="Line Width", alias="lw") + linestyle: str = pd.Field("--", title="Line Style", alias="ls") + marker: Any = pd.Field("o", title="Marker Style") + markeredgecolor: Any = pd.Field(None, title="Marker Edge Color", alias="mec") + markerfacecolor: Any = pd.Field(None, title="Marker Face Color", alias="mfc") + markersize: pd.NonNegativeFloat = pd.Field(10, title="Marker Size", alias="ms") + + +class PlotParams(AbstractPlotParams): + """Stores plotting parameters / specifications for a given model. + Corresponds with select properties of ``matplotlib.patches.Patch``. + """ + + edgecolor: Any = pd.Field(None, title="Edge Color", alias="ec") + facecolor: Any = pd.Field(None, title="Face Color", alias="fc") + fill: bool = pd.Field(True, title="Is Filled") + hatch: str = pd.Field(None, title="Hatch Style") + linewidth: pd.NonNegativeFloat = pd.Field(1, title="Line Width", alias="lw") + + # defaults for different tidy3d objects plot_params_geometry = PlotParams() plot_params_structure = PlotParams() diff --git a/tidy3d/plugins/microwave/custom_path_integrals.py b/tidy3d/plugins/microwave/custom_path_integrals.py index b21a212a91..78a00da9ff 100644 --- a/tidy3d/plugins/microwave/custom_path_integrals.py +++ b/tidy3d/plugins/microwave/custom_path_integrals.py @@ -12,7 +12,8 @@ from ...components.data.data_array import FreqDataArray, FreqModeDataArray, TimeDataArray from ...components.data.monitor_data import FieldData, FieldTimeData, ModeSolverData from ...components.geometry.base import Geometry -from ...components.types import ArrayFloat2D, Axis, Bound, Coordinate +from ...components.types import ArrayFloat2D, Ax, Axis, Bound, Coordinate +from ...components.viz import add_ax_if_none from ...constants import MICROMETER, fp_eps from ...exceptions import DataError, SetupError from .path_integrals import ( @@ -22,6 +23,13 @@ MonitorDataTypes, VoltageIntegralAxisAligned, ) +from .viz import ( + ARROW_CURRENT, + plot_params_current_path, + plot_params_voltage_minus, + plot_params_voltage_path, + plot_params_voltage_plus, +) FieldParameter = Literal["E", "H"] @@ -172,8 +180,8 @@ def from_circular_path( A path integral defined on a circular path. """ - # Helper for generating x,y vertices around a circle in the local coordinate frame - def generate_circle_coordinates(radius, num_points, clockwise): + def generate_circle_coordinates(radius: float, num_points: int, clockwise: bool): + """Helper for generating x,y vertices around a circle in the local coordinate frame.""" sign = 1.0 if clockwise: sign = -1.0 @@ -199,6 +207,7 @@ def generate_circle_coordinates(radius, num_points, clockwise): @cached_property def is_closed_contour(self) -> bool: + """Returns ``true`` when the first vertex equals the last vertex.""" return np.isclose( self.vertices[0, :], self.vertices[-1, :], @@ -261,6 +270,55 @@ def compute_voltage(self, em_field: MonitorDataTypes) -> IntegralResultTypes: voltage = VoltageIntegralAxisAligned._set_data_array_attributes(voltage) return voltage + @add_ax_if_none + def plot( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + **path_kwargs, + ) -> Ax: + """Plot path integral at single (x,y,z) coordinate. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **path_kwargs + Optional keyword arguments passed to the matplotlib plotting of the line. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + axis, position = Geometry.parse_xyz_kwargs(x=x, y=y, z=z) + if axis != self.main_axis or not np.isclose(position, self.position, rtol=fp_eps): + return ax + + plot_params = plot_params_voltage_path.include_kwargs(**path_kwargs) + plot_kwargs = plot_params.to_kwargs() + xs = self.vertices[:, 0] + ys = self.vertices[:, 1] + ax.plot(xs, ys, markevery=[0, -1], **plot_kwargs) + + # Plot special end points + end_kwargs = plot_params_voltage_plus.include_kwargs(**path_kwargs).to_kwargs() + start_kwargs = plot_params_voltage_minus.include_kwargs(**path_kwargs).to_kwargs() + ax.plot(xs[0], ys[0], **start_kwargs) + ax.plot(xs[-1], ys[-1], **end_kwargs) + + return ax + class CustomCurrentIntegral2D(CustomPathIntegral2D): """Class for computing conduction current via Ampère's circuital law on a custom path. @@ -283,3 +341,53 @@ def compute_current(self, em_field: MonitorDataTypes) -> IntegralResultTypes: current = self.compute_integral(field="H", em_field=em_field) current = CurrentIntegralAxisAligned._set_data_array_attributes(current) return current + + @add_ax_if_none + def plot( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + **path_kwargs, + ) -> Ax: + """Plot path integral at single (x,y,z) coordinate. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **path_kwargs + Optional keyword arguments passed to the matplotlib plotting of the line. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + axis, position = Geometry.parse_xyz_kwargs(x=x, y=y, z=z) + if axis != self.main_axis or not np.isclose(position, self.position, rtol=fp_eps): + return ax + + plot_params = plot_params_current_path.include_kwargs(**path_kwargs) + plot_kwargs = plot_params.to_kwargs() + xs = self.vertices[:, 0] + ys = self.vertices[:, 1] + ax.plot(xs, ys, **plot_kwargs) + + # Add arrow at start of contour + ax.annotate( + "", + xytext=(xs[0], ys[0]), + xy=(xs[1], ys[1]), + arrowprops=ARROW_CURRENT, + ) + return ax diff --git a/tidy3d/plugins/microwave/path_integrals.py b/tidy3d/plugins/microwave/path_integrals.py index bae29a1124..a155232fd5 100644 --- a/tidy3d/plugins/microwave/path_integrals.py +++ b/tidy3d/plugins/microwave/path_integrals.py @@ -22,8 +22,16 @@ from ...components.geometry.base import Box from ...components.types import Ax, Axis, Coordinate2D, Direction from ...components.validators import assert_line, assert_plane -from ...constants import AMP, VOLT +from ...components.viz import add_ax_if_none +from ...constants import AMP, VOLT, fp_eps from ...exceptions import DataError, Tidy3dError +from .viz import ( + ARROW_CURRENT, + plot_params_current_path, + plot_params_voltage_minus, + plot_params_voltage_path, + plot_params_voltage_plus, +) MonitorDataTypes = Union[FieldData, FieldTimeData, ModeSolverData] EMScalarFieldType = Union[ScalarFieldDataArray, ScalarFieldTimeDataArray, ScalarModeFieldDataArray] @@ -173,44 +181,7 @@ def main_axis(self) -> Axis: return index raise Tidy3dError("Failed to identify axis.") - # def intersections_plane(self, x: float = None, y: float = None, z: float = None): - # """Returns shapely geometry at plane specified by one non None value of x,y,z. - - # Parameters - # ---------- - # x : float = None - # Position of plane in x direction, only one of x,y,z can be specified to define plane. - # y : float = None - # Position of plane in y direction, only one of x,y,z can be specified to define plane. - # z : float = None - # Position of plane in z direction, only one of x,y,z can be specified to define plane. - - # Returns - # ------- - # List[shapely.geometry.base.BaseGeometry] - # List of 2D shapes that intersect plane. - # For more details refer to - # `Shapely's Documentation `_. - # """ - # axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - # if axis == self.main_axis: - # return [] - - # if not np.isclose(position, self.center[axis], rtol=fp_eps): - # return [] - # z0, (x0, y0) = self.pop_axis(self.center, axis=axis) - # Lz, (Lx, Ly) = self.pop_axis(self.size, axis=axis) - - # minx = x0 - Lx / 2 - # maxx = x0 + Lx / 2 - # miny = y0 - Ly / 2 - # maxy = y0 + Ly / 2 - # P1 = shapely.Point(minx, miny) - # P2 = shapely.Point(maxx, maxy) - # line = shapely.LineString([P1, P2]) - # return [line] - - def _vertices_2D(self, axis) -> tuple[Coordinate2D, Coordinate2D]: + def _vertices_2D(self, axis: Axis) -> tuple[Coordinate2D, Coordinate2D]: """Returns the two vertices of this path in the plane defined by ``axis``.""" min = self.bounds[0] max = self.bounds[1] @@ -221,29 +192,6 @@ def _vertices_2D(self, axis) -> tuple[Coordinate2D, Coordinate2D]: v = [min[1], max[1]] return (u, v) - def plot( - self, - x: float = None, - y: float = None, - z: float = None, - ax: Ax = None, - path_kwargs: dict = None, - endpoint_kwargs: tuple[dict, dict] = None, - ) -> Ax: - """Plot this path.""" - axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - if axis == self.main_axis or not np.isclose(position, self.center[axis]): - return ax - - (xs, ys) = self._vertices_2D(axis) - ax.plot(xs, ys, **path_kwargs) - if endpoint_kwargs[0]: - ax.plot(xs[0], ys[0], **endpoint_kwargs[0]) - if endpoint_kwargs[1]: - ax.plot(xs[1], ys[1], **endpoint_kwargs[1]) - - return ax - class VoltageIntegralAxisAligned(AxisAlignedPathIntegral): """Class for computing the voltage between two points defined by an axis-aligned line.""" @@ -276,40 +224,60 @@ def compute_voltage(self, em_field: MonitorDataTypes) -> IntegralResultTypes: @staticmethod def _set_data_array_attributes(data_array: IntegralResultTypes) -> IntegralResultTypes: + """Add explanatory attributes to the data array.""" data_array.name = "V" return data_array.assign_attrs(units=VOLT, long_name="voltage") + @add_ax_if_none def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, - **plot_kwargs, + **path_kwargs, ) -> Ax: - """Plot this source.""" + """Plot path integral at single (x,y,z) coordinate. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **path_kwargs + Optional keyword arguments passed to the matplotlib plotting of the line. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - if axis == self.main_axis or not np.isclose(position, self.center[axis]): + if axis == self.main_axis or not np.isclose(position, self.center[axis], rtol=fp_eps): return ax - path_style = dict( - marker="o", - linewidth="2", - linestyle="--", - markersize=10, - color="red", - fillstyle="full", - markerfacecolor="white", - markeredgecolor="red", - ) - start_style = dict(color="red", marker="+", markersize=6) - end_style = dict(color="red", marker="_", markersize=6) - if self.sign == "+": - start_style, end_style = end_style, start_style + (xs, ys) = self._vertices_2D(axis) + # Plot the path + plot_params = plot_params_voltage_path.include_kwargs(**path_kwargs) + plot_kwargs = plot_params.to_kwargs() + ax.plot(xs, ys, markevery=[0, -1], **plot_kwargs) - ax = super().plot( - x=x, y=y, z=z, ax=ax, path_kwargs=path_style, endpoint_kwargs=(start_style, end_style) - ) + # Plot special end points + end_kwargs = plot_params_voltage_plus.include_kwargs(**path_kwargs).to_kwargs() + start_kwargs = plot_params_voltage_minus.include_kwargs(**path_kwargs).to_kwargs() + + if self.sign == "-": + start_kwargs, end_kwargs = end_kwargs, start_kwargs + + ax.plot(xs[0], ys[0], **start_kwargs) + ax.plot(xs[1], ys[1], **end_kwargs) return ax @@ -459,64 +427,84 @@ def _to_path_integrals( @staticmethod def _set_data_array_attributes(data_array: IntegralResultTypes) -> IntegralResultTypes: + """Add explanatory attributes to the data array.""" data_array.name = "I" return data_array.assign_attrs(units=AMP, long_name="current") + @add_ax_if_none def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, - **plot_kwargs, + **path_kwargs, ) -> Ax: - """Plot this source.""" + """Plot path integral at single (x,y,z) coordinate. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **path_kwargs + Optional keyword arguments passed to the matplotlib plotting of the line. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - if axis != self.main_axis or not np.isclose(position, self.center[axis]): + if axis != self.main_axis or not np.isclose(position, self.center[axis], rtol=fp_eps): return ax - path_style = dict( - marker="", - linewidth="2", - linestyle="--", - markersize=10, - color="blue", - fillstyle="full", - markerfacecolor="white", - markeredgecolor="red", - ) - + plot_params = plot_params_current_path.include_kwargs(**path_kwargs) + plot_kwargs = plot_params.to_kwargs() path_integrals = self._to_path_integrals() - + # Plot the path for path in path_integrals: - ax = path.plot( - x=x, y=y, z=z, ax=ax, path_kwargs=path_style, endpoint_kwargs=(None, None) - ) (xs, ys) = path._vertices_2D(axis) - X = (xs[0] + xs[1]) / 2 - Y = (ys[0] + ys[1]) / 2 - dx = xs[1] - xs[0] - dy = ys[1] - ys[0] - lscale = 0.001 - ax.annotate( - "", - xytext=(X - 0.5 * lscale * dx, Y - 0.5 * lscale * dy), - xy=(X + 0.5 * lscale * dx, Y + 0.5 * lscale * dy), - arrowprops=dict(arrowstyle="->", color="blue"), - size=40, - ) - # ax.arrow( - # xs[0], - # ys[0], - # dx, - # dy, - # head_width = dx/10, - # shape="full", - # color="blue", - # linewidth=2, - # linestyle="--", - # length_includes_head=True, - # arrowstyle="->", - # #head_width=0.1, - # ) + ax.plot(xs, ys, **plot_kwargs) + + (ax1, ax2) = self.remaining_axes + + # Add arrow to bottom path, unless right path is longer + arrow_path = path_integrals[0] + if self.size[ax2] > self.size[ax1]: + arrow_path = path_integrals[1] + + (xs, ys) = arrow_path._vertices_2D(axis) + X = (xs[0] + xs[1]) / 2 + Y = (ys[0] + ys[1]) / 2 + center = np.array([X, Y]) + dx = xs[1] - xs[0] + dy = ys[1] - ys[0] + direction = np.array([dx, dy]) + segment_length = np.linalg.norm(direction) + unit_dir = direction / segment_length + + # Change direction of arrow depending on sign of current definition + if self.sign == "-": + unit_dir *= -1.0 + # Change direction of arrow when the "y" axis is dropped, + # since the plotted coordinate system will be left-handed (x, z) + if self.main_axis == 1: + unit_dir *= -1.0 + + start = center - unit_dir * segment_length + end = center + ax.annotate( + "", + xytext=(start[0], start[1]), + xy=(end[0], end[1]), + arrowprops=ARROW_CURRENT, + ) return ax diff --git a/tidy3d/plugins/microwave/viz.py b/tidy3d/plugins/microwave/viz.py new file mode 100644 index 0000000000..04c61b812e --- /dev/null +++ b/tidy3d/plugins/microwave/viz.py @@ -0,0 +1,54 @@ +"""Utilities for plotting microwave components""" + +from numpy import inf + +from ...components.viz import PathPlotParams + +""" Constants """ +VOLTAGE_COLOR = "red" +CURRENT_COLOR = "blue" +PATH_LINEWIDTH = 2 +ARROW_CURRENT = dict( + arrowstyle="-|>", + mutation_scale=32, + linestyle="", + lw=PATH_LINEWIDTH, + color=CURRENT_COLOR, +) + +plot_params_voltage_path = PathPlotParams( + alpha=1.0, + zorder=inf, + color=VOLTAGE_COLOR, + linestyle="--", + linewidth=PATH_LINEWIDTH, + marker="o", + markersize=10, + markeredgecolor=VOLTAGE_COLOR, + markerfacecolor="white", +) + +plot_params_voltage_plus = PathPlotParams( + alpha=1.0, + zorder=inf, + color=VOLTAGE_COLOR, + marker="+", + markersize=6, +) + +plot_params_voltage_minus = PathPlotParams( + alpha=1.0, + zorder=inf, + color=VOLTAGE_COLOR, + marker="_", + markersize=6, +) + +plot_params_current_path = PathPlotParams( + alpha=1.0, + zorder=inf, + color=CURRENT_COLOR, + linestyle="--", + linewidth=PATH_LINEWIDTH, + marker="", +) diff --git a/tidy3d/plugins/smatrix/component_modelers/base.py b/tidy3d/plugins/smatrix/component_modelers/base.py index e4693c7693..e15134418a 100644 --- a/tidy3d/plugins/smatrix/component_modelers/base.py +++ b/tidy3d/plugins/smatrix/component_modelers/base.py @@ -18,14 +18,18 @@ from ....exceptions import SetupError, Tidy3dKeyError from ....log import log from ....web.api.container import Batch, BatchData -from ..ports.base_terminal import AbstractTerminalPort +from ..ports.coaxial_lumped import CoaxialLumpedPort from ..ports.modal import Port +from ..ports.rectangular_lumped import LumpedPort from ..ports.wave import WavePort # fwidth of gaussian pulse in units of central frequency FWIDTH_FRAC = 1.0 / 10 DEFAULT_DATA_DIR = "." +LumpedPortType = Union[LumpedPort, CoaxialLumpedPort] +TerminalPortType = Union[LumpedPortType, WavePort] + class AbstractComponentModeler(ABC, Tidy3dBaseModel): """Tool for modeling devices and computing port parameters.""" @@ -36,7 +40,7 @@ class AbstractComponentModeler(ABC, Tidy3dBaseModel): description="Simulation describing the device without any sources present.", ) - ports: Tuple[Union[Port, AbstractTerminalPort], ...] = pd.Field( + ports: Tuple[Union[Port, TerminalPortType], ...] = pd.Field( (), title="Ports", description="Collection of ports describing the scattering matrix elements. " diff --git a/tidy3d/plugins/smatrix/component_modelers/terminal.py b/tidy3d/plugins/smatrix/component_modelers/terminal.py index 0da3dfa5c6..074c9bdacc 100644 --- a/tidy3d/plugins/smatrix/component_modelers/terminal.py +++ b/tidy3d/plugins/smatrix/component_modelers/terminal.py @@ -21,7 +21,7 @@ from ..ports.base_lumped import AbstractLumpedPort from ..ports.base_terminal import AbstractTerminalPort, TerminalPortDataArray from ..ports.wave import WavePort -from .base import FWIDTH_FRAC, AbstractComponentModeler +from .base import FWIDTH_FRAC, AbstractComponentModeler, TerminalPortType class PortDataArray(DataArray): @@ -43,7 +43,7 @@ class TerminalComponentModeler(AbstractComponentModeler): """Tool for modeling two-terminal multiport devices and computing port parameters with lumped and wave ports.""" - ports: Tuple[AbstractTerminalPort, ...] = pd.Field( + ports: Tuple[TerminalPortType, ...] = pd.Field( (), title="Terminal Ports", description="Collection of lumped and wave ports associated with the network. " diff --git a/tidy3d/plugins/smatrix/ports/coaxial_lumped.py b/tidy3d/plugins/smatrix/ports/coaxial_lumped.py index 7d125961b6..9a4982852d 100644 --- a/tidy3d/plugins/smatrix/ports/coaxial_lumped.py +++ b/tidy3d/plugins/smatrix/ports/coaxial_lumped.py @@ -336,6 +336,7 @@ def compute_current(self, sim_data: SimulationData) -> FreqDataArray: @cached_property def _voltage_axis(self) -> Axis: + """Helper to return the chosen voltage axis. We arbitrarily choose the first in-plane axis.""" return self.remaining_axes[0] @cached_property diff --git a/tidy3d/plugins/smatrix/ports/wave.py b/tidy3d/plugins/smatrix/ports/wave.py index 4edb94a852..5e08305244 100644 --- a/tidy3d/plugins/smatrix/ports/wave.py +++ b/tidy3d/plugins/smatrix/ports/wave.py @@ -63,10 +63,12 @@ def injection_axis(self): @cached_property def _field_monitor_name(self) -> str: + """Return the name of the :class:`.FieldMonitor` associated with this port.""" return f"{self.name}_field" @cached_property def _mode_monitor_name(self) -> str: + """Return the name of the :class:`.ModeMonitor` associated with this port.""" return f"{self.name}_mode" def to_source(self, source_time: GaussianPulse, snap_center: float = None) -> ModeSource: