From f7df7a764bfb4e4523cc4c94d78e9c80b5dff75d Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Thu, 18 Dec 2025 15:08:20 +0100 Subject: [PATCH] feat(FXC-4425) Updated HeatChargeSimulation::plot() so that electric BCs are plotted in Charge simulations --- CHANGELOG.md | 1 + tests/test_components/test_heat_charge.py | 130 ++++++++++++++++++ .../components/tcad/simulation/heat_charge.py | 76 +++++++++- 3 files changed, 205 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f84be6c38..c8a6347ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 6d0ed00655..d09092fb8d 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -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()" + ) diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 59b08ab1d7..06c91f9cc1 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -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 @@ -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( @@ -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: