Source code for tidy3d.components.heat.simulation

"""Defines heat simulation class"""
from __future__ import annotations

from typing import Tuple, List, Dict
from matplotlib import cm

import pydantic.v1 as pd

from .boundary import TemperatureBC, HeatFluxBC, ConvectionBC
from .boundary import HeatBoundarySpec
from .source import HeatSourceType, UniformHeatSource
from .monitor import HeatMonitorType
from .grid import HeatGridType
from .viz import HEAT_BC_COLOR_TEMPERATURE, HEAT_BC_COLOR_FLUX, HEAT_BC_COLOR_CONVECTION
from .viz import plot_params_heat_bc, plot_params_heat_source, HEAT_SOURCE_CMAP

from ..base_sim.simulation import AbstractSimulation
from ..base import cached_property, skip_if_fields_missing
from ..types import Ax, Shapely, TYPE_TAG_STR, ScalarSymmetry, Bound
from ..viz import add_ax_if_none, equal_aspect, PlotParams
from ..structure import Structure
from ..geometry.base import Box, GeometryGroup
from ..geometry.primitives import Sphere, Cylinder
from ..geometry.polyslab import PolySlab
from ..geometry.mesh import TriangleMesh
from ..scene import Scene
from ..heat_spec import SolidSpec

from ..bc_placement import StructureBoundary, StructureStructureInterface
from ..bc_placement import StructureSimulationBoundary, SimulationBoundary
from ..bc_placement import MediumMediumInterface

from ...exceptions import SetupError
from ...constants import inf, VOLUMETRIC_HEAT_RATE

from ...log import log

HEAT_BACK_STRUCTURE_STR = "<<<HEAT_BACKGROUND_STRUCTURE>>>"

HeatSingleGeometryType = (Box, Cylinder, Sphere, PolySlab, TriangleMesh)


