Source code for tidy3d.components.tcad.data.monitor_data.charge

"""Monitor level data, store the DataArrays associated with a single heat-charge monitor."""

from __future__ import annotations

from typing import Union

import numpy as np
import pydantic.v1 as pd

from tidy3d.components.base import skip_if_fields_missing
from tidy3d.components.data.data_array import (
    DataArray,
    IndexedFieldVoltageDataArray,
    IndexedVoltageDataArray,
    PointDataArray,
    SpatialDataArray,
    SteadyVoltageDataArray,
)
from tidy3d.components.data.utils import TetrahedralGridDataset, TriangularGridDataset
from tidy3d.components.tcad.data.monitor_data.abstract import HeatChargeMonitorData
from tidy3d.components.tcad.monitors.charge import (
    SteadyCapacitanceMonitor,
    SteadyElectricFieldMonitor,
    SteadyEnergyBandMonitor,
    SteadyFreeCarrierMonitor,
    SteadyPotentialMonitor,
)
from tidy3d.components.types import TYPE_TAG_STR, Ax, annotate_type
from tidy3d.components.viz import add_ax_if_none
from tidy3d.exceptions import DataError
from tidy3d.log import log

FieldDataset = Union[
    SpatialDataArray, annotate_type(Union[TriangularGridDataset, TetrahedralGridDataset])
]

UnstructuredFieldType = Union[TriangularGridDataset, TetrahedralGridDataset]


