diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 438c830549..6d0ed00655 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -8,6 +8,7 @@ from matplotlib import pyplot as plt import tidy3d as td +from tidy3d.components.tcad.simulation.heat_charge import TCADAnalysisTypes from tidy3d.components.tcad.types import ( AugerRecombination, CaugheyThomasMobility, @@ -2513,3 +2514,86 @@ def test_generation_recombination(): beta_n=1, beta_p=1, ) + + +def test_heat_only_simulation_with_semiconductor(): + """Test that a heat-only simulation with semiconductors does not trigger charge simulation. + Charge simulations are only triggered when `analysis_spec` is provided, not just when + semiconductors are present in the simulation. + """ + + # Create a semiconductor medium + semiconductor_medium = td.MultiPhysicsMedium( + optical=td.Medium(permittivity=5, conductivity=0.01), + heat=td.SolidMedium(conductivity=3, capacity=2), + charge=td.SemiconductorMedium( + N_c=td.ConstantEffectiveDOS(N=1e10), + N_v=td.ConstantEffectiveDOS(N=1e10), + E_g=td.ConstantEnergyBandGap(eg=1), + mobility_n=td.ConstantMobilityModel(mu=1500), + mobility_p=td.ConstantMobilityModel(mu=1500), + ), + name="semiconductor", + ) + + # Create a non-semiconductor solid medium + solid_medium = td.MultiPhysicsMedium( + optical=td.Medium(permittivity=5, conductivity=0.01), + heat=td.SolidMedium(conductivity=1, capacity=1), + charge=td.ChargeConductorMedium(conductivity=1), + name="solid", + ) + + # Create structures with both semiconductor and other materials + semiconductor_structure = td.Structure( + geometry=td.Box(center=(-0.5, 0, 0), size=(1, 1, 1)), + medium=semiconductor_medium, + name="semiconductor_structure", + ) + + solid_structure = td.Structure( + geometry=td.Box(center=(0.5, 0, 0), size=(1, 1, 1)), + medium=solid_medium, + name="solid_structure", + ) + + # Create heat-only boundary conditions (no electric BCs) + thermal_bc = td.HeatChargeBoundarySpec( + condition=td.TemperatureBC(temperature=300), + placement=td.StructureBoundary(structure="solid_structure"), + ) + + # Create heat source + heat_source = td.HeatSource(structures=["solid_structure"], rate=100) + + # Create heat monitor (no charge monitors) + temp_monitor = td.TemperatureMonitor( + center=(0, 0, 0), size=(2, 1, 1), name="temp_monitor", unstructured=True + ) + + # Create heat-only simulation (no analysis_spec, no electric BCs) + heat_sim = td.HeatChargeSimulation( + medium=td.MultiPhysicsMedium( + heat=td.FluidMedium(), charge=td.ChargeInsulatorMedium(), name="air" + ), + structures=[semiconductor_structure, solid_structure], + center=(0, 0, 0), + size=(3, 3, 3), + boundary_spec=[thermal_bc], + grid_spec=td.UniformUnstructuredGrid(dl=0.1), + sources=[heat_source], + monitors=[temp_monitor], + ) + + # Verify that only HEAT simulation type is returned, not CHARGE + simulation_types = heat_sim._get_simulation_types() + assert TCADAnalysisTypes.HEAT in simulation_types, ( + "Heat simulation should be triggered when heat sources/BCs are present." + ) + assert TCADAnalysisTypes.CHARGE not in simulation_types, ( + "Charge simulation should NOT be triggered when ChargeTypes analysis_spec is not provided, " + "even if semiconductors are present in the simulation." + ) + assert TCADAnalysisTypes.CONDUCTION not in simulation_types, ( + "Conduction simulation should NOT be triggered when no electric BCs are present." + ) diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 73dd7c130f..71a09071f5 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -109,6 +109,18 @@ HeatSourceTypes = (UniformHeatSource, HeatSource, HeatFromElectricSource) ChargeSourceTypes = () ElectricBCTypes = (VoltageBC, CurrentBC, InsulatingBC) +ChargeTypes = ( + SteadyChargeDCAnalysis, + IsothermalSteadyChargeDCAnalysis, + SSACAnalysis, + IsothermalSSACAnalysis, +) +ChargeMonitorTypes = ( + SteadyPotentialMonitor, + SteadyFreeCarrierMonitor, + SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, +) AnalysisSpecType = Union[ElectricalAnalysisType, UnsteadyHeatAnalysis] @@ -683,13 +695,6 @@ def check_freqs_requires_ac_source(cls, values): def check_charge_simulation(cls, values): """Makes sure that Charge simulations are set correctly.""" - ChargeMonitorType = ( - SteadyPotentialMonitor, - SteadyFreeCarrierMonitor, - SteadyCapacitanceMonitor, - SteadyCurrentDensityMonitor, - ) - simulation_types = cls._check_simulation_types(values=values) if TCADAnalysisTypes.CHARGE in simulation_types: @@ -707,7 +712,7 @@ def check_charge_simulation(cls, values): # check that we have at least one charge monitor monitors = values["monitors"] - if not any(isinstance(mnt, ChargeMonitorType) for mnt in monitors): + if not any(isinstance(mnt, ChargeMonitorTypes) for mnt in monitors): raise SetupError( "Charge simulations require the definition of, at least, one of these monitors: " "'[SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyCapacitanceMonitor, SteadyCurrentDensityMonitor]' " @@ -723,7 +728,13 @@ def check_charge_simulation(cls, values): "Currently, Charge simulations support only unstructured monitors. Please set " f"monitor '{mnt.name}' to 'unstructured = True'." ) - + # check that we have at least one semiconductor medium + structures = values["structures"] + sc_present = HeatChargeSimulation._check_if_semiconductor_present(structures=structures) + if not sc_present: + raise SetupError( + f"{TCADAnalysisTypes.CHARGE} simulations require the definition of at least one semiconductor medium." + ) return values @pd.root_validator(skip_on_failure=True) @@ -880,23 +891,23 @@ def _check_simulation_types( boundaries = list(values["boundary_spec"]) sources = list(values["sources"]) + analysis_spec = values["analysis_spec"] structures = list(values["structures"]) + + if isinstance(analysis_spec, ChargeTypes): + simulation_types.append(TCADAnalysisTypes.CHARGE) + semiconductor_present = HeatChargeSimulation._check_if_semiconductor_present( structures=structures ) - if semiconductor_present: - simulation_types.append(TCADAnalysisTypes.CHARGE) for boundary in boundaries: if isinstance(boundary.condition, HeatBCTypes): simulation_types.append(TCADAnalysisTypes.HEAT) if isinstance(boundary.condition, ElectricBCTypes): - # for the time being, assume tha the simulation will be of - # type CHARGE if we have semiconductors - if semiconductor_present: - simulation_types.append(TCADAnalysisTypes.CHARGE) - else: + # Add CONDUCTION type if we have no semiconductors + if not semiconductor_present: simulation_types.append(TCADAnalysisTypes.CONDUCTION) for source in sources: @@ -1060,7 +1071,7 @@ def check_transient_heat(cls, values): raise SetupError( f"Unsteady simulations require the temperature monitor '{mnt.name}' to be unstructured." ) - # additionaly check that the SolidSpec has capacity and density defined + # additionally check that the SolidSpec has capacity and density defined capacities = [] densities = [] conductivities = [] @@ -1115,7 +1126,7 @@ def check_transient_heat(cls, 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 + """Make sure that when a non-isothermal case is defined the structures have both electrical and thermal properties.""" analysis_spec = values.get("analysis_spec") @@ -1492,7 +1503,7 @@ def _construct_forward_boundaries( ) -> tuple[tuple[HeatChargeBoundarySpec, Shapely], ...]: """Construct Simulation, StructureSimulation, Structure, and MediumMedium boundaries.""" - # forward foop to take care of Simulation, StructureSimulation, Structure, + # forward loop to take care of Simulation, StructureSimulation, Structure, # and MediumMediums boundaries = [] # bc_spec, structure name, shape, bounds background_shapes = [] @@ -1583,7 +1594,7 @@ def _construct_reverse_boundaries( ) -> tuple[tuple[HeatChargeBoundarySpec, Shapely], ...]: """Construct StructureStructure boundaries.""" - # backward foop to take care of StructureStructure + # backward loop to take care of StructureStructure # we do it in this way because we define the boundary between # two overlapping structures A and B, where A comes before B, as # boundary(B) intersected by A @@ -1690,7 +1701,7 @@ def _construct_heat_charge_boundaries( # construct boundaries in 2 passes: - # 1. forward foop to take care of Simulation, StructureSimulation, Structure, + # 1. forward loop to take care of Simulation, StructureSimulation, Structure, # and MediumMediums boundaries = HeatChargeSimulation._construct_forward_boundaries( shapes=shapes, @@ -1933,17 +1944,8 @@ def _get_simulation_types(self) -> list[TCADAnalysisTypes]: """ simulation_types = [] - # NOTE: for the time being, if a simulation has SemiconductorMedium - # then we consider it of being a 'TCADAnalysisTypes.CHARGE' - ChargeTypes = ( - SteadyChargeDCAnalysis, - IsothermalSteadyChargeDCAnalysis, - SSACAnalysis, - IsothermalSSACAnalysis, - ) if isinstance(self.analysis_spec, ChargeTypes): - if self._check_if_semiconductor_present(self.structures): - return [TCADAnalysisTypes.CHARGE] + return [TCADAnalysisTypes.CHARGE] # check if unsteady heat if isinstance(self.analysis_spec, UnsteadyHeatAnalysis):