Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions tests/test_components/test_heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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."
)
64 changes: 33 additions & 31 deletions tidy3d/components/tcad/simulation/heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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:
Expand All @@ -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]' "
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
Loading