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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

### Changed
- For `HeatChargeSimulation` objects, the `plot` function now adds the simulation boundary conditions.

### Fixed

Expand Down
130 changes: 130 additions & 0 deletions tests/test_components/test_heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -2597,3 +2597,133 @@ def test_heat_only_simulation_with_semiconductor():
assert TCADAnalysisTypes.CONDUCTION not in simulation_types, (
"Conduction simulation should NOT be triggered when no electric BCs are present."
)


def test_heat_charge_simulation_plot():
"""Test the HeatChargeSimulation.plot() method adds BCs based on simulation type."""

# Create mediums
solid_medium = td.MultiPhysicsMedium(
heat=td.SolidMedium(conductivity=1, capacity=1),
name="solid",
)
fluid_medium = td.MultiPhysicsMedium(
heat=td.FluidMedium(),
name="fluid",
)

# Create structures
solid_structure = td.Structure(
geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)),
medium=solid_medium,
name="solid_structure",
)

# Create boundary conditions for heat simulation
bc_temp = td.HeatChargeBoundarySpec(
condition=td.TemperatureBC(temperature=300),
placement=td.StructureBoundary(structure="solid_structure"),
)

# Create heat source
heat_source = td.UniformHeatSource(rate=1e3, structures=["solid_structure"])

# Create monitor
temp_monitor = td.TemperatureMonitor(
center=(0, 0, 0),
size=(1, 1, 0),
name="temp_mnt",
)

# Create a HEAT simulation
heat_sim = td.HeatChargeSimulation(
medium=fluid_medium,
structures=[solid_structure],
center=(0, 0, 0),
size=(2, 2, 2),
boundary_spec=[bc_temp],
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
sources=[heat_source],
monitors=[temp_monitor],
)

# Test plot for HEAT simulation - should add heat BCs
_, ax_scene_only = plt.subplots()
heat_sim.scene.plot(z=0, ax=ax_scene_only)
num_children_scene_only = len(ax_scene_only.get_children())
plt.close()

_, ax_with_bc = plt.subplots()
heat_sim.plot(z=0, ax=ax_with_bc)
num_children_with_bc = len(ax_with_bc.get_children())
plt.close()

# heat_sim.plot() should have more visual elements than scene.plot()
# because it adds monitors and heat boundaries for HEAT simulations
assert num_children_with_bc - num_children_scene_only >= 2, (
"heat_sim.plot() should add at least monitors and heat boundaries "
"for HEAT simulations, resulting in at least 2 more visual elements "
"than heat_sim.scene.plot()"
)

# Now test with a CHARGE simulation
semicon = td.material_library["cSi"].variants["Si_MultiPhysics"].medium.charge
Si_n = semicon.updated_copy(N_d=[td.ConstantDoping(concentration=1e16)], name="Si_n")
Si_p = semicon.updated_copy(N_a=[td.ConstantDoping(concentration=1e16)], name="Si_p")

n_side = td.Structure(
geometry=td.Box(center=(-0.25, 0, 0), size=(0.5, 1, 1)),
medium=Si_n,
name="n_side",
)
p_side = td.Structure(
geometry=td.Box(center=(0.25, 0, 0), size=(0.5, 1, 1)),
medium=Si_p,
name="p_side",
)

bc_v1 = td.HeatChargeBoundarySpec(
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0)),
placement=td.MediumMediumInterface(mediums=[fluid_medium.name, Si_n.name]),
)
bc_v2 = td.HeatChargeBoundarySpec(
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0.5)),
placement=td.MediumMediumInterface(mediums=[fluid_medium.name, Si_p.name]),
)

volt_monitor = td.SteadyPotentialMonitor(
center=(0, 0, 0),
size=(1, 1, 0),
name="volt_mnt",
unstructured=True,
)

charge_sim = td.HeatChargeSimulation(
structures=[n_side, p_side],
medium=fluid_medium,
monitors=[volt_monitor],
center=(0, 0, 0),
size=(2, 2, 2),
grid_spec=td.UniformUnstructuredGrid(dl=0.05),
boundary_spec=[bc_v1, bc_v2],
analysis_spec=td.IsothermalSteadyChargeDCAnalysis(temperature=300),
)

# Test plot for CHARGE simulation - should add electric BCs
_, ax_scene_only = plt.subplots()
charge_sim.scene.plot(z=0, ax=ax_scene_only)
num_children_scene_only = len(ax_scene_only.get_children())
plt.close()

_, ax_with_bc = plt.subplots()
charge_sim.plot(z=0, ax=ax_with_bc)
num_children_with_bc = len(ax_with_bc.get_children())
plt.close()

# charge_sim.plot() should have more visual elements than scene.plot()
# because it adds monitors and electric boundaries for CHARGE simulations
assert num_children_with_bc - num_children_scene_only >= 2, (
"charge_sim.plot() should add at least monitors and electric boundaries "
"for CHARGE simulations, resulting in at least 2 more visual elements "
"than charge_sim.scene.plot()"
)
76 changes: 74 additions & 2 deletions tidy3d/components/tcad/simulation/heat_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from __future__ import annotations

from enum import Enum
from typing import Any, Optional, Union
from typing import Any, Literal, Optional, Union

import numpy as np
import pydantic.v1 as pd
Expand Down Expand Up @@ -1162,6 +1162,76 @@ def check_non_isothermal_is_possible(cls, values):
)
return values

@equal_aspect
@add_ax_if_none
def plot(
self,
x: Optional[float] = None,
y: Optional[float] = None,
z: Optional[float] = None,
ax: Ax = None,
source_alpha: Optional[float] = None,
monitor_alpha: Optional[float] = None,
hlim: Optional[tuple[float, float]] = None,
vlim: Optional[tuple[float, float]] = None,
fill_structures: bool = True,
**patch_kwargs: Any,
) -> 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.
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.
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.
fill_structures : bool = True
Whether to fill structures with color or just draw outlines.

Returns
-------
matplotlib.axes._subplots.Axes
The supplied or created matplotlib axes.
"""

# Call the parent's plot method
ax = super().plot(
x=x,
y=y,
z=z,
ax=ax,
source_alpha=source_alpha,
monitor_alpha=monitor_alpha,
hlim=hlim,
vlim=vlim,
fill_structures=fill_structures,
**patch_kwargs,
)

# Add boundaries based on simulation type
simulation_types = self._get_simulation_types()
if TCADAnalysisTypes.HEAT in simulation_types:
ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property="heat_conductivity")
if (
TCADAnalysisTypes.CHARGE in simulation_types
or TCADAnalysisTypes.CONDUCTION in simulation_types
):
ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property="electric_conductivity")

return ax

@equal_aspect
@add_ax_if_none
def plot_property(
Expand All @@ -1173,7 +1243,9 @@ def plot_property(
alpha: Optional[float] = None,
source_alpha: Optional[float] = None,
monitor_alpha: Optional[float] = None,
property: str = "heat_conductivity",
property: Literal[
"heat_conductivity", "electric_conductivity", "source"
] = "heat_conductivity",
hlim: Optional[tuple[float, float]] = None,
vlim: Optional[tuple[float, float]] = None,
) -> Ax:
Expand Down
Loading