Source code for tidy3d.components.base_sim.simulation

"""Abstract base for defining simulation classes of different solvers"""
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Tuple
from math import isclose

import pydantic.v1 as pd

from .monitor import AbstractMonitor

from ..base import cached_property, skip_if_fields_missing
from ..validators import assert_unique_names, assert_objects_in_sim_bounds
from ..geometry.base import Box
from ..types import Ax, Bound, Axis, Symmetry, TYPE_TAG_STR
from ..structure import Structure
from ..viz import add_ax_if_none, equal_aspect
from ..scene import Scene

from ..medium import Medium, MediumType3D

from ..viz import PlotParams, plot_params_symmetry

from ...version import __version__
from ...exceptions import Tidy3dKeyError
from ...log import log


[docs] class AbstractSimulation(Box, ABC): """Base class for simulation classes of different solvers.""" medium: MediumType3D = pd.Field( Medium(), title="Background Medium", description="Background medium of simulation, defaults to vacuum if not specified.", discriminator=TYPE_TAG_STR, ) """ Background medium of simulation, defaults to vacuum if not specified. """ structures: Tuple[Structure, ...] = pd.Field( (), title="Structures", description="Tuple of structures present in simulation. " "Note: Structures defined later in this list override the " "simulation material properties in regions of spatial overlap.", ) """ Tuple of structures present in simulation. Structures defined later in this list override the simulation material properties in regions of spatial overlap. Example ------- Simple application reference: .. code-block:: python Simulation( ... structures=[ Structure( geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), medium=Medium(permittivity=2.0), ), ], ... ) """ symmetry: Tuple[Symmetry, Symmetry, Symmetry] = pd.Field( (0, 0, 0), title="Symmetries", description="Tuple of integers defining reflection symmetry across a plane " "bisecting the simulation domain normal to the x-, y-, and z-axis " "at the simulation center of each axis, respectively. ", ) sources: Tuple[None, ...] = pd.Field( (), title="Sources", description="Sources in the simulation.", ) boundary_spec: None = pd.Field( None, title="Boundaries", description="Specification of boundary conditions.", ) monitors: Tuple[None, ...] = pd.Field( (), title="Monitors", description="Monitors in the simulation. ", ) grid_spec: None = pd.Field( None, title="Grid Specification", description="Specifications for the simulation grid.", ) version: str = pd.Field( __version__, title="Version", description="String specifying the front end version number.", ) """ Validating setup """ # make sure all names are unique _unique_monitor_names = assert_unique_names("monitors") _unique_structure_names = assert_unique_names("structures") _unique_source_names = assert_unique_names("sources") _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") _structures_in_bounds = assert_objects_in_sim_bounds("structures", error=False) @pd.validator("structures", always=True) @skip_if_fields_missing(["size", "center"]) def _structures_not_at_edges(cls, val, values): """Warn if any structures lie at the simulation boundaries.""" if val is None: return val sim_box = Box(size=values.get("size"), center=values.get("center")) sim_bound_min, sim_bound_max = sim_box.bounds sim_bounds = list(sim_bound_min) + list(sim_bound_max) with log as consolidated_logger: for istruct, structure in enumerate(val): struct_bound_min, struct_bound_max = structure.geometry.bounds struct_bounds = list(struct_bound_min) + list(struct_bound_max) for sim_val, struct_val in zip(sim_bounds, struct_bounds): if isclose(sim_val, struct_val): consolidated_logger.warning( f"Structure at 'structures[{istruct}]' has bounds that extend exactly " "to simulation edges. This can cause unexpected behavior. " "If intending to extend the structure to infinity along one dimension, " "use td.inf as a size variable instead to make this explicit.", custom_loc=["structures", istruct], ) return val """ Post-init validators """ def _post_init_validators(self) -> None: """Call validators taking z`self` that get run after init.""" _ = self.scene
[docs] def validate_pre_upload(self) -> None: """Validate the fully initialized simulation is ok for upload to our servers.""" pass
""" Accounting """ @cached_property def scene(self) -> Scene: """Scene instance associated with the simulation.""" return Scene(medium=self.medium, structures=self.structures)
[docs] def get_monitor_by_name(self, name: str) -> AbstractMonitor: """Return monitor named 'name'.""" for monitor in self.monitors: if monitor.name == name: return monitor raise Tidy3dKeyError(f"No monitor named '{name}'")
@cached_property def simulation_bounds(self) -> Bound: """Simulation bounds including auxiliary boundary zones such as PML layers.""" # in this default implementation we just take self.bounds # this should be changed in different solvers depending on whether automatic extensions # (like pml) are present return self.bounds @cached_property def simulation_geometry(self) -> Box: """The entire simulation domain including auxiliary boundary zones such as PML layers. It is identical to ``Simulation.geometry`` in the absence of such auxiliary zones. """ rmin, rmax = self.simulation_bounds return Box.from_bounds(rmin=rmin, rmax=rmax) @cached_property def simulation_structure(self) -> Structure: """Returns structure representing the domain of the simulation. This differs from ``Simulation.scene.background_structure`` in that it has finite extent.""" return Structure(geometry=self.simulation_geometry, medium=self.medium)
[docs] @equal_aspect @add_ax_if_none def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, source_alpha: float = None, monitor_alpha: float = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ hlim, vlim = Scene._get_plot_lims( bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) ax = self.scene.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = Scene._set_plot_bounds( bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) return ax
[docs] @equal_aspect @add_ax_if_none def plot_sources( self, x: float = None, y: float = None, z: float = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = None, alpha: float = None, ax: Ax = None, ) -> Ax: """Plot each of simulation's sources 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. alpha : float = None Opacity of the sources, 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. """ bounds = self.bounds for source in self.sources: ax = source.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) ax = Scene._set_plot_bounds( bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return ax
[docs] @equal_aspect @add_ax_if_none def plot_monitors( self, x: float = None, y: float = None, z: float = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = None, alpha: float = None, ax: Ax = None, ) -> Ax: """Plot each of simulation's monitors 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. alpha : float = None Opacity of the sources, 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. """ bounds = self.bounds for monitor in self.monitors: ax = monitor.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) ax = Scene._set_plot_bounds( bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return ax
[docs] @equal_aspect @add_ax_if_none def plot_symmetries( self, x: float = None, y: float = None, z: float = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = None, ax: Ax = None, ) -> Ax: """Plot each of simulation's symmetries 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy 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. """ normal_axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) for sym_axis, sym_value in enumerate(self.symmetry): if sym_value == 0 or sym_axis == normal_axis: continue sym_box = self._make_symmetry_box(sym_axis=sym_axis) plot_params = self._make_symmetry_plot_params(sym_value=sym_value) ax = sym_box.plot(x=x, y=y, z=z, ax=ax, **plot_params.to_kwargs()) ax = Scene._set_plot_bounds( bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return ax
def _make_symmetry_plot_params(self, sym_value: Symmetry) -> PlotParams: """Make PlotParams for symmetry.""" plot_params = plot_params_symmetry.copy() if sym_value == 1: plot_params = plot_params.copy( update={"facecolor": "lightsteelblue", "edgecolor": "lightsteelblue", "hatch": "++"} ) elif sym_value == -1: plot_params = plot_params.copy( update={"facecolor": "goldenrod", "edgecolor": "goldenrod", "hatch": "--"} ) return plot_params def _make_symmetry_box(self, sym_axis: Axis) -> Box: """Construct a :class:`.Box` representing the symmetry to be plotted.""" rmin, rmax = (list(bound) for bound in self.simulation_bounds) rmax[sym_axis] = (rmin[sym_axis] + rmax[sym_axis]) / 2 return Box.from_bounds(rmin, rmax)
[docs] @abstractmethod @equal_aspect @add_ax_if_none def plot_boundaries( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs, ) -> Ax: """Plot the simulation boundary conditions 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. """
[docs] @equal_aspect @add_ax_if_none def plot_structures( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ hlim_new, vlim_new = Scene._get_plot_lims( bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return self.scene.plot_structures(x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new)
[docs] @equal_aspect @add_ax_if_none 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, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ hlim, vlim = Scene._get_plot_lims( bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return self.scene.plot_structures_eps( freq=freq, cbar=cbar, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, reverse=reverse, )
[docs] @equal_aspect @add_ax_if_none def plot_structures_heat_conductivity( self, x: float = None, y: float = None, z: float = None, alpha: float = None, cbar: bool = True, reverse: bool = False, ax: Ax = None, hlim: Tuple[float, float] = None, vlim: Tuple[float, float] = 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. hlim : Tuple[float, float] = None The x range if plotting on xy or xz planes, y range if plotting on yz plane. vlim : Tuple[float, float] = None The z range if plotting on xz or yz planes, y plane if plotting on xy plane. Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ hlim, vlim = Scene._get_plot_lims( bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) return self.scene.plot_structures_heat_conductivity( cbar=cbar, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, reverse=reverse, )
[docs] @classmethod def from_scene(cls, scene: Scene, **kwargs) -> AbstractSimulation: """Create a simulation from a :class:.`Scene` instance. Must provide additional parameters to define a valid simulation (for example, ``size``, ``run_time``, ``grid_spec``, etc). Parameters ---------- scene : :class:.`Scene` Scene containing structures information. **kwargs Other arguments """ return cls( structures=scene.structures, medium=scene.medium, **kwargs, )