Skip to content

Commit 7a85bb5

Browse files
committed
feat(FXC-4425): Updated HeatChargeSimulation::plot() so that electric BCs are plotted in Charge simulations
1 parent 95dbdf2 commit 7a85bb5

File tree

3 files changed

+205
-2
lines changed

3 files changed

+205
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Added validation to `DCVoltageSource` that warns when duplicate voltage values are detected in the `voltage` array, including treating `0` and `-0` as the same value.
1616

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

1920
### Fixed
2021
- Fixed `AutoImpedanceSpec` validation to check path intersections against all conductors, not just filtered ones, as well as the mode plane bounds.

tests/test_components/test_heat_charge.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,3 +2616,133 @@ def test_heat_only_simulation_with_semiconductor():
26162616
assert TCADAnalysisTypes.CONDUCTION not in simulation_types, (
26172617
"Conduction simulation should NOT be triggered when no electric BCs are present."
26182618
)
2619+
2620+
2621+
def test_heat_charge_simulation_plot():
2622+
"""Test the HeatChargeSimulation.plot() method adds BCs based on simulation type."""
2623+
2624+
# Create mediums
2625+
solid_medium = td.MultiPhysicsMedium(
2626+
heat=td.SolidMedium(conductivity=1, capacity=1),
2627+
name="solid",
2628+
)
2629+
fluid_medium = td.MultiPhysicsMedium(
2630+
heat=td.FluidMedium(),
2631+
name="fluid",
2632+
)
2633+
2634+
# Create structures
2635+
solid_structure = td.Structure(
2636+
geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)),
2637+
medium=solid_medium,
2638+
name="solid_structure",
2639+
)
2640+
2641+
# Create boundary conditions for heat simulation
2642+
bc_temp = td.HeatChargeBoundarySpec(
2643+
condition=td.TemperatureBC(temperature=300),
2644+
placement=td.StructureBoundary(structure="solid_structure"),
2645+
)
2646+
2647+
# Create heat source
2648+
heat_source = td.UniformHeatSource(rate=1e3, structures=["solid_structure"])
2649+
2650+
# Create monitor
2651+
temp_monitor = td.TemperatureMonitor(
2652+
center=(0, 0, 0),
2653+
size=(1, 1, 0),
2654+
name="temp_mnt",
2655+
)
2656+
2657+
# Create a HEAT simulation
2658+
heat_sim = td.HeatChargeSimulation(
2659+
medium=fluid_medium,
2660+
structures=[solid_structure],
2661+
center=(0, 0, 0),
2662+
size=(2, 2, 2),
2663+
boundary_spec=[bc_temp],
2664+
grid_spec=td.UniformUnstructuredGrid(dl=0.1),
2665+
sources=[heat_source],
2666+
monitors=[temp_monitor],
2667+
)
2668+
2669+
# Test plot for HEAT simulation - should add heat BCs
2670+
_, ax_scene_only = plt.subplots()
2671+
heat_sim.scene.plot(z=0, ax=ax_scene_only)
2672+
num_children_scene_only = len(ax_scene_only.get_children())
2673+
plt.close()
2674+
2675+
_, ax_with_bc = plt.subplots()
2676+
heat_sim.plot(z=0, ax=ax_with_bc)
2677+
num_children_with_bc = len(ax_with_bc.get_children())
2678+
plt.close()
2679+
2680+
# heat_sim.plot() should have more visual elements than scene.plot()
2681+
# because it adds monitors and heat boundaries for HEAT simulations
2682+
assert num_children_with_bc - num_children_scene_only >= 2, (
2683+
"heat_sim.plot() should add at least monitors and heat boundaries "
2684+
"for HEAT simulations, resulting in at least 2 more visual elements "
2685+
"than heat_sim.scene.plot()"
2686+
)
2687+
2688+
# Now test with a CHARGE simulation
2689+
semicon = td.material_library["cSi"].variants["Si_MultiPhysics"].medium.charge
2690+
Si_n = semicon.updated_copy(N_d=[td.ConstantDoping(concentration=1e16)], name="Si_n")
2691+
Si_p = semicon.updated_copy(N_a=[td.ConstantDoping(concentration=1e16)], name="Si_p")
2692+
2693+
n_side = td.Structure(
2694+
geometry=td.Box(center=(-0.25, 0, 0), size=(0.5, 1, 1)),
2695+
medium=Si_n,
2696+
name="n_side",
2697+
)
2698+
p_side = td.Structure(
2699+
geometry=td.Box(center=(0.25, 0, 0), size=(0.5, 1, 1)),
2700+
medium=Si_p,
2701+
name="p_side",
2702+
)
2703+
2704+
bc_v1 = td.HeatChargeBoundarySpec(
2705+
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0)),
2706+
placement=td.MediumMediumInterface(mediums=[fluid_medium.name, Si_n.name]),
2707+
)
2708+
bc_v2 = td.HeatChargeBoundarySpec(
2709+
condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0.5)),
2710+
placement=td.MediumMediumInterface(mediums=[fluid_medium.name, Si_p.name]),
2711+
)
2712+
2713+
volt_monitor = td.SteadyPotentialMonitor(
2714+
center=(0, 0, 0),
2715+
size=(1, 1, 0),
2716+
name="volt_mnt",
2717+
unstructured=True,
2718+
)
2719+
2720+
charge_sim = td.HeatChargeSimulation(
2721+
structures=[n_side, p_side],
2722+
medium=fluid_medium,
2723+
monitors=[volt_monitor],
2724+
center=(0, 0, 0),
2725+
size=(2, 2, 2),
2726+
grid_spec=td.UniformUnstructuredGrid(dl=0.05),
2727+
boundary_spec=[bc_v1, bc_v2],
2728+
analysis_spec=td.IsothermalSteadyChargeDCAnalysis(temperature=300),
2729+
)
2730+
2731+
# Test plot for CHARGE simulation - should add electric BCs
2732+
_, ax_scene_only = plt.subplots()
2733+
charge_sim.scene.plot(z=0, ax=ax_scene_only)
2734+
num_children_scene_only = len(ax_scene_only.get_children())
2735+
plt.close()
2736+
2737+
_, ax_with_bc = plt.subplots()
2738+
charge_sim.plot(z=0, ax=ax_with_bc)
2739+
num_children_with_bc = len(ax_with_bc.get_children())
2740+
plt.close()
2741+
2742+
# charge_sim.plot() should have more visual elements than scene.plot()
2743+
# because it adds monitors and electric boundaries for CHARGE simulations
2744+
assert num_children_with_bc - num_children_scene_only >= 2, (
2745+
"charge_sim.plot() should add at least monitors and electric boundaries "
2746+
"for CHARGE simulations, resulting in at least 2 more visual elements "
2747+
"than charge_sim.scene.plot()"
2748+
)

