Source code for tidy3d.components.base_sim.data.monitor_data
"""Abstract base for monitor data structures."""
from __future__ import annotations
import copy
from abc import ABC
from typing import TYPE_CHECKING
import numpy as np
from pydantic import Field
from tidy3d.components.base_sim.monitor import AbstractMonitor
from tidy3d.components.data.data_array import SpatialDataArray
from tidy3d.components.data.dataset import Dataset
from tidy3d.components.types import Coordinate, Symmetry
if TYPE_CHECKING:
from typing import Literal
from xarray import DataArray as XrDataArray
from tidy3d.components.data.utils import UnstructuredGridDatasetType
[docs]
class AbstractMonitorData(Dataset, ABC):
"""Abstract base class of objects that store data pertaining to a single
:class:`AbstractMonitor`.
"""
monitor: AbstractMonitor = Field(
title="Monitor",
description="Monitor associated with the data.",
)
@property
def symmetry_expanded_copy(self) -> AbstractMonitorData:
"""Return copy of self with symmetry applied."""
return self.copy()
class AbstractUnstructuredMonitorData(AbstractMonitorData, ABC):
"""Abstract base class of objects that store data from unstructured monitors."""
symmetry: tuple[Symmetry, Symmetry, Symmetry] = Field(
(0, 0, 0),
title="Symmetry",
description="Symmetry of the original simulation in x, y, and z.",
)
symmetry_center: Coordinate = Field(
(0, 0, 0),
title="Symmetry Center",
description="Symmetry center of the original simulation in x, y, and z.",
)
def _symmetry_expanded_copy_base(
self,
data: UnstructuredGridDatasetType | SpatialDataArray,
custom_symmetry: tuple[Literal[-1, 1] | XrDataArray, ...] | None = None,
) -> UnstructuredGridDatasetType | SpatialDataArray:
"""Return the data with symmetry applied."""
# no symmetry
if all(sym == 0 for sym in self.symmetry):
return data
new_data = copy.copy(data)
mnt_bounds = np.array(self.monitor.bounds)
if isinstance(new_data, SpatialDataArray):
data_bounds = [
[np.min(new_data.x), np.min(new_data.y), np.min(new_data.z)],
[np.max(new_data.x), np.max(new_data.y), np.max(new_data.z)],
]
else:
data_bounds = new_data.bounds
dims_need_clipping_left = []
dims_need_clipping_right = []
for dim in range(3):
# do not expand monitor with zero size along symmetry direction
# this is done because 2d unstructured data does not support this
if self.symmetry[dim] != 0:
symmetry_factor = (
self.symmetry[dim] if custom_symmetry is None else custom_symmetry[dim]
)
center = self.symmetry_center[dim]
if mnt_bounds[1][dim] < data_bounds[0][dim]:
# (note that mnt_bounds[0][dim] < 2 * center - data_bounds[0][dim] will be satisfied based on backend behavior)
# simple reflection
new_data = new_data.reflect(
axis=dim, center=center, reflection_only=True, symmetry=symmetry_factor
)
elif mnt_bounds[0][dim] < 2 * center - data_bounds[0][dim]:
# expand only if monitor bounds missing data
# if we do expand, simply reflect symmetrically the whole data
new_data = new_data.reflect(axis=dim, center=center, symmetry=symmetry_factor)
# if it turns out that we expanded too much, we will trim unnecessary data later
if mnt_bounds[0][dim] > 2 * center - data_bounds[1][dim]:
dims_need_clipping_left.append(dim)
# likewise, if some of original data was only for symmetry expansion, trim excess on the right
if mnt_bounds[1][dim] < data_bounds[1][dim]:
dims_need_clipping_right.append(dim)
# trim over-expanded data
if dims_need_clipping_left or dims_need_clipping_right:
# enlarge clipping domain on positive side arbitrary by 1
# should not matter by how much
clip_bounds = [mnt_bounds[0] - 1, mnt_bounds[1] + 1]
for dim in dims_need_clipping_left:
clip_bounds[0][dim] = mnt_bounds[0][dim]
for dim in dims_need_clipping_right:
clip_bounds[1][dim] = mnt_bounds[1][dim]
if isinstance(new_data, SpatialDataArray):
new_data = new_data.sel_inside(clip_bounds)
else:
new_data = new_data.box_clip(bounds=clip_bounds)
return new_data