# ruff: noqa: W293, W291
"""Defines heat simulation class"""
from __future__ import annotations
from enum import Enum
from typing import Dict, List, Tuple, Union
import numpy as np
import pydantic.v1 as pd
try:
from matplotlib import colormaps
except ImportError:
pass
from tidy3d.components.base import skip_if_fields_missing
from tidy3d.components.base_sim.simulation import AbstractSimulation
from tidy3d.components.bc_placement import (
MediumMediumInterface,
SimulationBoundary,
StructureBoundary,
StructureSimulationBoundary,
StructureStructureInterface,
)
from tidy3d.components.geometry.base import Box
from tidy3d.components.material.tcad.charge import (
ChargeConductorMedium,
SemiconductorMedium,
)
from tidy3d.components.material.tcad.heat import (
SolidSpec,
)
from tidy3d.components.material.types import MultiPhysicsMedium, StructureMediumType
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.structure import Structure
from tidy3d.components.tcad.boundary.specification import (
HeatBoundarySpec,
HeatChargeBoundarySpec,
)
from tidy3d.components.tcad.grid import (
DistanceUnstructuredGrid,
UniformUnstructuredGrid,
UnstructuredGridType,
)
from tidy3d.components.tcad.monitors.charge import (
SteadyCapacitanceMonitor,
SteadyFreeCarrierMonitor,
SteadyPotentialMonitor,
)
from tidy3d.components.tcad.monitors.heat import (
TemperatureMonitor,
)
from tidy3d.components.tcad.source.abstract import (
GlobalHeatChargeSource,
)
from tidy3d.components.tcad.types import (
ConvectionBC,
CurrentBC,
HeatChargeMonitorType,
HeatChargeSourceType,
HeatFluxBC,
HeatFromElectricSource,
HeatSource,
InsulatingBC,
TemperatureBC,
UniformHeatSource,
VoltageBC,
)
from tidy3d.components.tcad.viz import (
CHARGE_BC_INSULATOR,
HEAT_BC_COLOR_CONVECTION,
HEAT_BC_COLOR_FLUX,
HEAT_BC_COLOR_TEMPERATURE,
HEAT_SOURCE_CMAP,
plot_params_heat_bc,
plot_params_heat_source,
)
from tidy3d.components.types import TYPE_TAG_STR, Ax, Bound, ScalarSymmetry, Shapely, annotate_type
from tidy3d.components.viz import PlotParams, add_ax_if_none, equal_aspect
from tidy3d.constants import VOLUMETRIC_HEAT_RATE, inf
from tidy3d.exceptions import SetupError
from tidy3d.log import log
HEAT_CHARGE_BACK_STRUCTURE_STR = "<<<HEAT_CHARGE_BACKGROUND_STRUCTURE>>>"
HeatBCTypes = (TemperatureBC, HeatFluxBC, ConvectionBC)
HeatSourceTypes = (UniformHeatSource, HeatSource, HeatFromElectricSource)
ChargeSourceTypes = ()
ElectricBCTypes = (VoltageBC, CurrentBC, InsulatingBC)
AnalysisSpecType = ElectricalAnalysisType
class TCADAnalysisTypes(str, Enum):
"""Enumeration of the types of simulations currently supported"""
HEAT = "Heat"
CONDUCTION = "Conduction"
CHARGE = "Charge"
[docs]
class HeatChargeSimulation(AbstractSimulation):
"""
Defines thermoelectric simulations.
Notes
-----
A ``HeatChargeSimulation`` supports different types of simulations. It solves the
heat and conduction equations using the Finite-Volume (FV) method. This solver
determines the required computation physics according to the simulation scene definition.
This is implemented in this way due to the strong multi-physics coupling.
The ``HeatChargeSimulation`` can solve multiple physics and the intention is to enable close thermo-electrical coupling.
Currently, this solver supports steady-state heat conduction where :math:`q` is the heat flux, :math:`k`
is the thermal conductivity, and :math:`T` is the temperature.
.. math::
-k \\cdot \\nabla(T) = q
The steady-state electrical ``Conduction`` equation depends on the electric conductivity (:math:`\\sigma`) of a
medium, and the electric field (:math:`\\mathbf{E} = -\\nabla(\\psi)`) derived from electrical potential (:math:`\\psi`).
Currently, in this type of simulation, no current sources or sinks are supported.
.. math::
\\text{div}(\\sigma \\cdot \\nabla(\\psi)) = 0
For further details on what equations are solved in ``Charge`` simulations, refer to the :class:`SemiconductorMedium`.
Let's understand how the physics solving is determined:
.. list-table::
:widths: 25 75
:header-rows: 1
* - Simulation Type
- Example Configuration Settings
* - ``Heat``
- The heat equation is solved with specified heat sources,
boundary conditions, etc. Structures should incorporate materials
with defined heat properties.
* - ``Conduction``
- The electrical conduction equation is solved with
specified boundary conditions such as ``SteadyVoltageBC``, ``SteadyCurrentBC``, ...
* - ``Charge``
- Drift-diffusion equations are solved for structures containing
a defined :class:`SemiconductorMedium`. Insulators with a
:class:`ChargeInsulatorMedium` can also be included. For these, only the
electric potential field is calculated.
Examples
--------
To run a thermal (``Heat`` |:fire:|) simulation with a solid conductive structure:
>>> import tidy3d as td
>>> heat_sim = td.HeatChargeSimulation(
... size=(3.0, 3.0, 3.0),
... structures=[
... td.Structure(
... geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)),
... medium=td.Medium(
... permittivity=2.0,
... heat_spec=td.SolidSpec(
... conductivity=1,
... capacity=1,
... )
... ),
... name="box",
... ),
... ],
... medium=td.Medium(permittivity=3.0, heat_spec=td.FluidSpec()),
... grid_spec=td.UniformUnstructuredGrid(dl=0.1),
... sources=[td.HeatSource(rate=1, structures=["box"])],
... boundary_spec=[
... td.HeatChargeBoundarySpec(
... placement=td.StructureBoundary(structure="box"),
... condition=td.TemperatureBC(temperature=500),
... )
... ],
... monitors=[td.TemperatureMonitor(size=(1, 2, 3), name="sample")],
... )
To run a drift-diffusion (``Charge`` |:zap:|) system:
>>> import tidy3d as td
>>> Si_n = td.MultiPhysicsMedium(charge=td.SemiconductorMedium(
... permittivity=11.7,
... N_d=1e15,
... N_a=0,
... ), name="Si_n",
... )
>>> Si_p = Si_n.updated_copy(N_d=0, N_p=1e16, name="Si_p")
>>> n_side = td.Structure(
... geometry=td.Box(center=(-0.5, 0, 0), size=(1, 1, 1)),
... medium=Si_n,
... name="n_side"
... )
>>> p_side = td.Structure(
... geometry=td.Box(center=(0.5, 0, 0), size=(1, 1, 1)),
... medium=Si_p,
... name="p_side"
... )
>>> bc_v1 = td.HeatChargeBoundarySpec(
... condition=td.VoltageBC(source=td.DCVoltageSource(voltage=[-1, 0, 0.5])),
... placement=td.MediumMediumInterface(mediums=[air.name, Si_n.name]),
... )
>>> bc_v2 = td.HeatChargeBoundarySpec(
... condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0)),
... placement=td.MediumMediumInterface(mediums=[air.name, Si_p.name]),
... )
>>> charge_sim = td.HeatChargeSimulation(
... structures=[n_side, p_side],
... medium=td.Medium(heat_spec=td.FluidSpec(), name="air"),
... monitors=[td.SteadyFreeCarrierMonitor(
... center=(0, 0, 0), size=(td.inf, td.inf, 0), name="charge_mnt", unstructured=True
... )],
... center=(0, 0, 0),
... size=(3, 3, 3),
... grid_spec=td.UniformUnstructuredGrid(dl=0.05),
... boundary_spec=[bc_v1, bc_v2],
... analysis_spec=td.SteadyChargeDCAnalysis(
... tolerance_settings=td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=3e3, max_iters=400),
... convergence_dv=10),
... )
Coupling between ``Heat`` and electrical ``Conduction`` simulations is currently limited to 1-way.
This is specified by defining a heat source of type :class:`HeatFromElectricSource`. With this coupling, joule heating is
calculated as part of the solution to a ``Conduction`` simulation and translated into the ``Heat`` simulation.
Two common scenarios can use this coupling definition:
1. One in which BCs and sources are specified for both ``Heat`` and ``Conduction`` simulations.
In this case one mesh will be generated and used for both the ``Conduction`` and ``Heat``
simulations.
2. Only heat BCs/sources are provided. In this case, only the ``Heat`` equation will be solved.
Before the simulation starts, it will try to load the heat source from file so a
previously run ``Conduction`` simulations must have run previously. Since the Conduction
and ``Heat`` meshes may differ, an interpolation between them will be performed prior to
starting the ``Heat`` simulation.
Additional heat sources can be defined, in which case, they will be added on
top of the coupling heat source.
"""
medium: StructureMediumType = pd.Field(
Medium(),
title="Background Medium",
description="Background medium of simulation, defaults to a standard dispersion-less :class:`Medium` if not "
"specified.",
discriminator=TYPE_TAG_STR,
)
"""
Background medium of simulation, defaults to a standard dispersion-less :class:`Medium` if not specified.
"""
sources: Tuple[annotate_type(HeatChargeSourceType), ...] = pd.Field(
(),
title="Heat and Charge sources",
description="List of heat and/or charge sources.",
)
monitors: Tuple[annotate_type(HeatChargeMonitorType), ...] = pd.Field(
(),
title="Monitors",
description="Monitors in the simulation.",
)
boundary_spec: Tuple[annotate_type(Union[HeatChargeBoundarySpec, HeatBoundarySpec]), ...] = (
pd.Field(
(),
title="Boundary Condition Specifications",
description="List of boundary condition specifications.",
)
)
# NOTE: creating a union with HeatBoundarySpec for backwards compatibility
grid_spec: UnstructuredGridType = pd.Field(
title="Grid Specification",
description="Grid specification for heat-charge simulation.",
discriminator=TYPE_TAG_STR,
)
symmetry: Tuple[ScalarSymmetry, ScalarSymmetry, ScalarSymmetry] = pd.Field(
(0, 0, 0),
title="Symmetries",
description="Tuple of integers defining reflection symmetry across a plane "
"bisecting the simulation domain normal to the x-, y-, and z-axis "
"at the simulation center of each axis, respectively. "
"Each element can be ``0`` (symmetry off) or ``1`` (symmetry on).",
)
analysis_spec: AnalysisSpecType = pd.Field(
None,
title="Analysis specification.",
description="The `analysis_spec` is used to validate that the simulation parameters and tolerance settings "
"are correctly configured as desired by the user.",
)
[docs]
@pd.validator("structures", always=True)
def check_unsupported_geometries(cls, val):
"""Error if structures contain unsupported yet geometries."""
for ind, structure in enumerate(val):
bbox = structure.geometry.bounding_box
if any(s == 0 for s in bbox.size):
raise SetupError(
f"'HeatSimulation' does not currently support structures with dimensions of zero size ('structures[{ind}]')."
)
return val
@staticmethod
def _check_cross_solids(objs: Tuple[Box, ...], values: Dict) -> Tuple[int, ...]:
"""Given model dictionary ``values``, check whether objects in list ``objs`` cross
a ``SolidSpec`` medium.
"""
# NOTE: when considering Conduction or Charge cases, both conductors and semiconductors
# will be accepted
valid_electric_medium = (SemiconductorMedium, ChargeConductorMedium)
try:
size = values["size"]
center = values["center"]
medium = values["medium"]
structures = values["structures"]
except KeyError:
raise SetupError(
"Function '_check_cross_solids' assumes dictionary 'values' contains well-defined "
"'size', 'center', 'medium', and 'structures'. Thus, it should only be used in "
"validators with @skip_if_fields_missing(['medium', 'center', 'size', 'structures']) "
"or root validators with option 'skip_on_failure=True'."
)
# list of structures including background as a Box()
structure_bg = Structure(
geometry=Box(
size=size,
center=center,
),
medium=medium,
)
total_structures = [structure_bg] + list(structures)
obj_do_not_cross_solid_idx = []
obj_do_not_cross_cond_idx = []
for ind, obj in enumerate(objs):
if obj.size.count(0.0) == 1:
# for planar objects we could do a rigorous check
medium_set = Scene.intersecting_media(obj, total_structures)
crosses_solid = any(
isinstance(medium.heat_spec, SolidSpec) for medium in medium_set
)
crosses_elec_spec = any(
any([isinstance(medium.charge, medium_i)] for medium_i in valid_electric_medium)
for medium in medium_set
)
else:
# approximate check for volumetric objects based on bounding boxes
# thus, it could still miss a case when there is no data inside the monitor
crosses_solid = any(
obj.intersects(structure.geometry)
for structure in total_structures
if isinstance(structure.medium.heat_spec, SolidSpec)
)
crosses_elec_spec = any(
obj.intersects(structure.geometry)
for structure in total_structures
if any(
[isinstance(structure.medium.charge, medium_i)]
for medium_i in valid_electric_medium
)
)
if not crosses_solid:
obj_do_not_cross_solid_idx.append(ind)
if not crosses_elec_spec:
obj_do_not_cross_cond_idx.append(ind)
return obj_do_not_cross_solid_idx, obj_do_not_cross_cond_idx
@pd.validator("monitors", always=True)
@skip_if_fields_missing(["medium", "center", "size", "structures"])
def _monitors_cross_solids(cls, val, values):
"""Error if monitors does not cross any solid medium."""
if val is None:
return val
failed_solid_idx, failed_elect_idx = cls._check_cross_solids(val, values)
temp_monitors = [idx for idx, mnt in enumerate(val) if isinstance(mnt, TemperatureMonitor)]
volt_monitors = [
idx for idx, mnt in enumerate(val) if isinstance(mnt, SteadyPotentialMonitor)
]
failed_temp_mnt = [idx for idx in temp_monitors if idx in failed_solid_idx]
failed_volt_mnt = [idx for idx in volt_monitors if idx in failed_elect_idx]
if len(failed_temp_mnt) > 0:
monitor_names = [f"'{val[ind].name}'" for ind in failed_temp_mnt]
raise SetupError(
f"Monitors {monitor_names} do not cross any solid materials "
"('heat_spec=SolidSpec(...)'). Temperature distribution is only recorded inside solid "
"materials. Thus, no information will be recorded in these monitors."
)
if len(failed_volt_mnt) > 0:
monitor_names = [f"'{val[ind].name}'" for ind in failed_volt_mnt]
raise SetupError(
f"Monitors {monitor_names} do not cross any conducting materials "
"('charge=ChargeConductorMedium(...)'). The voltage is only stored inside conducting "
"materials. Thus, no information will be recorded in these monitors."
)
return val
[docs]
@pd.root_validator(skip_on_failure=True)
def check_voltage_array_if_capacitance(cls, values):
"""Make sure an array of voltages has been defined if a
SteadyCapacitanceMonitor' has been defined"""
bounday_spec = values["boundary_spec"]
monitors = values["monitors"]
is_capacitance_mnt = any(isinstance(mnt, SteadyCapacitanceMonitor) for mnt in monitors)
voltage_array_present = False
if is_capacitance_mnt:
for bc in bounday_spec:
if isinstance(bc.condition, VoltageBC):
if isinstance(bc.condition.source, DCVoltageSource):
if isinstance(bc.condition.source.voltage, list) or isinstance(
bc.condition.source.voltage, tuple
):
if len(bc.condition.source.voltage) > 1:
voltage_array_present = True
if is_capacitance_mnt and not voltage_array_present:
raise SetupError(
"Monitors of type 'SteadyCapacitanceMonitor' have been defined but no array of voltages "
"has been supplied as voltage source, which is required for this type of monitor. "
"Voltage arrays can be included in a source in this manner: "
"'VoltageBC(source=DCVoltageSource(voltage=yourArray))'"
)
return values
[docs]
@pd.validator("size", always=True)
def check_zero_dim_domain(cls, val, values):
"""Error if heat domain have zero dimensions."""
dim_names = ["x", "y", "z"]
zero_dimensions = [False, False, False]
zero_dim_str = ""
for n, v in enumerate(val):
if v == 0:
zero_dimensions[n] = True
zero_dim_str += f"{dim_names[n]}- "
num_zero_dims = np.sum(zero_dimensions)
if num_zero_dims > 1:
mssg = f"The current 'HeatChargeSimulation' has zero size along the {zero_dim_str}dimensions. "
mssg += "Only 2- and 3-D simulations are currently supported."
raise SetupError(mssg)
return val
[docs]
@pd.validator("boundary_spec", always=True)
@skip_if_fields_missing(["structures", "medium"])
def names_exist_bcs(cls, val, values):
"""Error if boundary conditions point to non-existing structures/media."""
structures = values.get("structures")
structures_names = {s.name for s in structures}
mediums_names = {s.medium.name for s in structures}
mediums_names.add(values.get("medium").name)
for bc_ind, bc_spec in enumerate(val):
bc_place = bc_spec.placement
if isinstance(bc_place, (StructureBoundary, StructureSimulationBoundary)):
if bc_place.structure not in structures_names:
raise SetupError(
f"Structure '{bc_place.structure}' provided in "
f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}') "
"is not found among simulation structures."
)
if isinstance(bc_place, (StructureStructureInterface)):
for struct_name in bc_place.structures:
if struct_name and struct_name not in structures_names:
raise SetupError(
f"Structure '{struct_name}' provided in "
f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}') "
"is not found among simulation structures."
)
if isinstance(bc_place, (MediumMediumInterface)):
for med_name in bc_place.mediums:
if med_name not in mediums_names:
raise SetupError(
f"Material '{med_name}' provided in "
f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}') "
"is not found among simulation mediums."
)
return val
[docs]
@pd.validator("boundary_spec", always=True)
def check_only_one_voltage_array_provided(cls, val, values):
"""Issue error if more than one voltage array is provided.
Currently we only allow to sweep over one voltage array.
"""
array_already_provided = False
for bc in val:
if isinstance(bc.condition, VoltageBC):
voltages = []
# currently we're only supporting DC BCs, so let's check these values
if isinstance(bc.condition.source, DCVoltageSource):
voltages = bc.condition.source.voltage
if isinstance(voltages, tuple):
if len(voltages) > 1:
if not array_already_provided:
array_already_provided = True
else:
raise SetupError(
"More than one voltage array has been provided. "
"Currently voltage arrays are supported only for one of the BCs."
)
return val
[docs]
@pd.root_validator(skip_on_failure=True)
def check_charge_simulation(cls, values):
"""Makes sure that Charge simulations are set correctly."""
ChargeMonitorType = (
SteadyPotentialMonitor,
SteadyFreeCarrierMonitor,
SteadyCapacitanceMonitor,
)
simulation_types = cls._check_simulation_types(values=values)
if TCADAnalysisTypes.CHARGE in simulation_types:
# check that we have at least 2 'VoltageBC's
boundary_spec = values["boundary_spec"]
voltage_bcs = 0
for bc in boundary_spec:
if isinstance(bc.condition, VoltageBC):
voltage_bcs = voltage_bcs + 1
if voltage_bcs < 2:
raise SetupError(
"Defining a Charge simulation requires the definition of 'VoltageBC' boundaries. "
f"So far {voltage_bcs} 'VoltageBC' have been set."
)
# check that we have at least one charge monitor
monitors = values["monitors"]
if not any(isinstance(mnt, ChargeMonitorType) for mnt in monitors):
raise SetupError(
"Charge simulations require the definition of, at least, one of these monitors: "
"'[SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyCapacitanceMonitor]' "
"but none have been defined."
)
# NOTE: SteadyPotentialMonitor and SteadyFreeCarrierMonitor are only supported
# for unstructured = True
for mnt in monitors:
if isinstance(mnt, SteadyPotentialMonitor) or isinstance(
mnt, SteadyFreeCarrierMonitor
):
if not mnt.unstructured:
log.warning(
"Currently, Charge simulations support only unstructured monitors. Please set "
f"monitor '{mnt.name}' to 'unstructured = True'."
)
return values
[docs]
@pd.root_validator(skip_on_failure=True)
def not_all_neumann(cls, values):
"""Make sure not all BCs are of Neumann type"""
NeumannBCsHeat = (HeatFluxBC,)
NeumannBCsCharge = (CurrentBC, InsulatingBC)
simulation_types = cls._check_simulation_types(values=values)
bounday_conditions = values["boundary_spec"]
raise_error = False
for sim_type in simulation_types:
if sim_type == TCADAnalysisTypes.HEAT:
type_bcs = [
bc for bc in bounday_conditions if isinstance(bc.condition, HeatBCTypes)
]
if len(type_bcs) == 0 or all(
isinstance(bc.condition, NeumannBCsHeat) for bc in type_bcs
):
raise_error = True
elif sim_type == TCADAnalysisTypes.CONDUCTION:
type_bcs = [
bc for bc in bounday_conditions if isinstance(bc.condition, ElectricBCTypes)
]
if len(type_bcs) == 0 or all(
isinstance(bc.condition, NeumannBCsCharge) for bc in type_bcs
):
raise_error = True
names_neumann_Bcs = [BC.__name__ for BC in NeumannBCsHeat]
names_neumann_Bcs.extend([BC.__name__ for BC in NeumannBCsCharge])
if raise_error:
raise SetupError(
"Current 'HeatChargeSimulation' contains only Neumann-type boundary conditions. "
"Steady-state solution is undefined in this case. "
f"Current Neumann BCs are {names_neumann_Bcs}"
)
return values
[docs]
@pd.validator("grid_spec", always=True)
@skip_if_fields_missing(["structures"])
def names_exist_grid_spec(cls, val, values):
"""Warn if 'UniformUnstructuredGrid' points at a non-existing structure."""
structures = values.get("structures")
structures_names = {s.name for s in structures}
for structure_name in val.non_refined_structures:
if structure_name not in structures_names:
log.warning(
f"Structure '{structure_name}' listed as a non-refined structure in "
"'HeatChargeSimulation.grid_spec' is not present in 'HeatChargeSimulation.structures'"
)
return val
[docs]
@pd.validator("grid_spec", always=True)
def warn_if_minimal_mesh_size_override(cls, val, values):
"""Warn if minimal mesh size limit overrides desired mesh size."""
max_size = np.max(values.get("size"))
min_dl = val.relative_min_dl * max_size
if isinstance(val, UniformUnstructuredGrid):
desired_min_dl = val.dl
if isinstance(val, DistanceUnstructuredGrid):
desired_min_dl = min(val.dl_interface, val.dl_bulk)
if desired_min_dl < min_dl:
log.warning(
f"The resulting limit for minimal mesh size from parameter 'relative_min_dl={val.relative_min_dl}' is {min_dl}, while provided mesh size in 'grid_spec' is {desired_min_dl}. "
"Consider lowering parameter 'relative_min_dl' if a finer grid is required."
)
return val
[docs]
@pd.validator("sources", always=True)
@skip_if_fields_missing(["structures"])
def names_exist_sources(cls, val, values):
"""Error if a heat-charge source point to non-existing structures."""
structures = values.get("structures")
structures_names = {s.name for s in structures}
sources = [s for s in val if not isinstance(s, HeatFromElectricSource)]
for source in sources:
for name in source.structures:
if name not in structures_names:
raise SetupError(
f"Structure '{name}' provided in a '{source.type}' "
"is not found among simulation structures."
)
return val
[docs]
@pd.root_validator(skip_on_failure=True)
def check_medium_specs(cls, values):
"""Error if no appropriate specs."""
sim_box = (
Box(
size=values.get("size"),
center=values.get("center"),
),
)
failed_solid_idx, failed_elect_idx = cls._check_cross_solids(sim_box, values)
simulation_types = cls._check_simulation_types(values=values)
for sim_type in simulation_types:
if sim_type == TCADAnalysisTypes.HEAT:
if len(failed_solid_idx) > 0:
raise SetupError(
"No solid materials ('SolidSpec') are detected in heat simulation. Solution domain is empty."
)
elif sim_type == TCADAnalysisTypes.CONDUCTION:
if len(failed_elect_idx) > 0:
raise SetupError(
"No conducting materials ('ChargeConductorMedium') are detected in conduction simulation. Solution domain is empty."
)
return values
@staticmethod
def _check_if_semiconductor_present(structures) -> bool:
"""Checks whether the simulation object can run a Charge simulation."""
charge_sim = False
# make sure mediums with doping have been defined
for structure in structures:
if isinstance(structure.medium, MultiPhysicsMedium):
if structure.medium.charge is not None:
if isinstance(structure.medium.charge, SemiconductorMedium):
return True
return charge_sim
@staticmethod
def _check_simulation_types(
values: Dict,
HeatBCTypes=HeatBCTypes,
ElectricBCTypes=ElectricBCTypes,
HeatSourceTypes=HeatSourceTypes,
) -> list[TCADAnalysisTypes]:
"""Given model dictionary ``values``, check the type of simulations to be run
based on BCs and sources.
"""
simulation_types = []
boundaries = list(values["boundary_spec"])
sources = list(values["sources"])
structures = list(values["structures"])
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:
simulation_types.append(TCADAnalysisTypes.CONDUCTION)
for source in sources:
if isinstance(source, HeatSourceTypes):
simulation_types.append(TCADAnalysisTypes.HEAT)
return set(simulation_types)
[docs]
@pd.root_validator(skip_on_failure=True)
def check_coupling_source_can_be_applied(cls, values):
"""Error if material doesn't have the right specifications"""
HeatSourceTypes_noCoupling = (UniformHeatSource, HeatSource)
simulation_types = cls._check_simulation_types(
values, HeatSourceTypes=HeatSourceTypes_noCoupling
)
simulation_types = list(simulation_types)
sources = list(values["sources"])
for source in sources:
if isinstance(source, HeatFromElectricSource) and len(simulation_types) < 2:
raise SetupError(
f"Using 'HeatFromElectricSource' requires the definition of both "
f"{TCADAnalysisTypes.CONDUCTION.name} and {TCADAnalysisTypes.HEAT.name}. "
f"The current simulation setup contains only conditions of type {simulation_types[0].name}"
)
return values
[docs]
@pd.root_validator(skip_on_failure=True)
def estimate_charge_mesh_size(cls, values):
"""Make an estimate of the mesh size and raise a warning if too big.
NOTE: this is a very rough estimate. The back-end will actually stop
execution based on actual node-count."""
if TCADAnalysisTypes.CHARGE not in cls._check_simulation_types(values=values):
return values
# let's raise a warning if the estimate is larger than 2M nodes
max_nodes = 2e6
nodes_estimate = 0
structures = values["structures"]
grid_spec = values["grid_spec"]
non_refined_structures = grid_spec.non_refined_structures
if isinstance(grid_spec, UniformUnstructuredGrid):
dl_min = grid_spec.dl
dl_max = dl_min
elif isinstance(grid_spec, DistanceUnstructuredGrid):
dl_min = grid_spec.dl_interface
dl_max = grid_spec.dl_bulk
for struct in structures:
name = struct.name
bounds = struct.geometry.bounds
dl = dl_min
if name in non_refined_structures:
dl = dl_max
nodes_structure = 1
for coord_min, coord_max in zip(bounds[0], bounds[1]):
if (
(coord_min != coord_max)
and (np.abs(coord_min) != np.inf)
and (np.abs(coord_max) != np.inf)
):
nodes_structure = nodes_structure * (coord_max - coord_min) / dl
nodes_estimate = nodes_estimate + nodes_structure
if nodes_estimate > max_nodes:
log.warning(
"WARNING: It has been estimated the mesh to be bigger than the currently "
"supported mesh size for Charge simulations. The simulation may be "
"submitted but if the maximum number of nodes is indeed exceeded "
"the pipeline will be stopped. If this happens the grid specification "
"may need to be modified."
)
return values
[docs]
@equal_aspect
@add_ax_if_none
def plot_property(
self,
x: float = None,
y: float = None,
z: float = None,
ax: Ax = None,
alpha: float = None,
source_alpha: float = None,
monitor_alpha: float = None,
property: str = "heat_conductivity",
hlim: Tuple[float, float] = None,
vlim: Tuple[float, float] = None,
) -> Ax:
"""Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
Parameters
----------
x : float = None
position of plane in x direction, only one of x, y, z must be specified to define plane.
y : float = None
position of plane in y direction, only one of x, y, z must be specified to define plane.
z : float = None
position of plane in z direction, only one of x, y, z must be specified to define plane.
ax : matplotlib.axes._subplots.Axes = None
Matplotlib axes to plot on, if not specified, one is created.
alpha : float = None
Opacity of the structures being plotted.
Defaults to the structure default alpha.
source_alpha : float = None
Opacity of the sources. If ``None``, uses Tidy3d default.
monitor_alpha : float = None
Opacity of the monitors. If ``None``, uses Tidy3d default.
property : str = "heat_conductivity"
Specified the type of simulation for which the plot will be tailored.
Options are ["heat_conductivity", "electric_conductivity", "source"]
hlim : Tuple[float, float] = None
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
vlim : Tuple[float, float] = None
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
Returns
-------
matplotlib.axes._subplots.Axes
The supplied or created matplotlib axes.
"""
hlim, vlim = Scene._get_plot_lims(
bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim
)
cbar_cond = True
simulation_types = self._get_simulation_types()
if property == "source" and len(simulation_types) > 1:
raise ValueError(
"'plot_property' must be called with argument 'property' in "
"'HeatChargeSimulations' with multiple physics, i.e., a 'HeatChargeSimulation' "
f"with both {TCADAnalysisTypes.HEAT.name} and "
f"{TCADAnalysisTypes.CONDUCTION.name} simulation properties."
)
if len(simulation_types) == 1:
if (
property == "heat_conductivity" and TCADAnalysisTypes.CONDUCTION in simulation_types
) or (
property == "electric_conductivity" and TCADAnalysisTypes.HEAT in simulation_types
):
raise ValueError(
f"'property' in 'plot_property()' was defined as {property} but the "
f"simulation is of type {simulation_types[0]}."
)
if property != "source":
ax = self.scene.plot_heat_charge_property(
ax=ax,
x=x,
y=y,
z=z,
cbar=cbar_cond,
alpha=alpha,
hlim=hlim,
vlim=vlim,
property=property,
)
ax = self.plot_sources(
ax=ax, x=x, y=y, z=z, property=property, alpha=source_alpha, hlim=hlim, vlim=vlim
)
ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, alpha=monitor_alpha, hlim=hlim, vlim=vlim)
ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property=property)
ax = Scene._set_plot_bounds(
bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim
)
ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim)
if property == "source":
self._add_source_cbar(ax=ax, property=property)
return ax
[docs]
@equal_aspect
@add_ax_if_none
def plot_boundaries(
self,
x: float = None,
y: float = None,
z: float = None,
property: str = "heat_conductivity",
ax: Ax = None,
) -> Ax:
"""Plot each of simulation's boundary conditions on a plane defined by one nonzero x,y,z
coordinate.
Parameters
----------
x : float = None
position of plane in x direction, only one of x, y, z must be specified to define plane.
y : float = None
position of plane in y direction, only one of x, y, z must be specified to define plane.
z : float = None
position of plane in z direction, only one of x, y, z must be specified to define plane.
property : str = None
Specified the type of simulation for which the plot will be tailored.
Options are ["heat_conductivity", "electric_conductivity"]
ax : matplotlib.axes._subplots.Axes = None
Matplotlib axes to plot on, if not specified, one is created.
Returns
-------
matplotlib.axes._subplots.Axes
The supplied or created matplotlib axes.
"""
# get structure list
structures = [self.simulation_structure]
structures += list(self.structures)
# construct slicing plane
axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z)
center = Box.unpop_axis(position, (0, 0), axis=axis)
size = Box.unpop_axis(0, (inf, inf), axis=axis)
plane = Box(center=center, size=size)
# get boundary conditions in the plane
boundaries = self._construct_heat_charge_boundaries(
structures=structures,
plane=plane,
boundary_spec=self.boundary_spec,
)
# plot boundary conditions
if property == "heat_conductivity" or property == "source":
new_boundaries = [(b, s) for b, s in boundaries if isinstance(b.condition, HeatBCTypes)]
elif property == "electric_conductivity":
new_boundaries = [
(b, s) for b, s in boundaries if isinstance(b.condition, ElectricBCTypes)
]
for bc_spec, shape in new_boundaries:
ax = self._plot_boundary_condition(shape=shape, boundary_spec=bc_spec, ax=ax)
# clean up the axis display
ax = self.add_ax_lims(axis=axis, ax=ax)
ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z)
# Add the default axis labels, tick labels, and title
ax = Box.add_ax_labels_and_title(
ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units
)
return ax
def _get_bc_plot_params(self, boundary_spec: HeatChargeBoundarySpec) -> PlotParams:
"""Constructs the plot parameters for given boundary conditions."""
plot_params = plot_params_heat_bc
condition = boundary_spec.condition
if isinstance(condition, TemperatureBC):
plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_TEMPERATURE)
elif isinstance(condition, HeatFluxBC):
plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_FLUX)
elif isinstance(condition, ConvectionBC):
plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_CONVECTION)
elif isinstance(condition, InsulatingBC):
plot_params = plot_params.updated_copy(facecolor=CHARGE_BC_INSULATOR)
return plot_params
def _plot_boundary_condition(
self, shape: Shapely, boundary_spec: HeatChargeBoundarySpec, ax: Ax
) -> Ax:
"""Plot a structure's cross section shape for a given boundary condition."""
plot_params_bc = self._get_bc_plot_params(boundary_spec=boundary_spec)
ax = self.plot_shape(shape=shape, plot_params=plot_params_bc, ax=ax)
return ax
@staticmethod
def _structure_to_bc_spec_map(
plane: Box,
structures: Tuple[Structure, ...],
boundary_spec: Tuple[HeatChargeBoundarySpec, ...],
) -> Dict[str, HeatChargeBoundarySpec]:
"""Construct structure name to bc spec inverse mapping. One structure may correspond to
multiple boundary conditions."""
named_structures_present = {structure.name for structure in structures if structure.name}
struct_to_bc_spec = {}
for bc_spec in boundary_spec:
bc_place = bc_spec.placement
if (
isinstance(bc_place, (StructureBoundary, StructureSimulationBoundary))
and bc_place.structure in named_structures_present
):
if bc_place.structure in struct_to_bc_spec:
struct_to_bc_spec[bc_place.structure] += [bc_spec]
else:
struct_to_bc_spec[bc_place.structure] = [bc_spec]
if isinstance(bc_place, StructureStructureInterface):
for structure in bc_place.structures:
if structure in named_structures_present:
if structure in struct_to_bc_spec:
struct_to_bc_spec[structure] += [bc_spec]
else:
struct_to_bc_spec[structure] = [bc_spec]
if isinstance(bc_place, SimulationBoundary):
struct_to_bc_spec[HEAT_CHARGE_BACK_STRUCTURE_STR] = [bc_spec]
return struct_to_bc_spec
@staticmethod
def _medium_to_bc_spec_map(
plane: Box,
structures: Tuple[Structure, ...],
boundary_spec: Tuple[HeatChargeBoundarySpec, ...],
) -> Dict[str, HeatChargeBoundarySpec]:
"""Construct medium name to bc spec inverse mapping. One medium may correspond to
multiple boundary conditions."""
named_mediums_present = {
structure.medium.name for structure in structures if structure.medium.name
}
med_to_bc_spec = {}
for bc_spec in boundary_spec:
bc_place = bc_spec.placement
if isinstance(bc_place, MediumMediumInterface):
for med in bc_place.mediums:
if med in named_mediums_present:
if med in med_to_bc_spec:
med_to_bc_spec[med] += [bc_spec]
else:
med_to_bc_spec[med] = [bc_spec]
return med_to_bc_spec
@staticmethod
def _construct_forward_boundaries(
shapes: Tuple[Tuple[str, str, Shapely, Tuple[float, float, float, float]], ...],
struct_to_bc_spec: Dict[str, HeatChargeBoundarySpec],
med_to_bc_spec: Dict[str, HeatChargeBoundarySpec],
background_structure_shape: Shapely,
) -> Tuple[Tuple[HeatChargeBoundarySpec, Shapely], ...]:
"""Construct Simulation, StructureSimulation, Structure, and MediumMedium boundaries."""
# forward foop to take care of Simulation, StructureSimulation, Structure,
# and MediumMediums
boundaries = [] # bc_spec, structure name, shape, bounds
background_shapes = []
for name, medium, shape, bounds in shapes:
# intersect existing boundaries (both structure based and medium based)
for index, (_bc_spec, _name, _bdry, _bounds) in enumerate(boundaries):
# simulation bc is overridden only by StructureSimulationBoundary
if isinstance(_bc_spec.placement, SimulationBoundary):
if name not in struct_to_bc_spec:
continue
if any(
not isinstance(bc_spec.placement, StructureSimulationBoundary)
for bc_spec in struct_to_bc_spec[name]
):
continue
if Box._do_not_intersect(bounds, _bounds, shape, _bdry):
continue
diff_shape = _bdry - shape
boundaries[index] = (_bc_spec, _name, diff_shape, diff_shape.bounds)
# create new structure based boundary
if name in struct_to_bc_spec:
for bc_spec in struct_to_bc_spec[name]:
if isinstance(bc_spec.placement, StructureBoundary):
bdry = shape.exterior
bdry = bdry.intersection(background_structure_shape)
boundaries.append((bc_spec, name, bdry, bdry.bounds))
if isinstance(bc_spec.placement, SimulationBoundary):
boundaries.append((bc_spec, name, shape.exterior, shape.exterior.bounds))
if isinstance(bc_spec.placement, StructureSimulationBoundary):
bdry = background_structure_shape.exterior
bdry = bdry.intersection(shape)
boundaries.append((bc_spec, name, bdry, bdry.bounds))
# create new medium based boundary, and cut or merge relevant background shapes
# loop through background_shapes (note: all background are non-intersecting or merged)
# this is similar to _filter_structures_plane but only mediums participating in BCs
# are tracked
for index, (_medium, _shape, _bounds) in enumerate(background_shapes):
if Box._do_not_intersect(bounds, _bounds, shape, _shape):
continue
diff_shape = _shape - shape
# different medium, remove intersection from background shape
if medium != _medium and len(diff_shape.bounds) > 0:
background_shapes[index] = (_medium, diff_shape, diff_shape.bounds)
# in case when there is a bc between two media
# create a new boundary segment
for bc_spec in med_to_bc_spec[_medium.name]:
if medium.name in bc_spec.placement.mediums:
bdry = shape.exterior.intersection(_shape)
bdry = bdry.intersection(background_structure_shape)
boundaries.append((bc_spec, name, bdry, bdry.bounds))
# same medium, add diff shape to this shape and mark background shape for removal
# note: this only happens if this medium is listed in BCs
else:
shape = shape | diff_shape
background_shapes[index] = None
# after doing this with all background shapes, add this shape to the background
# but only if this medium is listed in BCs
if medium.name in med_to_bc_spec:
background_shapes.append((medium, shape, shape.bounds))
# remove any existing background shapes that have been marked as 'None'
background_shapes = [b for b in background_shapes if b is not None]
# filter out empty geometries
boundaries = [(bc_spec, bdry) for (bc_spec, name, bdry, _) in boundaries if bdry]
return boundaries
@staticmethod
def _construct_reverse_boundaries(
shapes: Tuple[Tuple[str, str, Shapely, Bound], ...],
struct_to_bc_spec: Dict[str, HeatChargeBoundarySpec],
background_structure_shape: Shapely,
) -> Tuple[Tuple[HeatChargeBoundarySpec, Shapely], ...]:
"""Construct StructureStructure boundaries."""
# backward foop 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
# So, in this loop as we go backwards through the structures we:
# - (1) when come upon B, create boundary(B)
# - (2) cut away from it by other structures
# - (3) when come upon A, intersect it with A and mark it as complete,
# that is, no more further modifications
boundaries_reverse = []
for name, _, shape, bounds in shapes[:0:-1]:
minx, miny, maxx, maxy = bounds
# intersect existing boundaries
for index, (_bc_spec, _name, _bdry, _bounds, _completed) in enumerate(
boundaries_reverse
):
if not _completed:
if Box._do_not_intersect(bounds, _bounds, shape, _bdry):
continue
# event (3) from above
if name in _bc_spec.placement.structures:
new_bdry = _bdry.intersection(shape)
boundaries_reverse[index] = (
_bc_spec,
_name,
new_bdry,
new_bdry.bounds,
True,
)
# event (2) from above
else:
new_bdry = _bdry - shape
boundaries_reverse[index] = (
_bc_spec,
_name,
new_bdry,
new_bdry.bounds,
_completed,
)
# create new boundary (event (1) from above)
if name in struct_to_bc_spec:
for bc_spec in struct_to_bc_spec[name]:
if isinstance(bc_spec.placement, StructureStructureInterface):
bdry = shape.exterior
bdry = bdry.intersection(background_structure_shape)
boundaries_reverse.append((bc_spec, name, bdry, bdry.bounds, False))
# filter and append completed boundaries to main list
filtered_boundaries = []
for bc_spec, _, bdry, _, is_completed in boundaries_reverse:
if bdry and is_completed:
filtered_boundaries.append((bc_spec, bdry))
return filtered_boundaries
@staticmethod
def _construct_heat_charge_boundaries(
structures: List[Structure],
plane: Box,
boundary_spec: List[HeatChargeBoundarySpec],
) -> List[Tuple[HeatChargeBoundarySpec, Shapely]]:
"""Compute list of boundary lines to plot on plane.
Parameters
----------
structures : List[:class:`.Structure`]
list of structures to filter on the plane.
plane : :class:`.Box`
target plane.
boundary_spec : List[HeatBoundarySpec]
list of boundary conditions associated with structures.
Returns
-------
List[Tuple[:class:`.HeatBoundarySpec`, shapely.geometry.base.BaseGeometry]]
List of boundary lines and boundary conditions on the plane after merging.
"""
# get structures in the plane and present named structures and media
shapes = [] # structure name, structure medium, shape, bounds
for structure in structures:
# get list of Shapely shapes that intersect at the plane
shapes_plane = plane.intersections_with(structure.geometry)
# append each of them and their medium information to the list of shapes
for shape in shapes_plane:
shapes.append((structure.name, structure.medium, shape, shape.bounds))
background_structure_shape = shapes[0][2]
# construct an inverse mapping structure -> bc for present structures
struct_to_bc_spec = HeatChargeSimulation._structure_to_bc_spec_map(
plane=plane, structures=structures, boundary_spec=boundary_spec
)
# construct an inverse mapping medium -> bc for present mediums
med_to_bc_spec = HeatChargeSimulation._medium_to_bc_spec_map(
plane=plane, structures=structures, boundary_spec=boundary_spec
)
# construct boundaries in 2 passes:
# 1. forward foop to take care of Simulation, StructureSimulation, Structure,
# and MediumMediums
boundaries = HeatChargeSimulation._construct_forward_boundaries(
shapes=shapes,
struct_to_bc_spec=struct_to_bc_spec,
med_to_bc_spec=med_to_bc_spec,
background_structure_shape=background_structure_shape,
)
# 2. reverse loop: construct structure-structure boundary
struct_struct_boundaries = HeatChargeSimulation._construct_reverse_boundaries(
shapes=shapes,
struct_to_bc_spec=struct_to_bc_spec,
background_structure_shape=background_structure_shape,
)
return boundaries + struct_struct_boundaries
[docs]
@equal_aspect
@add_ax_if_none
def plot_sources(
self,
x: float = None,
y: float = None,
z: float = None,
property: str = "heat_conductivity",
hlim: Tuple[float, float] = None,
vlim: Tuple[float, float] = None,
alpha: float = None,
ax: Ax = None,
) -> Ax:
"""Plot each of simulation's sources on a plane defined by one nonzero x,y,z coordinate.
Parameters
----------
x : float = None
position of plane in x direction, only one of x, y, z must be specified to define plane.
y : float = None
position of plane in y direction, only one of x, y, z must be specified to define plane.
z : float = None
position of plane in z direction, only one of x, y, z must be specified to define plane.
property : str = None
Specified the type of simulation for which the plot will be tailored.
Options are ["heat_conductivity", "electric_conductivity"]
hlim : Tuple[float, float] = None
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
vlim : Tuple[float, float] = None
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
alpha : float = None
Opacity of the sources, If ``None`` uses Tidy3d default.
ax : matplotlib.axes._subplots.Axes = None
Matplotlib axes to plot on, if not specified, one is created.
Returns
-------
matplotlib.axes._subplots.Axes
The supplied or created matplotlib axes.
"""
# background can't have source, so no need to add background structure
structures = self.structures
# alpha is None just means plot without any transparency
if alpha is None:
alpha = 1
if alpha <= 0:
return ax
# get appropriate sources
if property == "heat_conductivity" or property == "source":
source_list = [s for s in self.sources if isinstance(s, HeatSourceTypes)]
elif property == "electric_conductivity":
source_list = [s for s in self.sources if isinstance(s, ChargeSourceTypes)]
# distribute source where there are assigned
structure_source_map = {}
for source in source_list:
if not isinstance(source, GlobalHeatChargeSource):
for name in source.structures:
structure_source_map[name] = source
source_list = [structure_source_map.get(structure.name, None) for structure in structures]
axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z)
center = Box.unpop_axis(position, (0, 0), axis=axis)
size = Box.unpop_axis(0, (inf, inf), axis=axis)
plane = Box(center=center, size=size)
source_shapes = self.scene._filter_structures_plane(
structures=structures, plane=plane, property_list=source_list
)
source_min, source_max = self.source_bounds(property=property)
for source, shape in source_shapes:
if source is not None:
ax = self._plot_shape_structure_source(
alpha=alpha,
source=source,
source_min=source_min,
source_max=source_max,
shape=shape,
ax=ax,
)
# clean up the axis display
ax = self.add_ax_lims(axis=axis, ax=ax)
ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z)
# Add the default axis labels, tick labels, and title
ax = Box.add_ax_labels_and_title(
ax=ax, x=x, y=y, z=z, plot_length_units=self.plot_length_units
)
return ax
def _add_source_cbar(self, ax: Ax, property: str = "heat_conductivity"):
"""Add colorbar for heat sources."""
source_min, source_max = self.source_bounds(property=property)
self.scene._add_cbar(
vmin=source_min,
vmax=source_max,
label=f"Volumetric heat rate ({VOLUMETRIC_HEAT_RATE})",
cmap=HEAT_SOURCE_CMAP,
ax=ax,
)
def _safe_float_conversion(self, string) -> float:
"""Function to deal with failed string2float conversion when using
expressions in 'HeatSource'"""
try:
return float(string)
except ValueError:
return None
[docs]
def source_bounds(self, property: str = "heat_conductivity") -> Tuple[float, float]:
"""Compute range of heat sources present in the simulation."""
if property == "heat_conductivity" or property == "source":
rate_list = [
self._safe_float_conversion(source.rate)
for source in self.sources
if isinstance(source, HeatSource)
]
elif property == "electric_conductivity":
rate_list = [
self._safe_float_conversion(source.rate)
for source in self.sources
if isinstance(source, ChargeSourceTypes)
] # this is currently an empty list
rate_list.append(0)
rate_min = min(rate_list)
rate_max = max(rate_list)
return rate_min, rate_max
def _get_structure_source_plot_params(
self,
source: HeatChargeSourceType,
source_min: float,
source_max: float,
alpha: float = None,
) -> PlotParams:
"""Constructs the plot parameters for a given medium in simulation.plot_eps()."""
plot_params = plot_params_heat_source
if alpha is not None:
plot_params = plot_params.copy(update={"alpha": alpha})
if isinstance(source, HeatSource):
rate = self._safe_float_conversion(source.rate)
if rate is not None:
delta_rate = rate - source_min
delta_rate_max = source_max - source_min + 1e-5
rate_fraction = delta_rate / delta_rate_max
cmap = colormaps[HEAT_SOURCE_CMAP]
rgba = cmap(rate_fraction)
plot_params = plot_params.copy(update={"edgecolor": rgba})
return plot_params
def _plot_shape_structure_source(
self,
source: HeatChargeSourceType,
shape: Shapely,
source_min: float,
source_max: float,
ax: Ax,
alpha: float = None,
) -> Ax:
"""Plot a structure's cross section shape for a given medium, grayscale for permittivity."""
plot_params = self._get_structure_source_plot_params(
source=source,
source_min=source_min,
source_max=source_max,
alpha=alpha,
)
ax = self.plot_shape(shape=shape, plot_params=plot_params, ax=ax)
return ax
[docs]
@classmethod
def from_scene(cls, scene: Scene, **kwargs) -> HeatChargeSimulation:
"""Create a simulation from a :class:.`Scene` instance. Must provide additional parameters
to define a valid simulation (for example, ``size``, ``grid_spec``, etc).
Parameters
----------
scene : :class:.`Scene`
Scene containing structures information.
**kwargs
Other arguments
Example
-------
>>> from tidy3d import Scene, Medium, Box, Structure, UniformUnstructuredGrid
>>> box = Structure(
... geometry=Box(center=(0, 0, 0), size=(1, 2, 3)),
... medium=Medium(permittivity=5),
... name="box"
... )
>>> scene = Scene(
... structures=[box],
... medium=Medium(
... permittivity=3,
... heat_spec=SolidSpec(
... conductivity=1, capacity=1,
... ),
... ),
... )
>>> sim = HeatChargeSimulation.from_scene(
... scene=scene,
... center=(0, 0, 0),
... size=(5, 6, 7),
... grid_spec=UniformUnstructuredGrid(dl=0.4),
... boundary_spec=[
... HeatChargeBoundarySpec(
... placement=StructureBoundary(structure="box"),
... condition=TemperatureBC(temperature=500),
... )
... ],
... )
"""
return cls(
structures=scene.structures,
medium=scene.medium,
**kwargs,
)
def _get_simulation_types(self) -> list[TCADAnalysisTypes]:
"""
Checks through BCs and sources and returns the
types of simulations.
"""
simulation_types = []
# NOTE: for the time being, if a simulation has SemiconductorMedium
# then we consider it of being a 'TCADAnalysisTypes.CHARGE'
if self._check_if_semiconductor_present(self.structures):
return [TCADAnalysisTypes.CHARGE]
heat_source_present = any(isinstance(s, HeatSourceTypes) for s in self.sources)
heat_BCs_present = any(isinstance(bc.condition, HeatBCTypes) for bc in self.boundary_spec)
if heat_source_present and not heat_BCs_present:
raise SetupError("Heat sources defined but no heat BCs present.")
elif heat_BCs_present or heat_source_present:
simulation_types.append(TCADAnalysisTypes.HEAT)
# check for conduction simulation
electric_spec_present = any(
structure.medium.charge is not None for structure in self.structures
)
electric_BCs_present = any(
isinstance(bc.condition, ElectricBCTypes) for bc in self.boundary_spec
)
if electric_BCs_present and not electric_spec_present:
raise SetupError(
"Electric BC were specified but no structure in the simulation has "
"a defined '.medium.charge'. Structures with "
"'.medium.charge=None' are treated as insulators, thus, "
"the solution domain is empty."
)
elif electric_BCs_present and electric_spec_present:
simulation_types.append(TCADAnalysisTypes.CONDUCTION)
return simulation_types
def _useHeatSourceFromConductionSim(self):
"""Returns True if 'HeatFromElectricSource' has been defined."""
return any(isinstance(source, HeatFromElectricSource) for source in self.sources)