tidy3d/components/tcad/simulation/heat_charge.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from __future__ import annotations
55

66
from enum import Enum
7-
from typing import Any, Optional, Union
7+
from typing import Any, Literal, Optional, Union
88

99
import numpy as np
1010
import pydantic.v1 as pd
@@ -1162,6 +1162,76 @@ def check_non_isothermal_is_possible(cls, values):
11621162
)
11631163
return values
11641164

1165+
@equal_aspect
1166+
@add_ax_if_none
1167+
def plot(
1168+
self,
1169+
x: Optional[float] = None,
1170+
y: Optional[float] = None,
1171+
z: Optional[float] = None,
1172+
ax: Ax = None,
1173+
source_alpha: Optional[float] = None,
1174+
monitor_alpha: Optional[float] = None,
1175+
hlim: Optional[tuple[float, float]] = None,
1176+
vlim: Optional[tuple[float, float]] = None,
1177+
fill_structures: bool = True,
1178+
**patch_kwargs: Any,
1179+
) -> Ax:
1180+
"""Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate.
1181+
1182+
Parameters
1183+
----------
1184+
x : float = None
1185+
position of plane in x direction, only one of x, y, z must be specified to define plane.
1186+
y : float = None
1187+
position of plane in y direction, only one of x, y, z must be specified to define plane.
1188+
z : float = None
1189+
position of plane in z direction, only one of x, y, z must be specified to define plane.
1190+
ax : matplotlib.axes._subplots.Axes = None
1191+
Matplotlib axes to plot on, if not specified, one is created.
1192+
source_alpha : float = None
1193+
Opacity of the sources. If ``None``, uses Tidy3d default.
1194+
monitor_alpha : float = None
1195+
Opacity of the monitors. If ``None``, uses Tidy3d default.
1196+
hlim : Tuple[float, float] = None
1197+
The x range if plotting on xy or xz planes, y range if plotting on yz plane.
1198+
vlim : Tuple[float, float] = None
1199+
The z range if plotting on xz or yz planes, y plane if plotting on xy plane.
1200+
fill_structures : bool = True
1201+
Whether to fill structures with color or just draw outlines.
1202+
1203+
Returns
1204+
-------
1205+
matplotlib.axes._subplots.Axes
1206+
The supplied or created matplotlib axes.
1207+
"""
1208+
1209+
# Call the parent's plot method
1210+
ax = super().plot(
1211+
x=x,
1212+
y=y,
1213+
z=z,
1214+
ax=ax,
1215+
source_alpha=source_alpha,
1216+
monitor_alpha=monitor_alpha,
1217+
hlim=hlim,
1218+
vlim=vlim,
1219+
fill_structures=fill_structures,
1220+
**patch_kwargs,
1221+
)
1222+
1223+
# Add boundaries based on simulation type
1224+
# NOTE: there's no need to add heat boundaries since
1225+
# they are already added in the parent 'plot' method.
1226+
simulation_types = self._get_simulation_types()
1227+
if (
1228+
TCADAnalysisTypes.CHARGE in simulation_types
1229+
or TCADAnalysisTypes.CONDUCTION in simulation_types
1230+
):
1231+
ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z, property="electric_conductivity")
1232+
1233+
return ax
1234+
11651235
@equal_aspect
11661236
@add_ax_if_none
11671237
def plot_property(
@@ -1173,7 +1243,9 @@ def plot_property(
11731243
alpha: Optional[float] = None,
11741244
source_alpha: Optional[float] = None,
11751245
monitor_alpha: Optional[float] = None,
1176-
property: str = "heat_conductivity",
1246+
property: Literal[
1247+
"heat_conductivity", "electric_conductivity", "source"
1248+
] = "heat_conductivity",
11771249
hlim: Optional[tuple[float, float]] = None,
11781250
vlim: Optional[tuple[float, float]] = None,
11791251
) -> Ax:

0 commit comments

Comments
 (0)