"""Abstract base for defining simulation classes of different solvers"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Tuple
import autograd.numpy as anp
import pydantic.v1 as pd
from ...exceptions import Tidy3dKeyError
from ...log import log
from ...version import __version__
from ..base import cached_property, skip_if_fields_missing
from ..geometry.base import Box
from ..medium import Medium, MediumType3D
from ..scene import Scene
from ..structure import Structure
from ..types import TYPE_TAG_STR, Ax, Axis, Bound, Symmetry
from ..validators import assert_objects_in_sim_bounds, assert_unique_names
from ..viz import PlotParams, add_ax_if_none, equal_aspect, plot_params_symmetry
from .monitor import AbstractMonitor
[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 """
@pd.root_validator(pre=True)
def _update_simulation(cls, values):
"""Update the simulation if it is an earlier version."""
# dummy upgrade of version number
# this should be overriden by each simulation class if needed
current_version = values.get("version")
if current_version != __version__ and current_version is not None:
log.warning(f"updating {cls.__name__} from {current_version} to {__version__}")
values["version"] = __version__
return values
# 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", strict_inequality=True)
_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 anp.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],
)
continue
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,
)