Source code for tidy3d.components.tcad.simulation.heat_charge

# 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)