"""Monitor level data, store the DataArrays associated with a single heat-charge monitor."""
from __future__ import annotations
from typing import Dict, Union
import pydantic.v1 as pd
from tidy3d.components.base import skip_if_fields_missing
from tidy3d.components.data.data_array import (
DataArray,
IndexedVoltageDataArray,
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,
SteadyFreeCarrierMonitor,
SteadyPotentialMonitor,
)
from tidy3d.components.types import TYPE_TAG_STR, annotate_type
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 dict(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 plot."""
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 dict(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 plot."""
if val == "abs^2":
return "Electrons², Holes²"
else:
return "Electrons, Holes"
[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 plot."""
return ""
@property
def symmetry_expanded_copy(self) -> SteadyCapacitanceData:
"""Return copy of self with symmetry applied."""
return self