[docs] class SteadyPotentialData(HeatChargeMonitorData): """Stores electric potential :math:`\\psi` from a charge simulation.""" monitor: SteadyPotentialMonitor = pd.Field( ..., title="Electric potential monitor", description="Electric potential monitor associated with a `charge` simulation.", ) potential: FieldDataset = pd.Field( None, title="Electric potential series", description="Contains the electric potential series.", ) @property def field_components(self) -> dict[str, DataArray]: """Maps the field components to their associated data.""" return {"potential": self.potential}
[docs] @pd.validator("potential", always=True) @skip_if_fields_missing(["monitor"]) def warn_no_data(cls, val, values): """Warn if no data provided.""" mnt = values.get("monitor") if val is None: log.warning( f"No data is available for monitor '{mnt.name}'. This is typically caused by " "monitor not intersecting any solid medium." ) return val
@property def symmetry_expanded_copy(self) -> SteadyPotentialData: """Return copy of self with symmetry applied.""" new_potential = self._symmetry_expanded_copy(property=self.potential) return self.updated_copy(potential=new_potential, symmetry=(0, 0, 0))
[docs] def field_name(self, val: str) -> str: """Gets the name of the fields to be plotted.""" if val == "abs^2": return "|V|²" else: return "V"
[docs] class SteadyFreeCarrierData(HeatChargeMonitorData): """ Stores free-carrier concentration in charge simulations. Notes ----- This data contains the carrier concentrations: the amount of electrons and holes per unit volume as defined in the ``monitor``. """ monitor: SteadyFreeCarrierMonitor = pd.Field( ..., title="Free carrier monitor", description="Free carrier data associated with a Charge simulation.", ) electrons: UnstructuredFieldType = pd.Field( None, title="Electrons series", description=r"Contains the computed electrons concentration $n$.", discriminator=TYPE_TAG_STR, ) # n = electrons holes: UnstructuredFieldType = pd.Field( None, title="Holes series", description=r"Contains the computed holes concentration $p$.", discriminator=TYPE_TAG_STR, ) # p = holes @property def field_components(self) -> dict[str, DataArray]: """Maps the field components to their associated data.""" return {"electrons": self.electrons, "holes": self.holes}
[docs] @pd.root_validator(skip_on_failure=True) def check_correct_data_type(cls, values): """Issue error if incorrect data type is used""" mnt = values.get("monitor") field_data = {field: values.get(field) for field in ["electrons", "holes"]} for field, data in field_data.items(): if isinstance(data, TetrahedralGridDataset) or isinstance(data, TriangularGridDataset): if not isinstance(data.values, IndexedVoltageDataArray): raise ValueError( f"In the data associated with monitor {mnt}, the field {field} does not contain " "data associated to any voltage value." ) return values
[docs] @pd.root_validator(skip_on_failure=True) def warn_no_data(cls, values): """Warn if no data provided.""" mnt = values.get("monitor") electrons = values.get("electrons") holes = values.get("holes") if electrons is None or holes is None: log.warning( f"No data is available for monitor '{mnt.name}'. This is typically caused by " "monitor not intersecting any solid medium." ) return values
@property def symmetry_expanded_copy(self) -> SteadyFreeCarrierData: """Return copy of self with symmetry applied.""" new_electrons = self._symmetry_expanded_copy(property=self.electrons) new_holes = self._symmetry_expanded_copy(property=self.holes) return self.updated_copy( electrons=new_electrons, holes=new_holes, symmetry=(0, 0, 0), )
[docs] def field_name(self, val: str = "") -> str: """Gets the name of the fields to be plotted.""" if val == "abs^2": return "Electrons², Holes²" else: return "Electrons, Holes"
class SteadyEnergyBandData(HeatChargeMonitorData): """ Stores energy bands in charge simulations. Notes ----- This data contains the energy bands data: Ec -> Energy of the bottom of the conduction band, [eV] Ev -> Energy of the top of the valence band, [eV] Ei -> Intrinsic Fermi level, [eV] Efn -> Quasi-Fermi level for electrons, [eV] Efp -> Quasi-Fermi level for holes, [eV] as defined in the ``monitor``. """ monitor: SteadyEnergyBandMonitor = pd.Field( ..., title="Energy band monitor", description="Energy bands data associated with a Charge simulation.", ) Ec: UnstructuredFieldType = pd.Field( None, title="Conduction band series", description=r"Contains the computed energy of the bottom of the conduction band $Ec$.", discriminator=TYPE_TAG_STR, ) Ev: UnstructuredFieldType = pd.Field( None, title="Valence band series", description=r"Contains the computed energy of the top of the valence band $Ec$.", discriminator=TYPE_TAG_STR, ) Ei: UnstructuredFieldType = pd.Field( None, title="Intrinsic Fermi level series", description=r"Contains the computed intrinsic Fermi level for the material $Ei$.", discriminator=TYPE_TAG_STR, ) Efn: UnstructuredFieldType = pd.Field( None, title="Electron's quasi-Fermi level series", description=r"Contains the computed quasi-Fermi level for electrons $Efn$.", discriminator=TYPE_TAG_STR, ) Efp: UnstructuredFieldType = pd.Field( None, title="Hole's quasi-Fermi level series", description=r"Contains the computed quasi-Fermi level for holes $Efp$.", discriminator=TYPE_TAG_STR, ) @property def field_components(self) -> dict[str, DataArray]: """Maps the field components to their associated data.""" return {"Ec": self.Ec, "Ev": self.Ev, "Ei": self.Ei, "Efn": self.Efn, "Efp": self.Efp} @pd.root_validator(skip_on_failure=True) def check_correct_data_type(cls, values): """Issue error if incorrect data type is used""" mnt = values.get("monitor") field_data = {field: values.get(field) for field in ["Ec", "Ev", "Ei", "Efn", "Efp"]} for field, data in field_data.items(): if isinstance(data, TetrahedralGridDataset) or isinstance(data, TriangularGridDataset): if not isinstance(data.values, IndexedVoltageDataArray): raise ValueError( f"In the data associated with monitor {mnt}, the field {field} does not contain " "data associated to any voltage value." ) return values @pd.root_validator(skip_on_failure=True) def warn_no_data(cls, values): """Warn if no data provided.""" mnt = values.get("monitor") fields = ["Ec", "Ev", "Ei", "Efn", "Efp"] for field_name in fields: field_data = values.get(field_name) if field_data is None: log.warning( f"No data is available for monitor '{mnt.name}'. This is typically caused by " "monitor not intersecting any solid medium." ) return values @property def symmetry_expanded_copy(self) -> SteadyEnergyBandData: """Return copy of self with symmetry applied.""" new_Ec = self._symmetry_expanded_copy(property=self.Ec) new_Ev = self._symmetry_expanded_copy(property=self.Ev) new_Ei = self._symmetry_expanded_copy(property=self.Ei) new_Efn = self._symmetry_expanded_copy(property=self.Efn) new_Efp = self._symmetry_expanded_copy(property=self.Efp) return self.updated_copy( Ec=new_Ec, Ev=new_Ev, Ei=new_Ei, Efn=new_Efn, Efp=new_Efp, symmetry=(0, 0, 0), ) def field_name(self, val: str = "") -> str: """Gets the name of the fields to be plotted.""" if val == "abs^2": return "|Ec|², |Ev|², |Ei|², |Efn|², |Efp|²" else: return "Ec, Ev, Ei, Efn, Efp" @add_ax_if_none def plot(self, ax: Ax = None, **sel_kwargs) -> Ax: """Plot the 1D cross-section of the energy bandgap diagram. Parameters ---------- ax : matplotlib.axes._subplots.Axes = None matplotlib axes to plot on, if not specified, one is created. sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. These kwargs can select over the spatial dimensions (``x``, ``y``, or ``z``) and the bias voltage (``voltage``). For the plotting to work appropriately, the resulting data after selection must contain only one coordinate with len > 1. Furthermore, these should be spatial coordinates (``x``, ``y``, or ``z``). Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ selection_data = {} if ("voltage" not in sel_kwargs) and (self.Ec.values.coords.sizes["voltage"] > 1): raise DataError( "'voltage' is not selected for the plot with multiple voltage data points." ) selection_data = {coord: sel_kwargs[coord] for coord in "xyz" if coord in sel_kwargs.keys()} N_coords = len(selection_data.keys()) if "voltage" in sel_kwargs: selection_data["voltage"] = sel_kwargs["voltage"] if isinstance(self.Ec, TetrahedralGridDataset): if N_coords != 2: raise DataError( "2 spatial coordinate values have to be defined to plot the 1D cross-section figure for a 3D dataset." ) elif isinstance(self.Ec, TriangularGridDataset): if N_coords != 1: raise DataError( "1 spatial coordinate value has to be defined to plot the 1D cross-section figure for a 2D dataset." ) for index, coord_name in enumerate(["x", "y", "z"]): if coord_name in selection_data: axis = index continue if axis == self.Ec.normal_axis: raise DataError( f"Triangular grid (normal: {self.Ec.normal_axis}) cannot be sliced by a parallel plane." ) Ec_data = self.Ec Ev_data = self.Ev Ei_data = self.Ei Efn_data = self.Efn Efp_data = self.Efp for coord_name, coord_val in selection_data.items(): Ec_data = Ec_data.sel(**{coord_name: coord_val}, method="nearest") Ev_data = Ev_data.sel(**{coord_name: coord_val}, method="nearest") Ei_data = Ei_data.sel(**{coord_name: coord_val}, method="nearest") Efn_data = Efn_data.sel(**{coord_name: coord_val}, method="nearest") Efp_data = Efp_data.sel(**{coord_name: coord_val}, method="nearest") Ec_data.plot(ax=ax, label="Ec") Ev_data.plot(ax=ax, label="Ev") Ei_data.plot(ax=ax, label="Ei") Efn_data.plot(ax=ax, label="Efn") Efp_data.plot(ax=ax, label="Efp") ax.legend() return ax
[docs] class SteadyCapacitanceData(HeatChargeMonitorData): """ Class that stores capacitance data from a Charge simulation. Notes ----- The small signal-capacitance of electrons :math:`C_n` and holes :math:`C_p` is computed from the charge due to electrons :math:`Q_n` and holes :math:`Q_p` at an applied voltage :math:`V` at a voltage difference :math:`\\Delta V` between two simulations. .. math:: C_{n,p} = \\frac{Q_{n,p}(V + \\Delta V) - Q_{n,p}(V)}{\\Delta V} This is only computed when a voltage source with more than two sources is included within the simulation and determines the :math:`\\Delta V`. """ monitor: SteadyCapacitanceMonitor = pd.Field( ..., title="Capacitance monitor", description="Capacitance data associated with a Charge simulation.", ) hole_capacitance: SteadyVoltageDataArray = pd.Field( None, title="Hole capacitance", description=r"Small signal capacitance ($\frac{dQ_p}{dV}$) associated to the monitor.", ) # C_p = hole_capacitance electron_capacitance: SteadyVoltageDataArray = pd.Field( None, title="Electron capacitance", description=r"Small signal capacitance ($\frac{dQn}{dV}$) associated to the monitor.", ) # C_n = electron_capacitance
[docs] @pd.validator("hole_capacitance", always=True) @skip_if_fields_missing(["monitor"]) def warn_no_data(cls, val, values): """Warn if no data provided.""" mnt = values.get("monitor") if val is None: log.warning( f"No data is available for monitor '{mnt.name}'. This is typically caused by " "monitor not intersecting any solid medium." ) return val
[docs] def field_name(self, val: str) -> str: """Gets the name of the fields to be plotted.""" return ""
@property def symmetry_expanded_copy(self) -> SteadyCapacitanceData: """Return copy of self with symmetry applied.""" num_symmetries = np.sum(np.array([1 if d > 0 else 0 for d in self.symmetry])) scaling_factor = np.power(2, num_symmetries) if self.hole_capacitance is None: new_hole_capacitance = None else: new_values = self.hole_capacitance.values * scaling_factor new_hole_capacitance = SteadyVoltageDataArray( data=new_values, coords=self.hole_capacitance.coords ) if self.electron_capacitance is None: new_electron_capacitance = None else: new_values = self.electron_capacitance.values * scaling_factor new_electron_capacitance = SteadyVoltageDataArray( data=new_values, coords=self.electron_capacitance.coords ) return self.updated_copy( hole_capacitance=new_hole_capacitance, electron_capacitance=new_electron_capacitance, symmetry=(0, 0, 0), )
class SteadyElectricFieldData(HeatChargeMonitorData): """ Stores electric field :math:`\\vec{E}` from a charge simulation. Notes ----- The electric field is computed as the negative gradient of the electric potential :math:`\\vec{E} = -\\nabla \\psi`. It is given in units of :math:`V/\\mu m` (Volts per micrometer). """ monitor: SteadyElectricFieldMonitor = pd.Field( ..., title="Electric field monitor", description="Electric field data associated with a Charge simulation.", ) E: UnstructuredFieldType = pd.Field( None, title="Electric field", description=r"Contains the computed electric field in :math:`V/\\mu m`.", discriminator=TYPE_TAG_STR, ) @property def field_components(self) -> dict[str, UnstructuredFieldType]: """Maps the field components to their associated data.""" return {"E": self.E} @pd.root_validator(skip_on_failure=True) def warn_no_data(cls, values): """Warn if no data provided.""" mnt = values.get("monitor") E = values.get("E") if E is None: log.warning( f"No data is available for monitor '{mnt.name}'. This is typically caused by " "monitor not intersecting any solid medium." ) return values @pd.root_validator(skip_on_failure=True) def check_correct_data_type(cls, values): """Issue error if incorrect data type is used""" mnt = values.get("monitor") E = values.get("E") if isinstance(E, TetrahedralGridDataset) or isinstance(E, TriangularGridDataset): AcceptedTypes = (IndexedFieldVoltageDataArray, PointDataArray) if not isinstance(E.values, AcceptedTypes): raise ValueError( f"In the data associated with monitor {mnt}, must contain a field. This can be " "defined with IndexedFieldVoltageDataArray or PointDataArray." ) return values @property def symmetry_expanded_copy(self) -> SteadyElectricFieldData: """Return copy of self with symmetry applied.""" new_E = self._symmetry_expanded_copy(property=self.E) return self.updated_copy( E=new_E, symmetry=(0, 0, 0), ) def field_name(self, val: str = "") -> str: """Gets the name of the fields to be plotted.""" if val == "abs^2": return "E²" else: return "E"