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. 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..9965fb138a 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" - The heat-charge siimulation property to plot. The options are - ["heat_conductivity", "electric_conductivity"] + property : Literal["heat_conductivity", "electric_conductivity", "charge"] = "heat_conductivity" + 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. 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] 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)