Source code for tidy3d.components.base_sim.data.sim_data

"""Abstract base for simulation data structures."""

from __future__ import annotations

import pathlib
from abc import ABC
from typing import TYPE_CHECKING, Any, Optional

import numpy as np
from pydantic import Field, field_validator, model_validator

from tidy3d.components.base import Tidy3dBaseModel
from tidy3d.components.base_sim.data.monitor_data import AbstractMonitorData
from tidy3d.components.base_sim.simulation import AbstractSimulation
from tidy3d.components.file_util import replace_values
from tidy3d.exceptions import (
    DataError,
    FileError,
    Tidy3dKeyError,
    ValidationError,
    format_chained_exception_message,
)

if TYPE_CHECKING:
    from os import PathLike
    from typing import Union

    import xarray as xr

    from tidy3d.compat import Self
    from tidy3d.components.data.utils import UnstructuredGridDatasetType
    from tidy3d.components.monitor import AbstractMonitor
    from tidy3d.components.types import FieldVal


[docs] class AbstractSimulationData(Tidy3dBaseModel, ABC): """Stores data from a collection of :class:`AbstractMonitor` objects in a :class:`~tidy3d.components.base_sim.simulation.AbstractSimulation`. """ simulation: AbstractSimulation = Field( title="Simulation", description="Original :class:`~tidy3d.components.base_sim.simulation.AbstractSimulation` associated with the data.", ) data: tuple[AbstractMonitorData, ...] = Field( title="Monitor Data", description="List of :class:`~tidy3d.components.base_sim.data.monitor_data.AbstractMonitorData` instances " "associated with the monitors of the original :class:`~tidy3d.components.base_sim.simulation.AbstractSimulation`.", ) log: Optional[str] = Field( None, title="Solver Log", description="A string containing the log information from the simulation run.", ) def __getitem__(self, monitor_name: str) -> AbstractMonitorData: """Get a :class:`.AbstractMonitorData` by name. Apply symmetry if applicable.""" monitor_data = self.monitor_data[monitor_name] return monitor_data.symmetry_expanded_copy @property def monitor_data(self) -> dict[str, AbstractMonitorData]: """Dictionary mapping monitor name to its associated :class:`~tidy3d.components.base_sim.data.monitor_data.AbstractMonitorData`.""" return {monitor_data.monitor.name: monitor_data for monitor_data in self.data}
[docs] @model_validator(mode="after") def data_monitors_match_sim(self) -> Self: """Ensure each :class:`~tidy3d.components.base_sim.data.monitor_data.AbstractMonitorData` in ``.data`` corresponds to a monitor in ``.simulation``. """ sim = self.simulation for mnt_data in self.data: try: monitor_name = mnt_data.monitor.name sim.get_monitor_by_name(monitor_name) except Tidy3dKeyError as exc: raise DataError( format_chained_exception_message( f"Data with monitor name '{monitor_name}' supplied " f"but not found in the original '{sim.type}'", exc, ) ) from exc return self
[docs] @field_validator("data") @classmethod def validate_no_ambiguity( cls, val: tuple[AbstractMonitorData, ...] ) -> tuple[AbstractMonitorData, ...]: """Ensure all :class:`~tidy3d.components.base_sim.data.monitor_data.AbstractMonitorData` entries in ``.data`` correspond to different monitors in ``.simulation``. """ names = [mnt_data.monitor.name for mnt_data in val] if len(set(names)) != len(names): raise ValidationError("Some entries of '.data' provide data for same monitor(s).") return val
@staticmethod def _field_component_value( field_component: Union[xr.DataArray, UnstructuredGridDatasetType], val: FieldVal ) -> xr.DataArray: """return the desired value of a field component. Parameter ---------- field_component : Union[xarray.DataArray, UnstructuredGridDatasetType] Field component from which to calculate the value. val : Literal['real', 'imag', 'abs', 'abs^2', 'phase'] Which part of the field to return. Returns ------- xarray.DataArray Value extracted from the field component. """ if val in ("real", "re"): field_value = field_component.real field_value = field_value.rename(f"Re{{{field_component.name}}}") elif val in ("imag", "im"): field_value = field_component.imag field_value = field_value.rename(f"Im{{{field_component.name}}}") elif val == "abs": field_value = np.abs(field_component) field_value = field_value.rename(f"|{field_component.name}|") elif val == "abs^2": field_value = np.abs(field_component) ** 2 field_value = field_value.rename(f"|{field_component.name}|²") elif val == "phase": field_value = np.arctan2(field_component.imag, field_component.real) field_value = field_value.rename(f"∠{field_component.name}") else: raise Tidy3dKeyError( f"Couldn't find 'val={val}'. Must be one of 'real', 're', 'imag', 'im', 'abs', 'abs^2', 'phase'." ) return field_value @staticmethod def _apply_log_scale( field_data: xr.DataArray, vmin: Optional[float] = None, db_factor: float = 1.0, ) -> xr.DataArray: """Prepare field data for log-scale plotting by handling zeros. Takes absolute value of the data, replaces zeros with a fill value (to prevent log10(0) warnings), and applies log10 scaling. Parameters ---------- field_data : xr.DataArray The field data to prepare. vmin : float, optional The minimum value for the color scale. If provided, zeros are replaced with ``10 ** (vmin / db_factor)`` instead of NaN. db_factor : float Factor to multiply the log10 result by (e.g., 20 for dB scale of field, 10 for dB scale of power). Default is 1 (pure log10 scale). Returns ------- xr.DataArray The log-scaled field data. """ fill_val = np.nan if vmin is not None: fill_val = 10 ** (vmin / db_factor) field_data = np.abs(field_data) field_data = field_data.where((field_data > 0) | np.isnan(field_data), fill_val) return db_factor * np.log10(field_data)
[docs] def get_monitor_by_name(self, name: str) -> AbstractMonitor: """Return monitor named 'name'.""" return self.simulation.get_monitor_by_name(name)
[docs] def to_mat_file(self, fname: PathLike, **kwargs: Any) -> None: """Output the simulation data object as ``.mat`` MATLAB file. Parameters ---------- fname : PathLike Full path to the output file. Should include ``.mat`` file extension. **kwargs : dict, optional Extra arguments to ``scipy.io.savemat``: see ``scipy`` documentation for more detail. Example ------- >>> sim_data.to_mat_file('/path/to/file/data.mat') # doctest: +SKIP """ # Check .mat file extension is given extension = pathlib.Path(fname).suffixes[0].lower() if len(extension) == 0: raise FileError(f"File '{fname}' missing extension.") if extension != ".mat": raise FileError(f"File '{fname}' should have a .mat extension.") # Handle m_dict in kwargs if "m_dict" in kwargs: raise ValueError( "'m_dict' is automatically determined by 'to_mat_file', can't pass to 'savemat'." ) # Get SimData object as dictionary sim_dict = self.model_dump() # set long field names true by default, otherwise it wont save fields with > 31 characters if "long_field_names" not in kwargs: kwargs["long_field_names"] = True # Remove NoneType values from dict # Built from theory discussed in https://github.com/scipy/scipy/issues/3488 modified_sim_dict = replace_values(sim_dict, None, []) try: from scipy.io import savemat savemat(fname, modified_sim_dict, **kwargs) except Exception as e: raise ValueError( format_chained_exception_message( "Could not save supplied simulation data to file. As this is an experimental " "feature, we may not be able to support the contents of your dataset. If you " "receive this error, please feel free to raise an issue on our front end " "repository so we can investigate.", e, ) ) from e