diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 985e6d5044..5a37c7cfce 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -256,6 +256,10 @@ def monitors(): electric_field_mnt = td.SteadyElectricFieldMonitor(size=(1.6, 2, 3), name="electric_field_test") + current_density_mnt = td.SteadyCurrentDensityMonitor( + size=(1.6, 2, 3), name="electric_field_mnt" + ) + return [ temp_mnt1, # 0 temp_mnt2, # 1 @@ -270,6 +274,7 @@ def monitors(): energy_band_mnt1, # 10 mesh_mnt, # 11 electric_field_mnt, # 12 + current_density_mnt, # 13 ] @@ -755,6 +760,19 @@ def electric_field_monitor_data(monitors): return (mnt_data1, mnt_data2, mnt_data3) +@pytest.fixture(scope="module") +def current_density_monitor_data(monitors, electric_field_monitor_data): + """Creates different current density monitor data.""" + monitor = monitors[13] + e_data1, e_data2, e_data3 = electric_field_monitor_data + + mnt_data1 = td.SteadyCurrentDensityData(monitor=monitor, J=e_data1.E) + mnt_data2 = td.SteadyCurrentDensityData(monitor=monitor, J=e_data2.E) + mnt_data3 = td.SteadyCurrentDensityData(monitor=monitor, J=e_data3.E) + + return (mnt_data1, mnt_data2, mnt_data3) + + @pytest.fixture(scope="module") def simulation_data( heat_simulation, @@ -929,52 +947,68 @@ def test_monitor_crosses_medium(mediums, structures, heat_simulation, conduction def test_heat_charge_mnt_data( - temperature_monitor_data, voltage_monitor_data, electric_field_monitor_data + temperature_monitor_data, + voltage_monitor_data, + electric_field_monitor_data, + current_density_monitor_data, ): """Tests whether different heat-charge monitor data can be created.""" assert len(temperature_monitor_data) == 4, "Expected 4 temperature monitor data entries." assert len(voltage_monitor_data) == 4, "Expected 4 voltage monitor data entries." assert len(electric_field_monitor_data) == 3, "Expected 3 electric field monitor data entries." + assert len(current_density_monitor_data) == 3, ( + "Expected 3 current density monitor data entries." + ) + + for var, mnt_data_lists in [ + ("E", electric_field_monitor_data), + ("J", current_density_monitor_data), + ]: + for mnt_data in mnt_data_lists: + assert var in mnt_data.field_components.keys() + + symm_data = mnt_data.symmetry_expanded_copy + if var == "E": + assert symm_data.E == mnt_data.E + elif var == "J": + assert symm_data.J == mnt_data.J + + names = mnt_data.field_name("abs^2") + assert names == var + "²" + names = mnt_data.field_name() + assert names == var + + # make sure an error is raised if we don't use a field data array + # TriangularGridDataset + tri_grid_points = td.PointDataArray( + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], + dims=("index", "axis"), + ) - for mnt_data in electric_field_monitor_data: - assert "E" in mnt_data.field_components.keys() - - symm_data = mnt_data.symmetry_expanded_copy - assert symm_data.E == mnt_data.E - - names = mnt_data.field_name("abs^2") - assert names == "E²" - names = mnt_data.field_name() - assert names == "E" - - # make sure an error is raised if we don't use a field data array - # TriangularGridDataset - tri_grid_points = td.PointDataArray( - [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], - dims=("index", "axis"), - ) - - tri_grid_cells = td.CellDataArray( - [[0, 1, 2], [1, 2, 3]], - dims=("cell_index", "vertex_index"), - ) + tri_grid_cells = td.CellDataArray( + [[0, 1, 2], [1, 2, 3]], + dims=("cell_index", "vertex_index"), + ) - tri_grid_values = td.IndexedDataArray( - [1.0, 2.0, 3.0, 4.0], - dims=("index",), - name="T", - ) + tri_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0], + dims=("index",), + name="T", + ) - tri_grid = td.TriangularGridDataset( - normal_axis=1, - normal_pos=0, - points=tri_grid_points, - cells=tri_grid_cells, - values=tri_grid_values, - ) + tri_grid = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values, + ) - with pytest.raises(pd.ValidationError): - _ = mnt_data.updated_copy(E=tri_grid) + with pytest.raises(pd.ValidationError): + if var == "E": + _ = mnt_data.updated_copy(E=tri_grid) + elif var == "J": + _ = mnt_data.updated_copy(J=tri_grid) def test_grid_spec_validation(grid_specs): diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 50103849e2..c70c175362 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -37,6 +37,7 @@ ) from tidy3d.components.tcad.data.types import ( SteadyCapacitanceData, + SteadyCurrentDensityData, SteadyElectricFieldData, SteadyEnergyBandData, SteadyFreeCarrierData, @@ -54,6 +55,7 @@ from tidy3d.components.tcad.mesher import VolumeMesher from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, @@ -659,6 +661,8 @@ def set_logging_level(level: str) -> None: "Staircasing", "SteadyCapacitanceData", "SteadyCapacitanceMonitor", + "SteadyCurrentDensityData", + "SteadyCurrentDensityMonitor", "SteadyElectricFieldData", "SteadyElectricFieldMonitor", "SteadyEnergyBandData", diff --git a/tidy3d/components/tcad/data/monitor_data/charge.py b/tidy3d/components/tcad/data/monitor_data/charge.py index 8a08fdbe97..33718adff2 100644 --- a/tidy3d/components/tcad/data/monitor_data/charge.py +++ b/tidy3d/components/tcad/data/monitor_data/charge.py @@ -20,6 +20,7 @@ from tidy3d.components.tcad.data.monitor_data.abstract import HeatChargeMonitorData from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, @@ -467,7 +468,7 @@ def symmetry_expanded_copy(self) -> SteadyCapacitanceData: class SteadyElectricFieldData(HeatChargeMonitorData): """ - Stores electric field :math:`\\vec{E}` from a charge simulation. + Stores electric field :math:`\\vec{E}` from a Charge/Conduction simulation. Notes ----- @@ -478,7 +479,7 @@ class SteadyElectricFieldData(HeatChargeMonitorData): monitor: SteadyElectricFieldMonitor = pd.Field( ..., title="Electric field monitor", - description="Electric field data associated with a Charge simulation.", + description="Electric field data associated with a Charge/Conduction simulation.", ) E: UnstructuredFieldType = pd.Field( @@ -542,3 +543,78 @@ def field_name(self, val: str = "") -> str: return "E²" else: return "E" + + +class SteadyCurrentDensityData(HeatChargeMonitorData): + """ + Stores current density :math:`\\vec{J}` from a Charge/Conduction simulation. It is given in + units of :math:`A/\\mu m^2` + """ + + monitor: SteadyCurrentDensityMonitor = pd.Field( + ..., + title="Current density monitor", + description="Current density data associated with a Charge/Conduction simulation.", + ) + + J: UnstructuredFieldType = pd.Field( + None, + title="Current density", + description=r"Contains the computed current density in :math:`A/\\mu m^2`.", + discriminator=TYPE_TAG_STR, + ) + + @property + def field_components(self) -> dict[str, UnstructuredFieldType]: + """Maps the field components to their associated data.""" + return {"J": self.J} + + @pd.root_validator(skip_on_failure=True) + def warn_no_data(cls, values): + """Warn if no data provided.""" + + mnt = values.get("monitor") + J = values.get("J") + + if J is None: + log.warning( + f"No data is available for monitor '{mnt.name}'. This is typically caused by " + "monitor not intersecting any solid medium." + ) + + return values + + @pd.root_validator(skip_on_failure=True) + def check_correct_data_type(cls, values): + """Issue error if incorrect data type is used""" + + mnt = values.get("monitor") + J = values.get("J") + + if isinstance(J, TetrahedralGridDataset) or isinstance(J, TriangularGridDataset): + AcceptedTypes = (IndexedFieldVoltageDataArray, PointDataArray) + if not isinstance(J.values, AcceptedTypes): + raise ValueError( + f"In the data associated with monitor {mnt}, must contain a field. This can be " + "defined with IndexedFieldVoltageDataArray or PointDataArray." + ) + + return values + + @property + def symmetry_expanded_copy(self) -> SteadyCurrentDensityData: + """Return copy of self with symmetry applied.""" + + new_J = self._symmetry_expanded_copy(property=self.J) + + return self.updated_copy( + J=new_J, + symmetry=(0, 0, 0), + ) + + def field_name(self, val: str = "") -> str: + """Gets the name of the fields to be plotted.""" + if val == "abs^2": + return "J²" + else: + return "J" diff --git a/tidy3d/components/tcad/data/types.py b/tidy3d/components/tcad/data/types.py index cb2045584d..f072f11cd7 100644 --- a/tidy3d/components/tcad/data/types.py +++ b/tidy3d/components/tcad/data/types.py @@ -6,6 +6,7 @@ from tidy3d.components.tcad.data.monitor_data.charge import ( SteadyCapacitanceData, + SteadyCurrentDensityData, SteadyElectricFieldData, SteadyEnergyBandData, SteadyFreeCarrierData, @@ -20,4 +21,5 @@ SteadyElectricFieldData, SteadyEnergyBandData, SteadyCapacitanceData, + SteadyCurrentDensityData, ] diff --git a/tidy3d/components/tcad/monitors/charge.py b/tidy3d/components/tcad/monitors/charge.py index dc56129346..63838cc341 100644 --- a/tidy3d/components/tcad/monitors/charge.py +++ b/tidy3d/components/tcad/monitors/charge.py @@ -84,7 +84,7 @@ class SteadyCapacitanceMonitor(HeatChargeMonitor): class SteadyElectricFieldMonitor(HeatChargeMonitor): """ - Electric field monitor for Charge simulations. + Electric field monitor for Charge/Conduction simulations. Example ------- @@ -99,3 +99,22 @@ class SteadyElectricFieldMonitor(HeatChargeMonitor): title="Unstructured Grid", description="Return data on the original unstructured grid.", ) + + +class SteadyCurrentDensityMonitor(HeatChargeMonitor): + """ + Current density monitor for Charge/Conduction simulations. + + Example + ------- + >>> import tidy3d as td + >>> current_density_monitor_z0 = td.SteadyCurrentDensityMonitor( + ... center=(0, 0.14, 0), size=(0.6, 0.3, 0), name="current_density_z0", + ... ) + """ + + unstructured: Literal[True] = pd.Field( + True, + title="Unstructured Grid", + description="Return data on the original unstructured grid.", + ) diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index e409256574..4688124750 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -49,6 +49,7 @@ ) from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, ) @@ -546,6 +547,7 @@ def check_charge_simulation(cls, values): SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, ) simulation_types = cls._check_simulation_types(values=values) @@ -568,7 +570,7 @@ def check_charge_simulation(cls, values): if not any(isinstance(mnt, ChargeMonitorType) for mnt in monitors): raise SetupError( "Charge simulations require the definition of, at least, one of these monitors: " - "'[SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyCapacitanceMonitor]' " + "'[SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyCapacitanceMonitor, SteadyCurrentDensityMonitor]' " "but none have been defined." ) diff --git a/tidy3d/components/tcad/types.py b/tidy3d/components/tcad/types.py index dec3f44f62..9717a71644 100644 --- a/tidy3d/components/tcad/types.py +++ b/tidy3d/components/tcad/types.py @@ -13,6 +13,7 @@ from tidy3d.components.tcad.mobility import CaugheyThomasMobility, ConstantMobilityModel from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, @@ -37,6 +38,7 @@ SteadyEnergyBandMonitor, SteadyElectricFieldMonitor, SteadyCapacitanceMonitor, + SteadyCurrentDensityMonitor, ] HeatChargeSourceType = Union[HeatSource, HeatFromElectricSource, UniformHeatSource] HeatChargeBCType = Union[