From e695d6e91acc54befdec72f317d067a3558a04ff Mon Sep 17 00:00:00 2001 From: Vasily Zabelin Date: Mon, 24 Mar 2025 21:35:16 -0700 Subject: [PATCH] Add energy bandgap monitors for the CHARGE simulations. --- tests/test_components/test_heat_charge.py | 207 ++++++++++++++++- tidy3d/__init__.py | 4 + .../tcad/data/monitor_data/charge.py | 211 +++++++++++++++++- tidy3d/components/tcad/data/types.py | 2 + tidy3d/components/tcad/monitors/charge.py | 20 ++ tidy3d/components/tcad/types.py | 2 + 6 files changed, 439 insertions(+), 7 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 5a7b5c38ff..2f6fe22e63 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -220,6 +220,8 @@ def monitors(): free_carrier_mnt1 = td.SteadyFreeCarrierMonitor(size=(1.6, 2, 3), name="carrier_test") + energy_band_mnt1 = td.SteadyEnergyBandMonitor(size=(1.6, 2, 3), name="bandgap_test") + return [ temp_mnt1, temp_mnt2, @@ -231,6 +233,7 @@ def monitors(): volt_mnt4, capacitance_mnt1, free_carrier_mnt1, + energy_band_mnt1, ] @@ -478,7 +481,7 @@ 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, volt_mnt2, volt_mnt3, volt_mnt4, _, _, _ = monitors # SpatialDataArray nx, ny, nz = 9, 6, 5 @@ -551,7 +554,7 @@ def voltage_monitor_data(monitors): @pytest.fixture(scope="module") def capacitance_monitor_data(monitors): """Creates different voltage monitor data.""" - _, _, _, _, _, _, _, _, cap_mt1, _ = monitors + cap_mt1 = monitors[8] # SpatialDataArray cap_data1 = td.SteadyCapacitanceData(monitor=cap_mt1) @@ -563,7 +566,7 @@ def capacitance_monitor_data(monitors): @pytest.fixture(scope="module") def free_carrier_monitor_data(monitors): """Creates different voltage monitor data.""" - _, _, _, _, _, _, _, _, _, fc_mnt = monitors + fc_mnt = monitors[9] # SpatialDataArray fc_data1 = td.SteadyFreeCarrierData(monitor=fc_mnt) @@ -582,6 +585,28 @@ def free_carrier_monitor_data(monitors): return (fc_data1,) +@pytest.fixture(scope="module") +def energy_band_monitor_data(monitors): + """Creates different voltage monitor data.""" + eb_mnt = monitors[10] + + # SpatialDataArray + eb_data1 = td.SteadyEnergyBandData(monitor=eb_mnt) + eb_data2 = eb_data1.symmetry_expanded_copy + assert eb_data2 is not None + + field_components = eb_data1.field_components + + eb_fields = eb_data1.field_name("abs^2") + assert eb_fields is not None + eb_fields_default = eb_data1.field_name() + assert eb_fields_default is not None + + assert field_components is not None + + return (eb_data1,) + + @pytest.fixture(scope="module") def simulation_data( heat_simulation, @@ -592,6 +617,7 @@ def simulation_data( voltage_monitor_data, capacitance_monitor_data, free_carrier_monitor_data, + energy_band_monitor_data, ): """Creates 'HeatChargeSimulationData' for both Heat and Conduction simulations.""" heat_sim_data = td.HeatChargeSimulationData( @@ -1325,6 +1351,181 @@ def test_plotting_functions(simulation_data): heat_sim_data.plot_field("test", invalid_param=0) +def test_bandgap_monitor(): + """Test energy bandgap monitor ploting function.""" + # create a triangle grid + 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_single_voltage = td.IndexedVoltageDataArray( + [[0.0], [0], [3], [3]], + coords=dict(index=np.arange(4), voltage=[1]), + name="test", + ) + + tri_grid_values_multi_voltage = td.IndexedVoltageDataArray( + [[0.0, 0.0], [0, 0], [3, -3], [3, -3]], + coords=dict(index=np.arange(4), voltage=[-1, 1]), + name="test", + ) + + tri_grid_single_voltage = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values_single_voltage, + ) + + tri_grid_multi_voltage = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values_multi_voltage, + ) + + # create a tet mesh + 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], + [1.0, 0.0, 1.0], + [0.0, 1.0, 1.0], + [1.0, 1.0, 1.0], + ], + dims=("index", "axis"), + ) + + tet_grid_cells = td.CellDataArray( + [[0, 1, 3, 7], [0, 2, 7, 3], [0, 2, 6, 7], [0, 4, 7, 6], [0, 4, 5, 7], [0, 1, 7, 5]], + dims=("cell_index", "vertex_index"), + ) + + tet_grid_values_single_voltage = td.IndexedVoltageDataArray( + [[0.0], [0.0], [0.0], [0.0], [3.0], [3.0], [3.0], [3.0]], + coords=dict(index=np.arange(8), voltage=[1]), + name="test_tet", + ) + + tet_grid_values_multi_voltage = td.IndexedVoltageDataArray( + [ + [0.0, 0.5], + [0.0, 0.5], + [0.0, 0.5], + [0.0, 0.5], + [3.0, 3.5], + [3.0, 3.5], + [3.0, 3.5], + [3.0, 3.5], + ], + coords=dict(index=np.arange(8), voltage=[-1, 1]), + name="test_tet", + ) + + tet_grid_single_voltage = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values_single_voltage, + ) + + tet_grid_multi_voltage = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values_multi_voltage, + ) + + aux_monitor_2D = td.SteadyEnergyBandMonitor( + center=(0, 0.14, 0), size=(0.6, 0.3, 0), name="bands_2D", unstructured=True + ) + + aux_monitor_3D = td.SteadyEnergyBandMonitor( + center=(0, 0.14, 0.0), size=(0.6, 0.3, 0.5), name="bands_3D", unstructured=True + ) + + tri_single_voltage_data = td.SteadyEnergyBandData( + monitor=aux_monitor_2D, + Ec=tri_grid_single_voltage, + Ev=tri_grid_single_voltage, + Ei=tri_grid_single_voltage, + Efn=tri_grid_single_voltage, + Efp=tri_grid_single_voltage, + ) + + tri_multi_voltage_data = td.SteadyEnergyBandData( + monitor=aux_monitor_2D, + Ec=tri_grid_multi_voltage, + Ev=tri_grid_multi_voltage, + Ei=tri_grid_multi_voltage, + Efn=tri_grid_multi_voltage, + Efp=tri_grid_multi_voltage, + ) + + tet_single_voltage_data = td.SteadyEnergyBandData( + monitor=aux_monitor_3D, + Ec=tet_grid_single_voltage, + Ev=tet_grid_single_voltage, + Ei=tet_grid_single_voltage, + Efn=tet_grid_single_voltage, + Efp=tet_grid_single_voltage, + ) + + tet_multi_voltage_data = td.SteadyEnergyBandData( + monitor=aux_monitor_3D, + Ec=tet_grid_multi_voltage, + Ev=tet_grid_multi_voltage, + Ei=tet_grid_multi_voltage, + Efn=tet_grid_multi_voltage, + Efp=tet_grid_multi_voltage, + ) + + # test check for the voltage value in the list of arguments + + tri_single_voltage_data.plot(x=0.0) + tri_multi_voltage_data.plot(x=0.0, voltage=1.0) + + with pytest.raises(DataError): + tri_multi_voltage_data.plot(x=0.0) + + tet_single_voltage_data.plot(x=0.0, y=0.0) + tet_multi_voltage_data.plot(x=0.0, y=0.0, voltage=1.0) + + with pytest.raises(DataError): + tri_multi_voltage_data.plot(x=0.0, y=0.0) + + # test check for the number of coordinates in the list of arguments + + with pytest.raises(DataError): + tri_single_voltage_data.plot() + + with pytest.raises(DataError): + tri_single_voltage_data.plot(x=0.0, y=0.0) + + with pytest.raises(DataError): + tet_single_voltage_data.plot() + + with pytest.raises(DataError): + tet_single_voltage_data.plot(x=0.0) + + with pytest.raises(DataError): + tet_single_voltage_data.plot(x=0.0, y=0.0, z=0.0) + + # test check for the incorrect cross-section plane + + with pytest.raises(DataError): + tri_single_voltage_data.plot(y=0.0) + + def test_additional_edge_cases(): """Test additional edge cases and error handling.""" # Attempt to create a monitor with zero size diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 223f84a971..9cc9c1734a 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -32,6 +32,7 @@ ) from tidy3d.components.tcad.data.types import ( SteadyCapacitanceData, + SteadyEnergyBandData, SteadyFreeCarrierData, SteadyPotentialData, TemperatureData, @@ -41,6 +42,7 @@ from tidy3d.components.tcad.grid import DistanceUnstructuredGrid, UniformUnstructuredGrid from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, ) @@ -603,6 +605,7 @@ def set_logging_level(level: str) -> None: "HeatChargeSimulation", "SteadyPotentialData", "SteadyFreeCarrierData", + "SteadyEnergyBandData", "SteadyCapacitanceData", "CaugheyThomasMobility", "ConstantMobilityModel", @@ -616,6 +619,7 @@ def set_logging_level(level: str) -> None: "HeatChargeBoundarySpec", "SteadyPotentialMonitor", "SteadyFreeCarrierMonitor", + "SteadyEnergyBandMonitor", "SteadyCapacitanceMonitor", "SpaceTimeModulation", "SpaceModulation", diff --git a/tidy3d/components/tcad/data/monitor_data/charge.py b/tidy3d/components/tcad/data/monitor_data/charge.py index 42b4a21e12..4c1f532089 100644 --- a/tidy3d/components/tcad/data/monitor_data/charge.py +++ b/tidy3d/components/tcad/data/monitor_data/charge.py @@ -17,10 +17,13 @@ from tidy3d.components.tcad.data.monitor_data.abstract import HeatChargeMonitorData from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, ) -from tidy3d.components.types import TYPE_TAG_STR, annotate_type +from tidy3d.components.types import TYPE_TAG_STR, Ax, annotate_type +from tidy3d.components.viz import add_ax_if_none +from tidy3d.exceptions import DataError from tidy3d.log import log FieldDataset = Union[ @@ -73,7 +76,7 @@ def symmetry_expanded_copy(self) -> SteadyPotentialData: return self.updated_copy(potential=new_potential, symmetry=(0, 0, 0)) def field_name(self, val: str) -> str: - """Gets the name of the fields to be plot.""" + """Gets the name of the fields to be plotted.""" if val == "abs^2": return "|V|²" else: @@ -165,13 +168,213 @@ def symmetry_expanded_copy(self) -> SteadyFreeCarrierData: ) def field_name(self, val: str = "") -> str: - """Gets the name of the fields to be plot.""" + """Gets the name of the fields to be plotted.""" if val == "abs^2": return "Electrons², Holes²" else: return "Electrons, Holes" +class SteadyEnergyBandData(HeatChargeMonitorData): + """ + Stores energy bands in charge simulations. + + Notes + ----- + + This data contains the energy bands data: + Ec -> Energy of the bottom of the conduction band, [eV] + Ev -> Energy of the top of the valence band, [eV] + Ei -> Intrinsic Fermi level, [eV] + Efn -> Quasi-Fermi level for electrons, [eV] + Efp -> Quasi-Fermi level for holes, [eV] + as defined in the ``monitor``. + """ + + monitor: SteadyEnergyBandMonitor = pd.Field( + ..., + title="Energy band monitor", + description="Energy bands data associated with a Charge simulation.", + ) + + Ec: UnstructuredFieldType = pd.Field( + None, + title="Conduction band series", + description=r"Contains the computed energy of the bottom of the conduction band $Ec$.", + discriminator=TYPE_TAG_STR, + ) + + Ev: UnstructuredFieldType = pd.Field( + None, + title="Valence band series", + description=r"Contains the computed energy of the top of the valence band $Ec$.", + discriminator=TYPE_TAG_STR, + ) + + Ei: UnstructuredFieldType = pd.Field( + None, + title="Intrinsic Fermi level series", + description=r"Contains the computed intrinsic Fermi level for the material $Ei$.", + discriminator=TYPE_TAG_STR, + ) + + Efn: UnstructuredFieldType = pd.Field( + None, + title="Electron's quasi-Fermi level series", + description=r"Contains the computed quasi-Fermi level for electrons $Efn$.", + discriminator=TYPE_TAG_STR, + ) + + Efp: UnstructuredFieldType = pd.Field( + None, + title="Hole's quasi-Fermi level series", + description=r"Contains the computed quasi-Fermi level for holes $Efp$.", + discriminator=TYPE_TAG_STR, + ) + + @property + def field_components(self) -> Dict[str, DataArray]: + """Maps the field components to their associated data.""" + return dict(Ec=self.Ec, Ev=self.Ev, Ei=self.Ei, Efn=self.Efn, Efp=self.Efp) + + @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") + field_data = {field: values.get(field) for field in ["Ec", "Ev", "Ei", "Efn", "Efp"]} + + for field, data in field_data.items(): + if isinstance(data, TetrahedralGridDataset) or isinstance(data, TriangularGridDataset): + if not isinstance(data.values, IndexedVoltageDataArray): + raise ValueError( + f"In the data associated with monitor {mnt}, the field {field} does not contain " + "data associated to any voltage value." + ) + + return values + + @pd.root_validator(skip_on_failure=True) + def warn_no_data(cls, values): + """Warn if no data provided.""" + + mnt = values.get("monitor") + fields = ["Ec", "Ev", "Ei", "Efn", "Efp"] + for field_name in fields: + field_data = values.get(field_name) + + if field_data 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) -> SteadyEnergyBandData: + """Return copy of self with symmetry applied.""" + + new_Ec = self._symmetry_expanded_copy(property=self.Ec) + new_Ev = self._symmetry_expanded_copy(property=self.Ev) + new_Ei = self._symmetry_expanded_copy(property=self.Ei) + new_Efn = self._symmetry_expanded_copy(property=self.Efn) + new_Efp = self._symmetry_expanded_copy(property=self.Efp) + + return self.updated_copy( + Ec=new_Ec, + Ev=new_Ev, + Ei=new_Ei, + Efn=new_Efn, + Efp=new_Efp, + 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 "|Ec|², |Ev|², |Ei|², |Efn|², |Efp|²" + else: + return "Ec, Ev, Ei, Efn, Efp" + + @add_ax_if_none + def plot(self, ax: Ax = None, **sel_kwargs) -> Ax: + """Plot the 1D cross-section of the energy bandgap diagram. + + Parameters + ---------- + ax : matplotlib.axes._subplots.Axes = None + matplotlib axes to plot on, if not specified, one is created. + sel_kwargs : keyword arguments used to perform ``.sel()`` selection in the monitor data. + These kwargs can select over the spatial dimensions (``x``, ``y``, or ``z``) + and the bias voltage (``voltage``). + For the plotting to work appropriately, the resulting data after selection must contain + only one coordinate with len > 1. + Furthermore, these should be spatial coordinates (``x``, ``y``, or ``z``). + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + selection_data = dict() + + if ("voltage" not in sel_kwargs) and (self.Ec.values.coords.sizes["voltage"] > 1): + raise DataError( + "'voltage' is not selected for the plot with multiple voltage data points." + ) + + selection_data = {coord: sel_kwargs[coord] for coord in "xyz" if coord in sel_kwargs.keys()} + N_coords = len(selection_data.keys()) + + if "voltage" in sel_kwargs: + selection_data["voltage"] = sel_kwargs["voltage"] + + if isinstance(self.Ec, TetrahedralGridDataset): + if N_coords != 2: + raise DataError( + "2 spatial coordinate values have to be defined to plot the 1D cross-section figure for a 3D dataset." + ) + + elif isinstance(self.Ec, TriangularGridDataset): + if N_coords != 1: + raise DataError( + "1 spatial coordinate value has to be defined to plot the 1D cross-section figure for a 2D dataset." + ) + + for index, coord_name in enumerate(["x", "y", "z"]): + if coord_name in selection_data: + axis = index + continue + + if axis == self.Ec.normal_axis: + raise DataError( + f"Triangular grid (normal: {self.Ec.normal_axis}) cannot be sliced by a parallel plane." + ) + + Ec_data = self.Ec + Ev_data = self.Ev + Ei_data = self.Ei + Efn_data = self.Efn + Efp_data = self.Efp + + for coord_name, coord_val in selection_data.items(): + Ec_data = Ec_data.sel(**{coord_name: coord_val}, method="nearest") + Ev_data = Ev_data.sel(**{coord_name: coord_val}, method="nearest") + Ei_data = Ei_data.sel(**{coord_name: coord_val}, method="nearest") + Efn_data = Efn_data.sel(**{coord_name: coord_val}, method="nearest") + Efp_data = Efp_data.sel(**{coord_name: coord_val}, method="nearest") + + Ec_data.plot(ax=ax, label="Ec") + Ev_data.plot(ax=ax, label="Ev") + Ei_data.plot(ax=ax, label="Ei") + Efn_data.plot(ax=ax, label="Efn") + Efp_data.plot(ax=ax, label="Efp") + ax.legend() + + return ax + + class SteadyCapacitanceData(HeatChargeMonitorData): """ Class that stores capacitance data from a Charge simulation. @@ -226,7 +429,7 @@ def warn_no_data(cls, val, values): return val def field_name(self, val: str) -> str: - """Gets the name of the fields to be plot.""" + """Gets the name of the fields to be plotted.""" return "" @property diff --git a/tidy3d/components/tcad/data/types.py b/tidy3d/components/tcad/data/types.py index 85c5c2bf34..c1865d309e 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, + SteadyEnergyBandData, SteadyFreeCarrierData, SteadyPotentialData, ) @@ -15,5 +16,6 @@ TemperatureData, SteadyPotentialData, SteadyFreeCarrierData, + SteadyEnergyBandData, SteadyCapacitanceData, ] diff --git a/tidy3d/components/tcad/monitors/charge.py b/tidy3d/components/tcad/monitors/charge.py index 9cc789bef3..e575e30a86 100644 --- a/tidy3d/components/tcad/monitors/charge.py +++ b/tidy3d/components/tcad/monitors/charge.py @@ -40,6 +40,26 @@ class SteadyFreeCarrierMonitor(HeatChargeMonitor): ) +class SteadyEnergyBandMonitor(HeatChargeMonitor): + """ + Energy bands monitor for Charge simulations. + + Example + ------- + >>> import tidy3d as td + >>> energy_monitor_z0 = td.SteadyEnergyBandMonitor( + ... center=(0, 0.14, 0), size=(0.6, 0.3, 0), name="bands_z0", unstructured=True, + ... ) + """ + + # NOTE: for the time being supporting unstructured + unstructured: Literal[True] = pd.Field( + True, + title="Unstructured Grid", + description="Return data on the original unstructured grid.", + ) + + class SteadyCapacitanceMonitor(HeatChargeMonitor): """ Capacitance monitor associated with a charge simulation. diff --git a/tidy3d/components/tcad/types.py b/tidy3d/components/tcad/types.py index 7861585564..c3520d8a86 100644 --- a/tidy3d/components/tcad/types.py +++ b/tidy3d/components/tcad/types.py @@ -11,6 +11,7 @@ from tidy3d.components.tcad.mobility import CaugheyThomasMobility, ConstantMobilityModel from tidy3d.components.tcad.monitors.charge import ( SteadyCapacitanceMonitor, + SteadyEnergyBandMonitor, SteadyFreeCarrierMonitor, SteadyPotentialMonitor, ) @@ -30,6 +31,7 @@ TemperatureMonitor, SteadyPotentialMonitor, SteadyFreeCarrierMonitor, + SteadyEnergyBandMonitor, SteadyCapacitanceMonitor, ] HeatChargeSourceType = Union[HeatSource, HeatFromElectricSource, UniformHeatSource]