[docs] class HeatSimulation(AbstractSimulation): """Contains all information about heat simulation. Example ------- >>> from tidy3d import Medium, SolidSpec, FluidSpec, UniformUnstructuredGrid, TemperatureMonitor >>> heat_sim = HeatSimulation( ... size=(3.0, 3.0, 3.0), ... structures=[ ... Structure( ... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), ... medium=Medium( ... permittivity=2.0, heat_spec=SolidSpec( ... conductivity=1, ... capacity=1, ... ) ... ), ... name="box", ... ), ... ], ... medium=Medium(permittivity=3.0, heat_spec=FluidSpec()), ... grid_spec=UniformUnstructuredGrid(dl=0.1), ... sources=[UniformHeatSource(rate=1, structures=["box"])], ... boundary_spec=[ ... HeatBoundarySpec( ... placement=StructureBoundary(structure="box"), ... condition=TemperatureBC(temperature=500), ... ) ... ], ... monitors=[TemperatureMonitor(size=(1, 2, 3), name="sample")], ... ) """ boundary_spec: Tuple[HeatBoundarySpec, ...] = pd.Field( (), title="Boundary Condition Specifications", description="List of boundary condition specifications.", ) sources: Tuple[HeatSourceType, ...] = pd.Field( (), title="Heat Sources", description="List of heat sources.", ) monitors: Tuple[HeatMonitorType, ...] = pd.Field( (), title="Monitors", description="Monitors in the simulation.", ) grid_spec: HeatGridType = pd.Field( title="Grid Specification", description="Grid specification for heat 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).", )
[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}]')." ) if isinstance(structure.geometry, GeometryGroup): geometries = structure.geometry.geometries else: geometries = [structure.geometry] for geom in geometries: if isinstance(geom, (GeometryGroup)): raise SetupError( "'HeatSimulation' does not currently support recursive 'GeometryGroup's ('structures[{ind}]')." ) if not isinstance(geom, HeatSingleGeometryType): geom_names = [f"'{cl.__name__}'" for cl in HeatSingleGeometryType] raise SetupError( "'HeatSimulation' does not currently support geometries of type " f"'{geom.type}' ('structures[{ind}]'). Allowed geometries are " f"{', '.join(geom_names)}, " "and non-recursive 'GeometryGroup'." ) return val
[docs] @pd.validator("size", always=True) def check_zero_dim_domain(cls, val, values): """Error if heat domain have zero dimensions.""" if any(length == 0 for length in val): raise SetupError( "'HeatSimulation' does not currently support domains with dimensions of zero size." ) 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 not_all_neumann(cls, val): """Error if all boundary conditions are Neumann bc.""" if len(val) == 0 or all(isinstance(bc_spec.condition, HeatFluxBC) for bc_spec in val): raise SetupError( "Heat simulation contains only 'HeatFluxBC' (Neumann) boundary conditions. Steady-state solution is undefined in this case." ) return val
[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 " "'HeatSimulation.grid_spec' is not present in 'HeatSimulation.structures'" ) return val
[docs] @pd.validator("sources", always=True) @skip_if_fields_missing(["structures"]) def names_exist_sources(cls, val, values): """Error if a heat source point to non-existing structures.""" structures = values.get("structures") structures_names = {s.name for s in structures} for source in val: 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_heat_spec(cls, values): """Error if no structures with SolidSpec.""" medium = values["medium"] structures = values["structures"] if not ( isinstance(medium.heat_spec, SolidSpec) or any(isinstance(struct.medium.heat_spec, SolidSpec) for struct in structures) ): raise SetupError( "No solid materials ('SolidSpec') are detected in heat simulation. Solution domain is empty." ) return values
[docs] @equal_aspect @add_ax_if_none def plot_heat_conductivity( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, alpha: float = None, source_alpha: float = None, monitor_alpha: float = None, colorbar: str = "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. colorbar: str = "conductivity" Display colorbar for thermal conductivity ("conductivity") or heat source rate ("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 = colorbar == "conductivity" ax = self.scene.plot_heat_conductivity( ax=ax, x=x, y=y, z=z, cbar=cbar_cond, alpha=alpha, hlim=hlim, vlim=vlim ) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, 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) 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 colorbar == "source": self._add_heat_source_cbar(ax=ax) return ax
[docs] @equal_aspect @add_ax_if_none def plot_boundaries( self, x: float = None, y: float = None, z: float = None, 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. 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_boundaries( structures=structures, plane=plane, boundary_spec=self.boundary_spec, ) # plot boundary conditions for bc_spec, shape in boundaries: ax = self._plot_boundary_condition(shape=shape, boundary_spec=bc_spec, ax=ax) # clean up the axis display axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) ax = self.add_ax_labels_lims(axis=axis, ax=ax) ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) return ax
def _get_bc_plot_params(self, boundary_spec: HeatBoundarySpec) -> 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) return plot_params def _plot_boundary_condition( self, shape: Shapely, boundary_spec: HeatBoundarySpec, 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[HeatBoundarySpec, ...] ) -> Dict[str, HeatBoundarySpec]: """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_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[HeatBoundarySpec, ...] ) -> Dict[str, HeatBoundarySpec]: """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, HeatBoundarySpec], med_to_bc_spec: Dict[str, HeatBoundarySpec], background_structure_shape: Shapely, ) -> Tuple[Tuple[HeatBoundarySpec, 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, HeatBoundarySpec], background_structure_shape: Shapely, ) -> Tuple[Tuple[HeatBoundarySpec, 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_boundaries( structures: List[Structure], plane: Box, boundary_spec: List[HeatBoundarySpec], ) -> List[Tuple[HeatBoundarySpec, 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 = HeatSimulation._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 = HeatSimulation._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 = HeatSimulation._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 = HeatSimulation._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, 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. 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 # distribute source where there are assigned structure_source_map = {} for source in self.sources: 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 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 axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) ax = self.add_ax_labels_lims(axis=axis, ax=ax) ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) return ax
def _add_heat_source_cbar(self, ax: Ax): """Add colorbar for heat sources.""" source_min, source_max = self.source_bounds self.scene._add_cbar( vmin=source_min, vmax=source_max, label=f"Volumetric heat rate ({VOLUMETRIC_HEAT_RATE})", cmap=HEAT_SOURCE_CMAP, ax=ax, ) @cached_property def source_bounds(self) -> Tuple[float, float]: """Compute range of heat sources present in the simulation.""" rate_list = [ source.rate for source in self.sources if isinstance(source, UniformHeatSource) ] 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: HeatSourceType, 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, UniformHeatSource): rate = source.rate delta_rate = rate - source_min delta_rate_max = source_max - source_min + 1e-5 rate_fraction = delta_rate / delta_rate_max cmap = cm.get_cmap(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: HeatSourceType, 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) -> HeatSimulation: """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), ... ) >>> scene = Scene( ... structures=[box], ... medium=Medium( ... permittivity=3, ... heat_spec=SolidSpec( ... conductivity=1, capacity=1, ... ), ... ), ... ) >>> sim = HeatSimulation.from_scene( ... scene=scene, ... center=(0, 0, 0), ... size=(5, 6, 7), ... grid_spec=UniformUnstructuredGrid(dl=0.4), ... boundary_spec=[ ... HeatBoundarySpec( ... placement=SimulationBoundary(), ... condition=TemperatureBC(temperature=300) ... ) ... ], ... ) """ return cls( structures=scene.structures, medium=scene.medium, **kwargs, )