From 6f399fd5f99a7f18189d324e42b68bc8e33d64ce Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Thu, 10 Apr 2025 10:11:54 -0700 Subject: [PATCH 1/8] Add effective DOS classes for the Charge solver --- tidy3d/__init__.py | 8 ++ tidy3d/components/material/tcad/charge.py | 5 +- tidy3d/components/tcad/effective_DOS.py | 131 ++++++++++++++++++++++ tidy3d/components/tcad/types.py | 9 ++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tidy3d/components/tcad/effective_DOS.py diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index baa77266e7..dd14758e67 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -52,13 +52,17 @@ from tidy3d.components.tcad.types import ( AugerRecombination, CaugheyThomasMobility, + ConstantEffectiveDOS, ConstantMobilityModel, ConvectionBC, CurrentBC, + DualValleyEffectiveDOS, HeatFluxBC, HeatFromElectricSource, HeatSource, InsulatingBC, + IsotropicEffectiveDOS, + MultiValleyEffectiveDOS, RadiativeRecombination, ShockleyReedHallRecombination, SlotboomBandGapNarrowing, @@ -606,6 +610,10 @@ def set_logging_level(level: str) -> None: "SteadyCapacitanceData", "CaugheyThomasMobility", "ConstantMobilityModel", + "ConstantEffectiveDOS", + "IsotropicEffectiveDOS", + "MultiValleyEffectiveDOS", + "DualValleyEffectiveDOS", "SlotboomBandGapNarrowing", "ShockleyReedHallRecombination", "FossumCarrierLifetime", diff --git a/tidy3d/components/material/tcad/charge.py b/tidy3d/components/material/tcad/charge.py index cadc087d17..adf41b090f 100644 --- a/tidy3d/components/material/tcad/charge.py +++ b/tidy3d/components/material/tcad/charge.py @@ -11,6 +11,7 @@ from tidy3d.components.tcad.doping import DopingBoxType from tidy3d.components.tcad.types import ( BandGapNarrowingModelType, + EffectiveDOSModelType, MobilityModelType, RecombinationModelType, ) @@ -256,14 +257,14 @@ class SemiconductorMedium(AbstractChargeMedium): """ - N_c: pd.PositiveFloat = pd.Field( + N_c: Union[pd.PositiveFloat, EffectiveDOSModelType] = pd.Field( ..., title="Effective density of electron states", description=r"$N_c$ Effective density of states in the conduction band.", units="cm^(-3)", ) - N_v: pd.PositiveFloat = pd.Field( + N_v: Union[pd.PositiveFloat, EffectiveDOSModelType] = pd.Field( ..., title="Effective density of hole states", description=r"$N_v$ Effective density of states in the valence band.", diff --git a/tidy3d/components/tcad/effective_DOS.py b/tidy3d/components/tcad/effective_DOS.py new file mode 100644 index 0000000000..2b5bf1626e --- /dev/null +++ b/tidy3d/components/tcad/effective_DOS.py @@ -0,0 +1,131 @@ +from abc import ABC, abstractmethod + +import numpy as np +import pydantic.v1 as pd + +from tidy3d.components.base import Tidy3dBaseModel +from tidy3d.constants import HBAR + +from ...exceptions import DataError + +# constants definition +k_B = 1.380649e-23 # Boltzmann constant in J/K +m_e = 9.1093837139e-31 # electron mass in kg +DOS_aux_const = 2.0 * np.power((m_e * k_B) / (2 * np.pi * HBAR), 1.5) + + +class EffectiveDOS(Tidy3dBaseModel, ABC): + """Abstract class for the effective density of states""" + + @abstractmethod + def _calc_eff_DOS(self, T: float): + """Abstract method to calculate the effective density of states.""" + pass + + def get_effective_DOS(self, T: float): + if T <= 0: + raise DataError( + f"Incorrect temperature value ({T}) for the effectve density of states calculation." + ) + + return self._calc_eff_DOS(T) + + +class ConstantEffectiveDOS(EffectiveDOS): + """Constant effective density of states model.""" + + N: pd.NonNegativeFloat = pd.Field( + ..., title="Effective DOS", description="Effective density of states", units="cm^(-3)" + ) + + def _calc_eff_DOS(self, T: float): + return self.N + + +class IsotropicEffectiveDOS(EffectiveDOS): + """Effective density of states model that assumes single valley and isotropic effective mass. + The model assumes the standard equation for the 3D semiconductor with parabolic energy dispersion: + + .. math:: + + \\begin{equation} + \\mathbf{N_eff} = 2 * (\\m_eff \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + \\end{equation} + """ + + m_eff: pd.NonNegativeFloat = pd.Field( + ..., + title="Effective mass", + description="Effective mass of the carriers", + units="Electron mass", + ) + + def _calc_eff_DOS(self, T: float): + return np.power(self.m_eff * T, 1.5) * DOS_aux_const + + +class MultiValleyEffectiveDOS(EffectiveDOS): + """Effective density of states model that assumes multiple valleys and anisotropic effective mass. + The model assumes the standard equation for the 3D semiconductor with parabolic energy dispersion: + + .. math:: + + \\begin{equation} + \\mathbf{N_eff} = 2 * \\N_valley (\\m_eff_long * \\m_eff_trans * \\m_eff_trans)^(1/2) (\\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + \\end{equation} + """ + + m_eff_long: pd.NonNegativeFloat = pd.Field( + ..., + title="Longitudinal effective mass", + description="Effective mass of the carriers in the longitudinal direction", + units="Electron mass", + ) + + m_eff_trans: pd.NonNegativeFloat = pd.Field( + ..., + title="Longitudinal effective mass", + description="Effective mass of the carriers in the transverse direction", + units="Electron mass", + ) + + N_valley: pd.NonNegativeInt = pd.Field( + ..., title="Nnmber of valleys", description="Number of valleys in the energy band" + ) + + def _calc_eff_DOS(self, T: float): + return ( + self.N_valley + * np.power(self.m_eff_long * self.m_eff_trans * self.m_eff_trans, 0.5) + * np.power(T, 1.5) + * DOS_aux_const + ) + + +class DualValleyEffectiveDOS(EffectiveDOS): + """Effective density of states model that assumes combibation of light holes and heavy holes with isotropic effective masses. + The model assumes the standard equation for the 3D semiconductor with parabolic energy dispersion: + + .. math:: + + \\begin{equation} + \\mathbf{N_eff} = 2 * ( (\\m_eff_lh \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + (\\m_eff_hh \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) ) + \\end{equation} + """ + + m_eff_lh: pd.NonNegativeFloat = pd.Field( + ..., + title="Light hole effective mass", + description="Effective mass of the light holes", + units="Electron mass", + ) + + m_eff_hh: pd.NonNegativeFloat = pd.Field( + ..., + title="Heavy hole effective mass", + description="Effective mass of the heavy holes", + units="Electron mass", + ) + + def _calc_eff_DOS(self, T: float): + return (np.power(self.m_eff_lh * T, 1.5) + np.power(self.m_eff_hh * T, 1.5)) * DOS_aux_const diff --git a/tidy3d/components/tcad/types.py b/tidy3d/components/tcad/types.py index 7861585564..21e3f02085 100644 --- a/tidy3d/components/tcad/types.py +++ b/tidy3d/components/tcad/types.py @@ -3,6 +3,12 @@ from tidy3d.components.tcad.bandgap import SlotboomBandGapNarrowing from tidy3d.components.tcad.boundary.charge import CurrentBC, InsulatingBC, VoltageBC from tidy3d.components.tcad.boundary.heat import ConvectionBC, HeatFluxBC, TemperatureBC +from tidy3d.components.tcad.effective_DOS import ( + ConstantEffectiveDOS, + DualValleyEffectiveDOS, + IsotropicEffectiveDOS, + MultiValleyEffectiveDOS, +) from tidy3d.components.tcad.generation_recombination import ( AugerRecombination, RadiativeRecombination, @@ -19,6 +25,9 @@ from tidy3d.components.tcad.source.heat import HeatSource, UniformHeatSource from tidy3d.components.types import Union +EffectiveDOSModelType = Union[ + ConstantEffectiveDOS, IsotropicEffectiveDOS, MultiValleyEffectiveDOS, DualValleyEffectiveDOS +] MobilityModelType = Union[CaugheyThomasMobility, ConstantMobilityModel] RecombinationModelType = Union[ AugerRecombination, RadiativeRecombination, ShockleyReedHallRecombination From c267f478f6508e7fd96fdc0274434a82c20f59de Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Thu, 10 Apr 2025 10:11:54 -0700 Subject: [PATCH 2/8] Add effective DOS classes for the Charge solver --- tidy3d/components/tcad/effective_DOS.py | 32 +++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tidy3d/components/tcad/effective_DOS.py b/tidy3d/components/tcad/effective_DOS.py index 2b5bf1626e..e62c18d35e 100644 --- a/tidy3d/components/tcad/effective_DOS.py +++ b/tidy3d/components/tcad/effective_DOS.py @@ -4,14 +4,17 @@ import pydantic.v1 as pd from tidy3d.components.base import Tidy3dBaseModel -from tidy3d.constants import HBAR from ...exceptions import DataError # constants definition k_B = 1.380649e-23 # Boltzmann constant in J/K m_e = 9.1093837139e-31 # electron mass in kg -DOS_aux_const = 2.0 * np.power((m_e * k_B) / (2 * np.pi * HBAR), 1.5) +Planck_hbar = 1.054571817e-34 # reduced Planck constant in J*s +m_3_to_cm_3 = 1e-6 # conversion factor from m^(-3) to cm^(-3) +DOS_aux_const = ( + 2.0 * np.power((m_e * k_B) / (2 * np.pi * Planck_hbar * Planck_hbar), 1.5) * m_3_to_cm_3 +) class EffectiveDOS(Tidy3dBaseModel, ABC): @@ -22,6 +25,11 @@ def _calc_eff_DOS(self, T: float): """Abstract method to calculate the effective density of states.""" pass + @abstractmethod + def _calc_eff_DOS_derivative(self, T: float): + """Abstract method to calculate the temperature derivative of the effective density of states.""" + pass + def get_effective_DOS(self, T: float): if T <= 0: raise DataError( @@ -30,6 +38,14 @@ def get_effective_DOS(self, T: float): return self._calc_eff_DOS(T) + def get_effective_DOS_derivative(self, T: float): + if T <= 0: + raise DataError( + f"Incorrect temperature value ({T}) for the effectve density of states calculation." + ) + + return self._calc_eff_DOS_derivative(T) + class ConstantEffectiveDOS(EffectiveDOS): """Constant effective density of states model.""" @@ -41,6 +57,9 @@ class ConstantEffectiveDOS(EffectiveDOS): def _calc_eff_DOS(self, T: float): return self.N + def _calc_eff_DOS_derivative(self, T: float): + return 0.0 + class IsotropicEffectiveDOS(EffectiveDOS): """Effective density of states model that assumes single valley and isotropic effective mass. @@ -63,6 +82,9 @@ class IsotropicEffectiveDOS(EffectiveDOS): def _calc_eff_DOS(self, T: float): return np.power(self.m_eff * T, 1.5) * DOS_aux_const + def _calc_eff_DOS_derivative(self, T: float): + return self._calc_eff_DOS(T) * 1.5 / T + class MultiValleyEffectiveDOS(EffectiveDOS): """Effective density of states model that assumes multiple valleys and anisotropic effective mass. @@ -101,6 +123,9 @@ def _calc_eff_DOS(self, T: float): * DOS_aux_const ) + def _calc_eff_DOS_derivative(self, T: float): + return self._calc_eff_DOS(T) * 1.5 / T + class DualValleyEffectiveDOS(EffectiveDOS): """Effective density of states model that assumes combibation of light holes and heavy holes with isotropic effective masses. @@ -129,3 +154,6 @@ class DualValleyEffectiveDOS(EffectiveDOS): def _calc_eff_DOS(self, T: float): return (np.power(self.m_eff_lh * T, 1.5) + np.power(self.m_eff_hh * T, 1.5)) * DOS_aux_const + + def _calc_eff_DOS_derivative(self, T: float): + return self._calc_eff_DOS(T) * 1.5 / T From ab2abdd383d8ee616364ebb3478051c7c6ec329b Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Thu, 10 Apr 2025 10:11:54 -0700 Subject: [PATCH 3/8] Add effective DOS classes for the Charge solver --- tidy3d/components/tcad/effective_DOS.py | 37 ++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tidy3d/components/tcad/effective_DOS.py b/tidy3d/components/tcad/effective_DOS.py index e62c18d35e..0d531d8083 100644 --- a/tidy3d/components/tcad/effective_DOS.py +++ b/tidy3d/components/tcad/effective_DOS.py @@ -4,17 +4,16 @@ import pydantic.v1 as pd from tidy3d.components.base import Tidy3dBaseModel +from tidy3d.constants import C_0, HBAR, K_B from ...exceptions import DataError # constants definition -k_B = 1.380649e-23 # Boltzmann constant in J/K -m_e = 9.1093837139e-31 # electron mass in kg -Planck_hbar = 1.054571817e-34 # reduced Planck constant in J*s -m_3_to_cm_3 = 1e-6 # conversion factor from m^(-3) to cm^(-3) -DOS_aux_const = ( - 2.0 * np.power((m_e * k_B) / (2 * np.pi * Planck_hbar * Planck_hbar), 1.5) * m_3_to_cm_3 -) +m_e_C_square = 0.51099895069e6 # (electron mass * C_0^2) in eV +m_e_eV = 0.51099895069e6 / C_0 / C_0 # equivalent electron mass in eV +um_3_to_cm_3 = 1e12 # conversion factor from micron^(-3) to cm^(-3) + +DOS_aux_const = 2.0 * np.power((m_e_eV * K_B) / (2 * np.pi * HBAR * HBAR), 1.5) * um_3_to_cm_3 class EffectiveDOS(Tidy3dBaseModel, ABC): @@ -50,7 +49,7 @@ def get_effective_DOS_derivative(self, T: float): class ConstantEffectiveDOS(EffectiveDOS): """Constant effective density of states model.""" - N: pd.NonNegativeFloat = pd.Field( + N: pd.PositiveFloat = pd.Field( ..., title="Effective DOS", description="Effective density of states", units="cm^(-3)" ) @@ -68,11 +67,11 @@ class IsotropicEffectiveDOS(EffectiveDOS): .. math:: \\begin{equation} - \\mathbf{N_eff} = 2 * (\\m_eff \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + \\mathbf{N_eff} = 2 * (\\frac{m_eff * m_e * k_B T}{2 \\pi \\hbar^2})^(3/2) \\end{equation} """ - m_eff: pd.NonNegativeFloat = pd.Field( + m_eff: pd.PositiveFloat = pd.Field( ..., title="Effective mass", description="Effective mass of the carriers", @@ -87,32 +86,32 @@ def _calc_eff_DOS_derivative(self, T: float): class MultiValleyEffectiveDOS(EffectiveDOS): - """Effective density of states model that assumes multiple valleys and anisotropic effective mass. + """Effective density of states model that assumes multiple equivalent valleys and anisotropic effective mass. The model assumes the standard equation for the 3D semiconductor with parabolic energy dispersion: .. math:: \\begin{equation} - \\mathbf{N_eff} = 2 * \\N_valley (\\m_eff_long * \\m_eff_trans * \\m_eff_trans)^(1/2) (\\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + \\mathbf{N_eff} = 2 * N_valley (\\frac{(m_{eff_long} * m_{eff_trans} * m_{eff_trans})^(1/2) * m_e * k_B * T}{2 \\pi * \\hbar^2})^(3/2) \\end{equation} """ - m_eff_long: pd.NonNegativeFloat = pd.Field( + m_eff_long: pd.PositiveFloat = pd.Field( ..., title="Longitudinal effective mass", description="Effective mass of the carriers in the longitudinal direction", units="Electron mass", ) - m_eff_trans: pd.NonNegativeFloat = pd.Field( + m_eff_trans: pd.PositiveFloat = pd.Field( ..., title="Longitudinal effective mass", description="Effective mass of the carriers in the transverse direction", units="Electron mass", ) - N_valley: pd.NonNegativeInt = pd.Field( - ..., title="Nnmber of valleys", description="Number of valleys in the energy band" + N_valley: pd.PositiveFloat = pd.Field( + ..., title="Number of valleys", description="Number of effective valleys" ) def _calc_eff_DOS(self, T: float): @@ -134,18 +133,18 @@ class DualValleyEffectiveDOS(EffectiveDOS): .. math:: \\begin{equation} - \\mathbf{N_eff} = 2 * ( (\\m_eff_lh \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) + (\\m_eff_hh \\m_e \\k_B \\T / (2 \\pi \\hbar^2))^(3/2) ) + \\mathbf{N_eff} = 2 * ( {\\frac{m_{eff_lh} * m_e * k_B * T}{2 \\pi \\hbar^2})^(3/2) + (\\frac{m_{eff_hh} * m_e * k_B * T}{2 \\pi \\hbar^2})^(3/2) ) \\end{equation} """ - m_eff_lh: pd.NonNegativeFloat = pd.Field( + m_eff_lh: pd.PositiveFloat = pd.Field( ..., title="Light hole effective mass", description="Effective mass of the light holes", units="Electron mass", ) - m_eff_hh: pd.NonNegativeFloat = pd.Field( + m_eff_hh: pd.PositiveFloat = pd.Field( ..., title="Heavy hole effective mass", description="Effective mass of the heavy holes", From acdae492c8dba740ceaaa19a80b2297ea14a21e7 Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Mon, 21 Apr 2025 11:46:22 -0700 Subject: [PATCH 4/8] Update of the effective DOS model according to the comments. --- tidy3d/components/tcad/effective_DOS.py | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tidy3d/components/tcad/effective_DOS.py b/tidy3d/components/tcad/effective_DOS.py index 0d531d8083..bf29df71a5 100644 --- a/tidy3d/components/tcad/effective_DOS.py +++ b/tidy3d/components/tcad/effective_DOS.py @@ -10,7 +10,7 @@ # constants definition m_e_C_square = 0.51099895069e6 # (electron mass * C_0^2) in eV -m_e_eV = 0.51099895069e6 / C_0 / C_0 # equivalent electron mass in eV +m_e_eV = m_e_C_square / C_0 / C_0 # equivalent electron mass in eV um_3_to_cm_3 = 1e12 # conversion factor from micron^(-3) to cm^(-3) DOS_aux_const = 2.0 * np.power((m_e_eV * K_B) / (2 * np.pi * HBAR * HBAR), 1.5) * um_3_to_cm_3 @@ -20,12 +20,12 @@ class EffectiveDOS(Tidy3dBaseModel, ABC): """Abstract class for the effective density of states""" @abstractmethod - def _calc_eff_DOS(self, T: float): + def _calc_eff_dos(self, T: float): """Abstract method to calculate the effective density of states.""" pass @abstractmethod - def _calc_eff_DOS_derivative(self, T: float): + def _calc_eff_dos_derivative(self, T: float): """Abstract method to calculate the temperature derivative of the effective density of states.""" pass @@ -35,7 +35,7 @@ def get_effective_DOS(self, T: float): f"Incorrect temperature value ({T}) for the effectve density of states calculation." ) - return self._calc_eff_DOS(T) + return self._calc_eff_dos(T) def get_effective_DOS_derivative(self, T: float): if T <= 0: @@ -43,7 +43,7 @@ def get_effective_DOS_derivative(self, T: float): f"Incorrect temperature value ({T}) for the effectve density of states calculation." ) - return self._calc_eff_DOS_derivative(T) + return self._calc_eff_dos_derivative(T) class ConstantEffectiveDOS(EffectiveDOS): @@ -53,10 +53,10 @@ class ConstantEffectiveDOS(EffectiveDOS): ..., title="Effective DOS", description="Effective density of states", units="cm^(-3)" ) - def _calc_eff_DOS(self, T: float): + def _calc_eff_dos(self, T: float): return self.N - def _calc_eff_DOS_derivative(self, T: float): + def _calc_eff_dos_derivative(self, T: float): return 0.0 @@ -78,11 +78,11 @@ class IsotropicEffectiveDOS(EffectiveDOS): units="Electron mass", ) - def _calc_eff_DOS(self, T: float): + def _calc_eff_dos(self, T: float): return np.power(self.m_eff * T, 1.5) * DOS_aux_const - def _calc_eff_DOS_derivative(self, T: float): - return self._calc_eff_DOS(T) * 1.5 / T + def _calc_eff_dos_derivative(self, T: float): + return self._calc_eff_dos(T) * 1.5 / T class MultiValleyEffectiveDOS(EffectiveDOS): @@ -114,7 +114,7 @@ class MultiValleyEffectiveDOS(EffectiveDOS): ..., title="Number of valleys", description="Number of effective valleys" ) - def _calc_eff_DOS(self, T: float): + def _calc_eff_dos(self, T: float): return ( self.N_valley * np.power(self.m_eff_long * self.m_eff_trans * self.m_eff_trans, 0.5) @@ -122,8 +122,8 @@ def _calc_eff_DOS(self, T: float): * DOS_aux_const ) - def _calc_eff_DOS_derivative(self, T: float): - return self._calc_eff_DOS(T) * 1.5 / T + def _calc_eff_dos_derivative(self, T: float): + return self._calc_eff_dos(T) * 1.5 / T class DualValleyEffectiveDOS(EffectiveDOS): @@ -151,8 +151,8 @@ class DualValleyEffectiveDOS(EffectiveDOS): units="Electron mass", ) - def _calc_eff_DOS(self, T: float): + def _calc_eff_dos(self, T: float): return (np.power(self.m_eff_lh * T, 1.5) + np.power(self.m_eff_hh * T, 1.5)) * DOS_aux_const - def _calc_eff_DOS_derivative(self, T: float): - return self._calc_eff_DOS(T) * 1.5 / T + def _calc_eff_dos_derivative(self, T: float): + return self._calc_eff_dos(T) * 1.5 / T From 529b2b8d76dbf7b3bb34b48e89a8f88ee6862d0d Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Mon, 28 Apr 2025 19:29:27 -0700 Subject: [PATCH 5/8] Minor code change --- tidy3d/components/tcad/effective_DOS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/tcad/effective_DOS.py b/tidy3d/components/tcad/effective_DOS.py index bf29df71a5..87eabe8aad 100644 --- a/tidy3d/components/tcad/effective_DOS.py +++ b/tidy3d/components/tcad/effective_DOS.py @@ -92,7 +92,7 @@ class MultiValleyEffectiveDOS(EffectiveDOS): .. math:: \\begin{equation} - \\mathbf{N_eff} = 2 * N_valley (\\frac{(m_{eff_long} * m_{eff_trans} * m_{eff_trans})^(1/2) * m_e * k_B * T}{2 \\pi * \\hbar^2})^(3/2) + \\mathbf{N_eff} = 2 * N_valley * (m_{eff_long} * m_{eff_trans} * m_{eff_trans})^(1/2) *(\\frac{m_e * k_B * T}{2 \\pi * \\hbar^2})^(3/2) \\end{equation} """ From 3c5faa3d1ee9377d9a7c0bd43451d7d0092b70c2 Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Tue, 29 Apr 2025 16:49:27 -0700 Subject: [PATCH 6/8] Removed possibility to define the density of states by a float number --- tidy3d/components/material/tcad/charge.py | 4 ++-- tidy3d/material_library/material_library.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tidy3d/components/material/tcad/charge.py b/tidy3d/components/material/tcad/charge.py index adf41b090f..a86c3518d7 100644 --- a/tidy3d/components/material/tcad/charge.py +++ b/tidy3d/components/material/tcad/charge.py @@ -257,14 +257,14 @@ class SemiconductorMedium(AbstractChargeMedium): """ - N_c: Union[pd.PositiveFloat, EffectiveDOSModelType] = pd.Field( + N_c: EffectiveDOSModelType = pd.Field( ..., title="Effective density of electron states", description=r"$N_c$ Effective density of states in the conduction band.", units="cm^(-3)", ) - N_v: Union[pd.PositiveFloat, EffectiveDOSModelType] = pd.Field( + N_v: EffectiveDOSModelType = pd.Field( ..., title="Effective density of hole states", description=r"$N_v$ Effective density of states in the valence band.", diff --git a/tidy3d/material_library/material_library.py b/tidy3d/material_library/material_library.py index 7c6f7472ff..d1d2759280 100644 --- a/tidy3d/material_library/material_library.py +++ b/tidy3d/material_library/material_library.py @@ -10,6 +10,7 @@ from tidy3d.components.tcad.types import ( AugerRecombination, CaugheyThomasMobility, + ConstantEffectiveDOS, RadiativeRecombination, ShockleyReedHallRecombination, SlotboomBandGapNarrowing, @@ -1883,8 +1884,8 @@ def medium(self, optical_axis: Axis): optical=cSi_Green2008.medium, charge=SemiconductorMedium( permittivity=11.7, - N_c=2.86e19, - N_v=3.1e19, + N_c=ConstantEffectiveDOS(N=2.86e19), + N_v=ConstantEffectiveDOS(N=3.1e19), E_g=1.11, mobility_n=CaugheyThomasMobility( mu_min=52.2, From b81f6c67e7cafd23f1854b4cb7979693329ddc03 Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Tue, 29 Apr 2025 21:01:54 -0700 Subject: [PATCH 7/8] Update tests for the Charge solver --- tests/test_components/test_heat_charge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 5a7b5c38ff..9868c32d30 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -8,6 +8,7 @@ from tidy3d.components.tcad.types import ( AugerRecombination, CaugheyThomasMobility, + ConstantEffectiveDOS, SlotboomBandGapNarrowing, ) from tidy3d.exceptions import DataError @@ -36,8 +37,8 @@ class CHARGE_SIMULATION: permittivity=11.7, N_d=0, N_a=0, - N_c=2.86e19, - N_v=3.1e19, + N_c=ConstantEffectiveDOS(N=2.86e19), + N_v=ConstantEffectiveDOS(N=3.1e19), E_g=1.11, mobility_n=CaugheyThomasMobility( mu_min=52.2, From 11cd4695b9b92767b93880950ceb9533b6728a54 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Fri, 2 May 2025 17:30:12 +0200 Subject: [PATCH 8/8] Non-isothermal charge simulations --- tests/test_components/test_heat_charge.py | 72 +++++++++++++++---- tidy3d/__init__.py | 2 + tidy3d/components/spice/analysis/dc.py | 24 ++++--- tidy3d/components/spice/types.py | 7 +- .../components/tcad/simulation/heat_charge.py | 47 +++++++++++- 5 files changed, 126 insertions(+), 26 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 09a61b12d9..9268829b9e 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -952,23 +952,30 @@ def Si_p(self): semiconductor = CHARGE_SIMULATION.intrinsic_Si.charge semiconductor = semiconductor.updated_copy( N_a=CHARGE_SIMULATION.acceptors, + ) + return CHARGE_SIMULATION.intrinsic_Si.updated_copy( + charge=semiconductor, + heat=td.SolidMedium(conductivity=1), name="Si_p", ) - return CHARGE_SIMULATION.intrinsic_Si.updated_copy(charge=semiconductor) @pytest.fixture(scope="class") def Si_n(self): semiconductor = CHARGE_SIMULATION.intrinsic_Si.charge semiconductor = semiconductor.updated_copy( N_d=CHARGE_SIMULATION.donors, + ) + return CHARGE_SIMULATION.intrinsic_Si.updated_copy( + charge=semiconductor, + heat=td.SolidMedium(conductivity=1), name="Si_n", ) - return CHARGE_SIMULATION.intrinsic_Si.updated_copy(charge=semiconductor) @pytest.fixture(scope="class") def SiO2(self): return td.MultiPhysicsMedium( charge=td.ChargeInsulatorMedium(permittivity=3.9), + heat=td.SolidMedium(conductivity=2), name="SiO2", ) @@ -1049,18 +1056,13 @@ def capacitance_global_mnt(self): # Define charge settings as fixtures within the class @pytest.fixture(scope="class") def charge_tolerance(self): - return td.IsothermalSteadyChargeDCAnalysis( - temperature=300, - tolerance_settings=td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400), - fermi_dirac=True, - ) - - @pytest.fixture(scope="class") - def charge_dc_regime(self): - return td.DCVoltageSource(voltage=[1]) + return td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400) def test_charge_simulation( self, + Si_n, + Si_p, + SiO2, oxide, p_side, n_side, @@ -1070,9 +1072,14 @@ def test_charge_simulation( bc_n, bc_p, charge_tolerance, - charge_dc_regime, ): """Ensure charge simulation produces the correct errors when needed.""" + # NOTE: start tests with isothermal spec + isothermal_spec = td.IsothermalSteadyChargeDCAnalysis( + temperature=300, + tolerance_settings=charge_tolerance, + fermi_dirac=True, + ) sim = td.HeatChargeSimulation( structures=[oxide, p_side, n_side], medium=td.MultiPhysicsMedium( @@ -1083,7 +1090,7 @@ def test_charge_simulation( size=CHARGE_SIMULATION.sim_size, grid_spec=td.UniformUnstructuredGrid(dl=0.05), boundary_spec=[bc_n, bc_p], - analysis_spec=charge_tolerance, + analysis_spec=isothermal_spec, ) # At least one ChargeSimulationMonitor should be added @@ -1118,6 +1125,45 @@ def test_charge_simulation( ) _ = sim.updated_copy(boundary_spec=[new_bc_p, bc_n]) + # test non isothermal spec + non_isothermal_spec = td.SteadyChargeDCAnalysis(tolerance_settings=charge_tolerance) + + sim = sim.updated_copy(analysis_spec=non_isothermal_spec) + with pytest.raises(pd.ValidationError): + # remove heat from mediums + new_structs = [] + for struct in sim.structures: + new_structs.append( + struct.updated_copy(medium=struct.medium.updated_copy(heat=None)) + ) + _ = sim.updated_copy(structures=new_structs) + + with pytest.raises(pd.ValidationError): + # remove charge from mediums + new_structs = [] + for struct in sim.structures: + new_structs.append( + struct.updated_copy(medium=struct.medium.updated_copy(charge=None)) + ) + _ = sim.updated_copy(structures=new_structs) + + with pytest.raises(pd.ValidationError): + # make sure there is at least one semiconductor + new_structs = [] + for struct in sim.structures: + if isinstance(struct.medium.charge, td.SemiconductorMedium): + new_structs.append( + struct.updated_copy( + medium=struct.medium.updated_copy( + charge=td.ChargeInsulatorMedium(permittivity=1), + heat=None, + ) + ) + ) + else: + new_structs.append(struct) + _ = sim.updated_copy(structures=new_structs) + def test_doping_distributions(self): """Test doping distributions.""" # Implementation needed diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 6bc0bd31ab..e70dc483e9 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -20,6 +20,7 @@ from tidy3d.components.spice.analysis.dc import ( ChargeToleranceSpec, IsothermalSteadyChargeDCAnalysis, + SteadyChargeDCAnalysis, ) from tidy3d.components.spice.sources.dc import DCCurrentSource, DCVoltageSource from tidy3d.components.spice.sources.types import VoltageSourceType @@ -651,6 +652,7 @@ def set_logging_level(level: str) -> None: "Staircasing", "SteadyCapacitanceData", "SteadyCapacitanceMonitor", + "SteadyChargeDCAnalysis", "SteadyEnergyBandData", "SteadyEnergyBandMonitor", "SteadyFreeCarrierData", diff --git a/tidy3d/components/spice/analysis/dc.py b/tidy3d/components/spice/analysis/dc.py index e9e1e2aca5..ca66d5ff3e 100644 --- a/tidy3d/components/spice/analysis/dc.py +++ b/tidy3d/components/spice/analysis/dc.py @@ -49,19 +49,11 @@ class ChargeToleranceSpec(Tidy3dBaseModel): ) -class IsothermalSteadyChargeDCAnalysis(Tidy3dBaseModel): +class SteadyChargeDCAnalysis(Tidy3dBaseModel): """ Configures relevant steady-state DC simulation parameters for a charge simulation. """ - temperature: pd.PositiveFloat = pd.Field( - 300, - title="Temperature", - description="Lattice temperature. Assumed constant throughout the device. " - "Carriers are assumed to be at thermodynamic equilibrium with the lattice.", - units=KELVIN, - ) - tolerance_settings: ChargeToleranceSpec = pd.Field( default=ChargeToleranceSpec(), title="Tolerance settings" ) @@ -83,3 +75,17 @@ class IsothermalSteadyChargeDCAnalysis(Tidy3dBaseModel): "where very high doping may lead the pseudo-Fermi energy level to approach " "either the conduction or valence energy bands.", ) + + +class IsothermalSteadyChargeDCAnalysis(SteadyChargeDCAnalysis): + """ + Configures relevant steady-state DC simulation parameters for a charge simulation. + """ + + temperature: pd.PositiveFloat = pd.Field( + 300, + title="Temperature", + description="Lattice temperature. Assumed constant throughout the device. " + "Carriers are assumed to be at thermodynamic equilibrium with the lattice.", + units=KELVIN, + ) diff --git a/tidy3d/components/spice/types.py b/tidy3d/components/spice/types.py index 486bd0e734..daac472938 100644 --- a/tidy3d/components/spice/types.py +++ b/tidy3d/components/spice/types.py @@ -2,6 +2,9 @@ from typing import Union -from tidy3d.components.spice.analysis.dc import IsothermalSteadyChargeDCAnalysis +from tidy3d.components.spice.analysis.dc import ( + IsothermalSteadyChargeDCAnalysis, + SteadyChargeDCAnalysis, +) -ElectricalAnalysisType = Union[IsothermalSteadyChargeDCAnalysis] +ElectricalAnalysisType = Union[SteadyChargeDCAnalysis, IsothermalSteadyChargeDCAnalysis] diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index d93ec0b14b..21a0dd7f60 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -35,7 +35,11 @@ from tidy3d.components.medium import Medium from tidy3d.components.scene import Scene from tidy3d.components.spice.sources.dc import DCVoltageSource -from tidy3d.components.spice.types import ElectricalAnalysisType +from tidy3d.components.spice.types import ( + ElectricalAnalysisType, + IsothermalSteadyChargeDCAnalysis, + SteadyChargeDCAnalysis, +) from tidy3d.components.structure import Structure from tidy3d.components.tcad.analysis.heat_simulation_type import UnsteadyHeatAnalysis from tidy3d.components.tcad.boundary.specification import ( @@ -951,6 +955,44 @@ def check_transient_heat(cls, values): ) return values + @pd.root_validator(skip_on_failure=True) + def check_non_isothermal_is_possible(cls, values): + """Make sure that when a non-isothermal case is defined the structrures + have both electrical and thermal properties.""" + + analysis_spec = values.get("analysis_spec") + if isinstance(analysis_spec, SteadyChargeDCAnalysis) and not isinstance( + analysis_spec, IsothermalSteadyChargeDCAnalysis + ): + has_heat = False + has_elec = False + structures = values.get("structures") + for struct in structures: + if isinstance(struct.medium, MultiPhysicsMedium): + if struct.medium.heat is not None: + if isinstance(struct.medium.heat, SolidMedium): + has_heat = True + if struct.medium.charge is not None: + if isinstance(struct.medium.charge, SemiconductorMedium): + has_elec = True + + if not has_heat and has_elec: + raise SetupError( + "The current simulation is defined as non-isothermal but no solid " + "materials with heat properties have been defined. " + ) + elif not has_elec and has_heat: + raise SetupError( + "The current simulation is defined as non-isothermal but no " + "semiconductor materials have been defined. " + ) + elif not has_heat and not has_elec: + raise SetupError( + "The current simulation is defined as non-isothermal but no " + "solid or semiconductor materials have been defined. " + ) + return values + @equal_aspect @add_ax_if_none def plot_property( @@ -1735,7 +1777,8 @@ def _get_simulation_types(self) -> list[TCADAnalysisTypes]: # NOTE: for the time being, if a simulation has SemiconductorMedium # then we consider it of being a 'TCADAnalysisTypes.CHARGE' - if isinstance(self.analysis_spec, ElectricalAnalysisType): + ChargeTypes = (SteadyChargeDCAnalysis, IsothermalSteadyChargeDCAnalysis) + if isinstance(self.analysis_spec, ChargeTypes): if self._check_if_semiconductor_present(self.structures): return [TCADAnalysisTypes.CHARGE]