diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 835d48c474..985e6d5044 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -254,19 +254,22 @@ def monitors(): mesh_mnt = td.VolumeMeshMonitor(size=(1.6, 2, 3), name="mesh_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, - mesh_mnt, + 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 + mesh_mnt, # 11 + electric_field_mnt, # 12 ] @@ -519,7 +522,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 @@ -679,6 +685,76 @@ 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[12] + + # 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.PointDataArray( + [[0.0, 1.0, 0.0], [1.0, 1.0, 1.0], [3.0, 5.0, 1.0], [4.0, 5.0, 3.0], [5.0, 2.0, 1.0]], + dims=( + "index", + "axis", + ), + name="T", + ) + + tet_grid = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values, + ) + + mnt_data1 = td.SteadyElectricFieldData(monitor=monitor, E=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.IndexedFieldVoltageDataArray( + [ + [[1.0, 1.5], [-1.0, 1.1], [5.1, 0.0]], + [[1.0, 1.5], [-1.0, 1.1], [5.1, 0.0]], + [[1.0, 1.5], [-1.0, 1.1], [5.1, 0.0]], + [[1.0, 1.5], [-1.0, 1.1], [5.1, 0.0]], + ], + coords={"index": np.arange(4), "axis": np.arange(3), "voltage": [-1, 1]}, + 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, E=tri_grid) + + mnt_data3 = td.SteadyElectricFieldData(monitor=monitor, E=None) + + return (mnt_data1, mnt_data2, mnt_data3) + + @pytest.fixture(scope="module") def simulation_data( heat_simulation, @@ -853,11 +929,52 @@ 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) == 3, "Expected 3 electric field monitor data entries." + + 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_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, + ) + + with pytest.raises(pd.ValidationError): + _ = mnt_data.updated_copy(E=tri_grid) def test_grid_spec_validation(grid_specs): diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index d04dc44d80..100fe8012f 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -37,6 +37,7 @@ ) from tidy3d.components.tcad.data.types import ( SteadyCapacitanceData, + SteadyElectricFieldData, SteadyEnergyBandData, SteadyFreeCarrierData, SteadyPotentialData, @@ -53,6 +54,7 @@ from tidy3d.components.tcad.mesher import VolumeMesher from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyElectricFieldMonitor, SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, @@ -136,6 +138,7 @@ FluxTimeDataArray, HeatDataArray, IndexedDataArray, + IndexedFieldVoltageDataArray, IndexedTimeDataArray, IndexedVoltageDataArray, ModeAmpsDataArray, @@ -565,6 +568,7 @@ def set_logging_level(level: str) -> None: "HuraySurfaceRoughness", "IndexPerturbation", "IndexedDataArray", + "IndexedFieldVoltageDataArray", "IndexedTimeDataArray", "IndexedVoltageDataArray", "InsulatingBC", @@ -653,6 +657,8 @@ def set_logging_level(level: str) -> None: "Staircasing", "SteadyCapacitanceData", "SteadyCapacitanceMonitor", + "SteadyElectricFieldData", + "SteadyElectricFieldMonitor", "SteadyEnergyBandData", "SteadyEnergyBandMonitor", "SteadyFreeCarrierData", diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index caa053449d..af66543b9a 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -1175,9 +1175,9 @@ class SteadyVoltageDataArray(DataArray): class PointDataArray(DataArray): - """A two-dimensional array that stores coordinates of a collection of points. + """A two-dimensional array that stores coordinates/field components for a collection of points. Dimension ``index`` denotes the index of a point in the collection, and dimension ``axis`` - denotes the point's coordinate along that axis. + denotes the field component (or point coordinate) in that direction. Example ------- @@ -1188,6 +1188,14 @@ class PointDataArray(DataArray): >>> point3 = point_array.sel(index=3) >>> # get x coordinates of all points >>> x_coords = point_array.sel(axis=0) + >>> + >>> field_da = PointDataArray( + ... np.random.random((120, 3)), coords=dict(index=np.arange(120), axis=np.arange(3)), + ... ) + >>> # get field of point number 90 + >>> field_point90 = field_da.sel(index=90) + >>> # get z component of all points + >>> z_field = field_da.sel(axis=2) """ __slots__ = () @@ -1265,6 +1273,20 @@ class IndexedTimeDataArray(DataArray): _dims = ("index", "t") +class IndexedFieldVoltageDataArray(DataArray): + """Stores indexed values of vector fields for different voltages. It is typically used + in conjuction with a ``PointDataArray`` to store point-associated vector data. + Example + ------- + >>> indexed_array = IndexedFieldVoltageDataArray( + ... (1+1j) * np.random.random((4,3,2)), coords=dict(index=np.arange(4), axis=np.arange(3), voltage=[-1, 1]) + ... ) + """ + + __slots__ = () + _dims = ("index", "axis", "voltage") + + class SpatialVoltageDataArray(AbstractSpatialDataArray): """Spatial distribution with voltage mapping. @@ -1319,6 +1341,7 @@ class PerturbationCoefficientDataArray(DataArray): PointDataArray, CellDataArray, IndexedDataArray, + IndexedFieldVoltageDataArray, IndexedVoltageDataArray, SpatialVoltageDataArray, PerturbationCoefficientDataArray, @@ -1326,4 +1349,10 @@ class PerturbationCoefficientDataArray(DataArray): ] DATA_ARRAY_MAP = {data_array.__name__: data_array for data_array in DATA_ARRAY_TYPES} -IndexedDataArrayTypes = Union[IndexedDataArray, IndexedVoltageDataArray, IndexedTimeDataArray] +IndexedDataArrayTypes = Union[ + IndexedDataArray, + IndexedVoltageDataArray, + IndexedTimeDataArray, + IndexedFieldVoltageDataArray, + PointDataArray, +] diff --git a/tidy3d/components/data/unstructured/base.py b/tidy3d/components/data/unstructured/base.py index f77d7aa219..fb51132c11 100644 --- a/tidy3d/components/data/unstructured/base.py +++ b/tidy3d/components/data/unstructured/base.py @@ -620,6 +620,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 83898b3011..8a08fdbe97 100644 --- a/tidy3d/components/tcad/data/monitor_data/charge.py +++ b/tidy3d/components/tcad/data/monitor_data/charge.py @@ -10,7 +10,9 @@ from tidy3d.components.base import skip_if_fields_missing from tidy3d.components.data.data_array import ( DataArray, + IndexedFieldVoltageDataArray, IndexedVoltageDataArray, + PointDataArray, SpatialDataArray, SteadyVoltageDataArray, ) @@ -18,6 +20,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 +463,82 @@ 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.", + ) + + E: UnstructuredFieldType = pd.Field( + None, + title="Electric field", + description=r"Contains the computed 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 {"E": self.E} + + @pd.root_validator(skip_on_failure=True) + def warn_no_data(cls, values): + """Warn if no data provided.""" + + mnt = values.get("monitor") + E = values.get("E") + + if E 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") + E = values.get("E") + + if isinstance(E, TetrahedralGridDataset) or isinstance(E, TriangularGridDataset): + AcceptedTypes = (IndexedFieldVoltageDataArray, PointDataArray) + if not isinstance(E.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) -> SteadyElectricFieldData: + """Return copy of self with symmetry applied.""" + + new_E = self._symmetry_expanded_copy(property=self.E) + + return self.updated_copy( + E=new_E, + 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 "E²" + else: + return "E" diff --git a/tidy3d/components/tcad/data/types.py b/tidy3d/components/tcad/data/types.py index c1865d309e..cb2045584d 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 df479897fc..dc56129346 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 002bce16bf..dec3f44f62 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]