diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 09a61b12d..477f2f23d 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -252,18 +252,21 @@ def monitors(): energy_band_mnt1 = td.SteadyEnergyBandMonitor(size=(1.6, 2, 3), name="bandgap_test") + electric_field_mnt = td.SteadyElectricFieldMonitor(size=(1.6, 2, 3), name="electric_field_test") + return [ - temp_mnt1, - temp_mnt2, - temp_mnt3, - temp_mnt4, - volt_mnt1, - volt_mnt2, - volt_mnt3, - volt_mnt4, - capacitance_mnt1, - free_carrier_mnt1, - energy_band_mnt1, + temp_mnt1, # 0 + temp_mnt2, # 1 + temp_mnt3, # 2 + temp_mnt4, # 3 + volt_mnt1, # 4 + volt_mnt2, # 5 + volt_mnt3, # 6 + volt_mnt4, # 7 + capacitance_mnt1, # 8 + free_carrier_mnt1, # 9 + energy_band_mnt1, # 10 + electric_field_mnt, # 11 ] @@ -516,7 +519,10 @@ def temperature_monitor_data(monitors): @pytest.fixture(scope="module") def voltage_monitor_data(monitors): """Creates different voltage monitor data.""" - _, _, _, _, volt_mnt1, volt_mnt2, volt_mnt3, volt_mnt4, _, _, _ = monitors + volt_mnt1 = monitors[4] + volt_mnt2 = monitors[5] + volt_mnt3 = monitors[6] + volt_mnt4 = monitors[7] # SpatialDataArray nx, ny, nz = 9, 6, 5 @@ -642,6 +648,84 @@ def energy_band_monitor_data(monitors): return (eb_data1,) +@pytest.fixture(scope="module") +def electric_field_monitor_data(monitors): + """Creates different electric field monitor data.""" + monitor = monitors[11] + + # TetrahedralGridDataset + tet_grid_points = td.PointDataArray( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + dims=("index", "axis"), + ) + + tet_grid_cells = td.CellDataArray( + [[0, 1, 2, 4], [1, 2, 3, 4]], + dims=("cell_index", "vertex_index"), + ) + + tet_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0, 5.0], + dims=("index",), + name="T", + ) + + tet_grid = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values, + ) + + mnt_data1 = td.SteadyElectricFieldData(monitor=monitor, Ex=tet_grid, Ey=tet_grid, Ez=tet_grid) + + # 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_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, + ) + + mnt_data2 = td.SteadyElectricFieldData(monitor=monitor, Ex=tri_grid, Ey=tri_grid, Ez=tri_grid) + + mnt_data3 = td.SteadyElectricFieldData(monitor=monitor, Ex=None, Ey=None, Ez=None) + + data_v = td.IndexedVoltageDataArray( + [[0.0, 1.5], [1.5, 2], [2.5, 3.0], [3.5, -4.0]], + coords={"index": np.arange(4), "voltage": [-1, 1]}, + name="test", + ) + tri_grid_v = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=data_v, + ) + mnt_data4 = td.SteadyElectricFieldData( + monitor=monitor, Ex=tri_grid_v, Ey=tri_grid_v, Ez=tri_grid_v + ) + + return (mnt_data1, mnt_data2, mnt_data3, mnt_data4) + + @pytest.fixture(scope="module") def simulation_data( heat_simulation, @@ -797,11 +881,27 @@ def test_monitor_crosses_medium(mediums, structures, heat_simulation, conduction def test_heat_charge_mnt_data( - temperature_monitor_data, voltage_monitor_data, capacitance_monitor_data + temperature_monitor_data, voltage_monitor_data, electric_field_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) == 4, "Expected 4 electric field monitor data entries." + + for mnt_data in electric_field_monitor_data: + assert "Ex" in mnt_data.field_components.keys() + assert "Ey" in mnt_data.field_components.keys() + assert "Ez" in mnt_data.field_components.keys() + + symm_data = mnt_data.symmetry_expanded_copy + assert symm_data.Ex == mnt_data.Ex + assert symm_data.Ey == mnt_data.Ey + assert symm_data.Ez == mnt_data.Ez + + names = mnt_data.field_name("abs^2") + assert names == "Ex², Ey², Ez²" + names = mnt_data.field_name() + assert names == "Ex, Ey, Ez" def test_grid_spec_validation(grid_specs): diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 6bc0bd31a..1c312d89f 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -35,6 +35,7 @@ ) from tidy3d.components.tcad.data.types import ( SteadyCapacitanceData, + SteadyElectricFieldData, SteadyEnergyBandData, SteadyFreeCarrierData, SteadyPotentialData, @@ -50,6 +51,7 @@ ) from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, @@ -651,6 +653,8 @@ def set_logging_level(level: str) -> None: "Staircasing", "SteadyCapacitanceData", "SteadyCapacitanceMonitor", + "SteadyElectricFieldData", + "SteadyElectricFieldMonitor", "SteadyEnergyBandData", "SteadyEnergyBandMonitor", "SteadyFreeCarrierData", diff --git a/tidy3d/components/data/unstructured/base.py b/tidy3d/components/data/unstructured/base.py index b6be6dce3..cfc56f638 100644 --- a/tidy3d/components/data/unstructured/base.py +++ b/tidy3d/components/data/unstructured/base.py @@ -566,6 +566,20 @@ def to_vtu(self, fname: str): writer.SetInputData(self._vtk_obj) writer.Write() + @classmethod + @requires_vtk + def _cell_to_point_data( + cls, + vtk_obj, + ): + """Get point data values from a VTK object.""" + + cellDataToPointData = vtk["mod"].vtkCellDataToPointData() + cellDataToPointData.SetInputData(vtk_obj) + cellDataToPointData.Update() + + return cellDataToPointData.GetOutput() + @classmethod @requires_vtk def _get_values_from_vtk( diff --git a/tidy3d/components/tcad/data/monitor_data/charge.py b/tidy3d/components/tcad/data/monitor_data/charge.py index 83898b301..053f57306 100644 --- a/tidy3d/components/tcad/data/monitor_data/charge.py +++ b/tidy3d/components/tcad/data/monitor_data/charge.py @@ -18,6 +18,7 @@ from tidy3d.components.tcad.data.monitor_data.abstract import HeatChargeMonitorData from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, @@ -460,3 +461,85 @@ def symmetry_expanded_copy(self) -> SteadyCapacitanceData: electron_capacitance=new_electron_capacitance, symmetry=(0, 0, 0), ) + + +class SteadyElectricFieldData(HeatChargeMonitorData): + """ + Stores electric field :math:`\\vec{E}` from a charge simulation. + + Notes + ----- + The electric field is computed as the negative gradient of the electric potential :math:`\\vec{E} = -\\nabla \\psi`. + It is given in units of :math:`V/\\mu m` (Volts per micrometer). + """ + + monitor: SteadyElectricFieldMonitor = pd.Field( + ..., + title="Electric field monitor", + description="Electric field data associated with a Charge simulation.", + ) + + Ex: UnstructuredFieldType = pd.Field( + None, + title="x component of the electric field", + description=r"Contains the computed x component of the electric field in :math:`V/\\mu m`.", + discriminator=TYPE_TAG_STR, + ) + + Ey: UnstructuredFieldType = pd.Field( + None, + title="y component of the electric field", + description=r"Contains the computed y component of the electric field in :math:`V/\\mu m`.", + discriminator=TYPE_TAG_STR, + ) + + Ez: UnstructuredFieldType = pd.Field( + None, + title="z component of the electric field", + description=r"Contains the computed z component of the electric field in :math:`V/\\mu m`.", + discriminator=TYPE_TAG_STR, + ) + + @property + def field_components(self) -> dict[str, UnstructuredFieldType]: + """Maps the field components to their associated data.""" + return {"Ex": self.Ex, "Ey": self.Ey, "Ez": self.Ez} + + @pd.root_validator(skip_on_failure=True) + def warn_no_data(cls, values): + """Warn if no data provided.""" + + mnt = values.get("monitor") + Ex = values.get("Ex") + Ey = values.get("Ey") + Ez = values.get("Ez") + + if Ex is None or Ey is None or Ez 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 + + @property + def symmetry_expanded_copy(self) -> SteadyElectricFieldData: + """Return copy of self with symmetry applied.""" + + new_Ex = self._symmetry_expanded_copy(property=self.Ex) + new_Ey = self._symmetry_expanded_copy(property=self.Ey) + new_Ez = self._symmetry_expanded_copy(property=self.Ez) + + return self.updated_copy( + Ex=new_Ex, + Ey=new_Ey, + Ez=new_Ez, + 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 "Ex², Ey², Ez²" + else: + return "Ex, Ey, Ez" diff --git a/tidy3d/components/tcad/data/types.py b/tidy3d/components/tcad/data/types.py index c1865d309..cb2045584 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, + SteadyElectricFieldData, SteadyEnergyBandData, SteadyFreeCarrierData, SteadyPotentialData, @@ -16,6 +17,7 @@ TemperatureData, SteadyPotentialData, SteadyFreeCarrierData, + SteadyElectricFieldData, SteadyEnergyBandData, SteadyCapacitanceData, ] diff --git a/tidy3d/components/tcad/monitors/charge.py b/tidy3d/components/tcad/monitors/charge.py index df479897f..dc5612934 100644 --- a/tidy3d/components/tcad/monitors/charge.py +++ b/tidy3d/components/tcad/monitors/charge.py @@ -80,3 +80,22 @@ class SteadyCapacitanceMonitor(HeatChargeMonitor): title="Unstructured Grid", description="Return data on the original unstructured grid.", ) + + +class SteadyElectricFieldMonitor(HeatChargeMonitor): + """ + Electric field monitor for Charge simulations. + + Example + ------- + >>> import tidy3d as td + >>> electric_field_monitor_z0 = td.SteadyElectricFieldMonitor( + ... center=(0, 0.14, 0), size=(0.6, 0.3, 0), name="electric_field_z0", + ... ) + """ + + unstructured: Literal[True] = pd.Field( + True, + title="Unstructured Grid", + description="Return data on the original unstructured grid.", + ) diff --git a/tidy3d/components/tcad/types.py b/tidy3d/components/tcad/types.py index 002bce16b..dec3f44f6 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, + SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, @@ -34,6 +35,7 @@ SteadyPotentialMonitor, SteadyFreeCarrierMonitor, SteadyEnergyBandMonitor, + SteadyElectricFieldMonitor, SteadyCapacitanceMonitor, ] HeatChargeSourceType = Union[HeatSource, HeatFromElectricSource, UniformHeatSource]