diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 09a61b12d..9268829b9 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 6bc0bd31a..e70dc483e 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 e9e1e2aca..ca66d5ff3 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 486bd0e73..daac47293 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 d93ec0b14..21a0dd7f6 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]