From ddac266514a387cc3af8bb367b17ec86fe5cab36 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Thu, 4 Dec 2025 15:51:09 +0100 Subject: [PATCH 1/3] Adding plot function for charge --- tests/test_components/test_heat_charge.py | 39 +++++++++++--- tidy3d/components/scene.py | 54 ++++++++++++++----- .../components/tcad/simulation/heat_charge.py | 21 +++++--- 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 438c830549..0563ad7592 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -83,6 +83,12 @@ class CHARGE_SIMULATION: # -------------------------- +@pytest.fixture(scope="module") +def charge_tolerance(): + """Charge tolerance settings for simulations.""" + return td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400) + + @pytest.fixture(scope="module") def mediums(): """Creates mediums with different specifications.""" @@ -385,7 +391,7 @@ def voltage_capacitance_simulation(mediums, structures, boundary_conditions, mon condition=td.VoltageBC(source=td.DCVoltageSource(voltage=0)), ) - # Let’s pick a couple of monitors. We'll definitely include the CapacitanceMonitor + # Let's pick a couple of monitors. We'll definitely include the CapacitanceMonitor # (monitors[8] -> 'cap_mt1') so that we can measure capacitance. We can also include # a potential monitor to see the fields, e.g. monitors[4] -> volt_mnt1 for demonstration. cap_monitor = monitors[8] # 'capacitance_mnt1' @@ -1527,11 +1533,6 @@ def capacitance_global_mnt(self): unstructured=True, ) - # Define charge settings as fixtures within the class - @pytest.fixture(scope="class") - def charge_tolerance(self): - return td.ChargeToleranceSpec(rel_tol=1e5, abs_tol=1e3, max_iters=400) - def test_charge_simulation( self, Si_n, @@ -2513,3 +2514,29 @@ def test_generation_recombination(): beta_n=1, beta_p=1, ) + + +def test_plot_property_charge(heat_simulation, conduction_simulation, charge_tolerance): + """Test plot_property with property="charge".""" + + # transform the conduction simulation into a charge simulation + new_structs = [] + for struct in conduction_simulation.structures: + new_medium = CHARGE_SIMULATION.intrinsic_Si.updated_copy(name=struct.medium.name) + new_structs.append(struct.updated_copy(medium=new_medium)) + analysis_spec = td.IsothermalSteadyChargeDCAnalysis( + temperature=300, + convergence_dv=0.1, + tolerance_settings=charge_tolerance, + ) + charge_simulation = conduction_simulation.updated_copy( + structures=new_structs, + analysis_spec=analysis_spec, + validate=False, + ) + charge_simulation.plot_property(property="charge", z=0) + + with pytest.raises(ValueError): + heat_simulation.plot_property(property="charge", z=0) + with pytest.raises(ValueError): + conduction_simulation.plot_property(property="charge", z=0) diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index 583f5221a4..22cd4ea60a 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -564,12 +564,14 @@ def _plot_shape_structure( shape: Shapely, ax: Ax, fill: bool = True, + property: str = "heat_conductivity", ) -> Ax: """Plot a structure's cross section shape for a given medium.""" plot_params_struct = self._get_structure_plot_params( medium=medium, mat_index=mat_index, fill=fill, + property=property, ) ax = self.box.plot_shape(shape=shape, plot_params=plot_params_struct, ax=ax) return ax @@ -579,6 +581,7 @@ def _get_structure_plot_params( mat_index: int, medium: MultiPhysicsMediumType3D, fill: bool = True, + property: str = "heat_conductivity", ) -> PlotParams: """Constructs the plot parameters for a given medium in scene.plot().""" @@ -629,6 +632,9 @@ def _get_structure_plot_params( if medium.viz_spec is not None: plot_params = plot_params.override_with_viz_spec(medium.viz_spec) + if property == "charge": + plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1}) + if not fill: plot_params = plot_params.copy(update={"fill": False}) if plot_params.linewidth == 0: @@ -1496,7 +1502,9 @@ def plot_heat_charge_property( z: Optional[float] = None, alpha: Optional[float] = None, cbar: bool = True, - property: str = "heat_conductivity", + property: Literal[ + "heat_conductivity", "electric_conductivity", "charge" + ] = "heat_conductivity", ax: Ax = None, hlim: Optional[tuple[float, float]] = None, vlim: Optional[tuple[float, float]] = None, @@ -1517,9 +1525,9 @@ def plot_heat_charge_property( Defaults to the structure default alpha. cbar : bool = True Whether to plot a colorbar for the thermal conductivity. - property : str = "heat_conductivity" + property : Literal["heat_conductivity", "electric_conductivity", "charge"] = "heat_conductivity" The heat-charge siimulation property to plot. The options are - ["heat_conductivity", "electric_conductivity"] + ["heat_conductivity", "electric_conductivity", "charge"] ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. hlim : Tuple[float, float] = None @@ -1615,7 +1623,9 @@ def plot_structures_heat_charge_property( z: Optional[float] = None, alpha: Optional[float] = None, cbar: bool = True, - property: str = "heat_conductivity", + property: Literal[ + "heat_conductivity", "electric_conductivity", "charge" + ] = "heat_conductivity", reverse: bool = False, ax: Ax = None, hlim: Optional[tuple[float, float]] = None, @@ -1676,16 +1686,26 @@ def plot_structures_heat_charge_property( property_val_min, property_val_max = self.heat_charge_property_bounds(property=property) for medium, shape in medium_shapes: - ax = self._plot_shape_structure_heat_charge_property( - alpha=alpha, - medium=medium, - property_val_min=property_val_min, - property_val_max=property_val_max, - reverse=reverse, - shape=shape, - ax=ax, - property=property, - ) + if property == "charge": + ax = self._plot_shape_structure( + medium=medium, + mat_index=self.medium_map[medium], + shape=shape, + ax=ax, + fill=True, + property=property, + ) + else: + ax = self._plot_shape_structure_heat_charge_property( + alpha=alpha, + medium=medium, + property_val_min=property_val_min, + property_val_max=property_val_max, + reverse=reverse, + shape=shape, + ax=ax, + property=property, + ) if cbar: label = "" @@ -1732,6 +1752,8 @@ def heat_charge_property_bounds(self, property) -> tuple[float, float]: medium for medium in medium_list if isinstance(medium.charge, ChargeConductorMedium) ] cond_list = [medium.charge.conductivity for medium in cond_mediums] + elif property == "charge": + return 0, 1 # Return a default range for 'charge' property if len(cond_list) == 0: cond_list = [0] @@ -1786,6 +1808,10 @@ def _get_structure_heat_charge_property_plot_params( cond_medium = medium.charge.conductivity elif property == "doping": cond_medium = None + elif property == "charge": + plot_params = plot_params.copy( + update={"facecolor": "lightgray", "edgecolor": "k", "linewidth": 1} + ) if cond_medium is not None: delta_cond = cond_medium - property_val_min diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 73dd7c130f..6e22c92925 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1187,7 +1187,7 @@ def plot_property( Opacity of the monitors. If ``None``, uses Tidy3d default. property : str = "heat_conductivity" Specified the type of simulation for which the plot will be tailored. - Options are ["heat_conductivity", "electric_conductivity", "source"] + Options are ["heat_conductivity", "electric_conductivity", "source", "charge"] 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 @@ -1204,6 +1204,8 @@ def plot_property( ) cbar_cond = True + if property == "charge": + cbar_cond = False simulation_types = self._get_simulation_types() if property == "source" and len(simulation_types) > 1: @@ -1215,9 +1217,15 @@ def plot_property( ) if len(simulation_types) == 1: if ( - property == "heat_conductivity" and TCADAnalysisTypes.CONDUCTION in simulation_types - ) or ( - property == "electric_conductivity" and TCADAnalysisTypes.HEAT in simulation_types + ( + property == "heat_conductivity" + and TCADAnalysisTypes.CONDUCTION in simulation_types + ) + or ( + property == "electric_conductivity" + and TCADAnalysisTypes.HEAT in simulation_types + ) + or (property == "charge" and TCADAnalysisTypes.CHARGE not in simulation_types) ): raise ValueError( f"'property' in 'plot_property()' was defined as {property} but the " @@ -1378,7 +1386,7 @@ def plot_boundaries( # plot boundary conditions if property == "heat_conductivity" or property == "source": new_boundaries = [(b, s) for b, s in boundaries if isinstance(b.condition, HeatBCTypes)] - elif property == "electric_conductivity": + elif property == "electric_conductivity" or property == "charge": new_boundaries = [ (b, s) for b, s in boundaries if isinstance(b.condition, ElectricBCTypes) ] @@ -1762,7 +1770,7 @@ def plot_sources( # get appropriate sources if property == "heat_conductivity" or property == "source": source_list = [s for s in self.sources if isinstance(s, HeatSourceTypes)] - elif property == "electric_conductivity": + elif property == "electric_conductivity" or property == "charge": source_list = [s for s in self.sources if isinstance(s, ChargeSourceTypes)] # distribute source where there are assigned @@ -1818,6 +1826,7 @@ def _add_source_cbar(self, ax: Ax, property: str = "heat_conductivity") -> None: def source_bounds(self, property: str = "heat_conductivity") -> tuple[float, float]: """Compute range of heat sources present in the simulation.""" + rate_list = [] if property == "heat_conductivity" or property == "source": rate_list = [ np.mean(source.rate) for source in self.sources if isinstance(source, HeatSource) From 2da99ab02236cae6141143b7f635c9d6d9f6d2ca Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Thu, 4 Dec 2025 17:08:32 +0100 Subject: [PATCH 2/3] Adding entry to Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d10cd5983a..9468f232ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `symmetrize_mirror`, `symmetrize_rotation`, `symmetrize_diagonal` functions to the autograd plugin. They can be used for enforcing symmetries in topology optimization. +- Added property `charge` to the `plot_property` of `HeatChargeSimulation`. This allows to visualize charge simulations with its BCs. ### Changed - Removed validator that would warn if `PerturbationMedium` values could become numerically unstable, since an error will anyway be raised if this actually happens when the medium is converted using actual perturbation data. From 0d6be703fc3a5087df25e776fb5c7866660e382f Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Fri, 5 Dec 2025 09:16:34 +0100 Subject: [PATCH 3/3] Copilot comments --- tidy3d/components/scene.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index 22cd4ea60a..9965fb138a 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -1526,7 +1526,7 @@ def plot_heat_charge_property( cbar : bool = True Whether to plot a colorbar for the thermal conductivity. property : Literal["heat_conductivity", "electric_conductivity", "charge"] = "heat_conductivity" - The heat-charge siimulation property to plot. The options are + The heat-charge simulation property to plot. The options are ["heat_conductivity", "electric_conductivity", "charge"] ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. @@ -1808,10 +1808,6 @@ def _get_structure_heat_charge_property_plot_params( cond_medium = medium.charge.conductivity elif property == "doping": cond_medium = None - elif property == "charge": - plot_params = plot_params.copy( - update={"facecolor": "lightgray", "edgecolor": "k", "linewidth": 1} - ) if cond_medium is not None: delta_cond = cond_medium - property_val_min