"""Defines heat simulation class"""
from __future__ import annotations
from typing import Dict, List, Tuple
import numpy as np
import pydantic.v1 as pd
from matplotlib import colormaps
from ...constants import VOLUMETRIC_HEAT_RATE, inf
from ...exceptions import SetupError
from ...log import log
from ..base import cached_property, skip_if_fields_missing
from ..base_sim.simulation import AbstractSimulation
from ..bc_placement import (
MediumMediumInterface,
SimulationBoundary,
StructureBoundary,
StructureSimulationBoundary,
StructureStructureInterface,
)
from ..geometry.base import Box
from ..heat_spec import SolidSpec
from ..scene import Scene
from ..structure import Structure
from ..types import TYPE_TAG_STR, Ax, Bound, ScalarSymmetry, Shapely
from ..viz import PlotParams, add_ax_if_none, equal_aspect
from .boundary import ConvectionBC, HeatBoundarySpec, HeatFluxBC, TemperatureBC
from .grid import DistanceUnstructuredGrid, HeatGridType, UniformUnstructuredGrid
from .monitor import HeatMonitorType
from .source import HeatSourceType, UniformHeatSource
from .viz import (
HEAT_BC_COLOR_CONVECTION,
HEAT_BC_COLOR_FLUX,
HEAT_BC_COLOR_TEMPERATURE,
HEAT_SOURCE_CMAP,
plot_params_heat_bc,
plot_params_heat_source,
)
HEAT_BACK_STRUCTURE_STR = "<<<HEAT_BACKGROUND_STRUCTURE>>>"
[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}]')."
)
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.
"""
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)
failed_obj_inds = []
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
)
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)
)
if not crosses_solid:
failed_obj_inds.append(ind)
return failed_obj_inds
@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_mnt_inds = cls._check_cross_solids(val, values)
if len(failed_mnt_inds) > 0:
monitor_names = [f"'{val[ind].name}'" for ind in failed_mnt_inds]
raise SetupError(
f"Monitors {monitor_names} do not cross any solid materials "
"('heat_spec=SolidSpec(...)'). The heat equation is solved only inside solid "
"materials. Thus, no information will be recorded in these monitors."
)
return val
[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"Your current HeatSimulation 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 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("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 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."""
sim_box = (
Box(
size=values.get("size"),
center=values.get("center"),
),
)
failed = cls._check_cross_solids(sim_box, values)
if len(failed) > 0:
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 = 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: 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,
)