From 1dfa1799730c22572d5932ec4603e2663f72df00 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Wed, 16 Apr 2025 15:45:02 +0200 Subject: [PATCH 1/3] Unsteady heat support --- tidy3d/__init__.py | 5 ++ tidy3d/components/data/data_array.py | 19 ++++- tidy3d/components/tcad/analysis/__init__.py | 0 .../tcad/analysis/heat_simulation_type.py | 71 +++++++++++++++++++ .../components/tcad/data/monitor_data/heat.py | 17 ----- .../components/tcad/simulation/heat_charge.py | 38 +++++++++- 6 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 tidy3d/components/tcad/analysis/__init__.py create mode 100644 tidy3d/components/tcad/analysis/heat_simulation_type.py diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 9cc9c1734a..ee602281a5 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -21,6 +21,7 @@ ) from tidy3d.components.spice.sources.dc import DCCurrentSource, DCVoltageSource from tidy3d.components.spice.sources.types import VoltageSourceType +from tidy3d.components.tcad.analysis.heat_simulation_type import UnsteadyHeatAnalysis, UnsteadySpec from tidy3d.components.tcad.boundary.specification import ( HeatBoundarySpec, HeatChargeBoundarySpec, @@ -126,6 +127,7 @@ FluxTimeDataArray, HeatDataArray, IndexedDataArray, + IndexedTimeDataArray, IndexedVoltageDataArray, ModeAmpsDataArray, ModeIndexDataArray, @@ -598,6 +600,8 @@ def set_logging_level(level: str) -> None: "UniformHeatSource", "HeatSource", "HeatFromElectricSource", + "UnsteadyHeatAnalysis", + "UnsteadySpec", "UniformUnstructuredGrid", "DistanceUnstructuredGrid", "TemperatureData", @@ -628,6 +632,7 @@ def set_logging_level(level: str) -> None: "PointDataArray", "CellDataArray", "IndexedDataArray", + "IndexedTimeDataArray", "IndexedVoltageDataArray", "SteadyVoltageDataArray", "TriangularGridDataset", diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index 9cabd11796..5afdabadc7 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -1236,6 +1236,22 @@ class IndexedVoltageDataArray(DataArray): _dims = ("index", "voltage") +class IndexedTimeDataArray(DataArray): + """Stores a two-dimensional array with coordinates ``index`` and ``t``, where + ``index`` is usually associated with ``PointDataArray`` and ``t`` indicates at what + simulated time the data was obtained. + + Example + ------- + >>> indexed_array = IndexedTimeDataArray( + ... (1+1j) * np.random.random((3,2)), coords=dict(index=np.arange(3), t=[0, 1]) + ... ) + """ + + __slots__ = () + _dims = ("index", "t") + + class SpatialVoltageDataArray(AbstractSpatialDataArray): """Spatial distribution with voltage mapping. @@ -1286,7 +1302,8 @@ class SpatialVoltageDataArray(AbstractSpatialDataArray): CellDataArray, IndexedDataArray, IndexedVoltageDataArray, + IndexedTimeDataArray, ] DATA_ARRAY_MAP = {data_array.__name__: data_array for data_array in DATA_ARRAY_TYPES} -IndexedDataArrayTypes = Union[IndexedDataArray, IndexedVoltageDataArray] +IndexedDataArrayTypes = Union[IndexedDataArray, IndexedVoltageDataArray, IndexedTimeDataArray] diff --git a/tidy3d/components/tcad/analysis/__init__.py b/tidy3d/components/tcad/analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tidy3d/components/tcad/analysis/heat_simulation_type.py b/tidy3d/components/tcad/analysis/heat_simulation_type.py new file mode 100644 index 0000000000..9d11047c43 --- /dev/null +++ b/tidy3d/components/tcad/analysis/heat_simulation_type.py @@ -0,0 +1,71 @@ +"""Dealing with time specifications for DeviceSimulation""" + +import pydantic.v1 as pd + +from ....constants import KELVIN, SECOND +from ...base import Tidy3dBaseModel + + +class UnsteadySpec(Tidy3dBaseModel): + """Defines an unsteady specification + + Example + -------- + >>> import tidy3d as td + >>> time_spec = td.UnsteadySpec( + ... time_step=0.01, + ... total_time_steps=200, + ... output_fr=50, + ... ) + """ + + time_step: pd.PositiveFloat = pd.Field( + ..., + title="Time-step", + description="Time step taken for each iteration of the time integration loop.", + units=SECOND, + ) + + total_time_steps: pd.PositiveInt = pd.Field( + ..., + title="Total time steps", + description="Specifies the total number of time steps run during the simulation.", + ) + + output_fr: pd.PositiveInt = pd.Field( + 1, + title="Output frequency", + description="Determines how often output files will be written. I.e., an output " + "file will be written every 'output_fr' time steps.", + ) + + +class UnsteadyHeatAnalysis(Tidy3dBaseModel): + """ + Configures relevant unsteady-state heat simulation parameters. + + Example + ------- + >>> import tidy3d as td + >>> time_spec = td.UnsteadyHeatAnalysis( + ... initial_temperature=300, + ... unsteady_spec=td.UnsteadySpec( + ... time_step=0.01, + ... total_time_steps=200, + ... output_fr=50, + ... ), + ... ) + """ + + initial_temperature: pd.PositiveFloat = pd.Field( + ..., + title="Initial temperature.", + description="Initial value for the temperature field.", + units=KELVIN, + ) + + unsteady_spec: UnsteadySpec = pd.Field( + ..., + title="Unsteady specification", + description="Time step and total time steps for the unsteady simulation.", + ) diff --git a/tidy3d/components/tcad/data/monitor_data/heat.py b/tidy3d/components/tcad/data/monitor_data/heat.py index eac51a52b0..31902c9d66 100644 --- a/tidy3d/components/tcad/data/monitor_data/heat.py +++ b/tidy3d/components/tcad/data/monitor_data/heat.py @@ -9,7 +9,6 @@ from tidy3d.components.base import skip_if_fields_missing from tidy3d.components.data.data_array import ( DataArray, - IndexedDataArray, SpatialDataArray, ) from tidy3d.components.data.utils import TetrahedralGridDataset, TriangularGridDataset @@ -75,22 +74,6 @@ def warn_no_data(cls, val, values): return val - @pd.validator("temperature", always=True) - @skip_if_fields_missing(["monitor"]) - def check_correct_data_type(cls, val, values): - """Issue error if incorrect data type is used""" - - mnt = values.get("monitor") - - if isinstance(val, TetrahedralGridDataset) or isinstance(val, TriangularGridDataset): - if not isinstance(val.values, IndexedDataArray): - raise ValueError( - f"Monitor {mnt} of type 'TemperatureMonitor' cannot be associated with data arrays " - "of type 'IndexVoltageDataArray'." - ) - - return val - def field_name(self, val: str = "") -> str: """Gets the name of the fields to be plot.""" if val == "abs^2": diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 60f57ffa1b..99e2c7d087 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -85,6 +85,8 @@ from tidy3d.exceptions import SetupError from tidy3d.log import log +from ..analysis.heat_simulation_type import UnsteadyHeatAnalysis + HEAT_CHARGE_BACK_STRUCTURE_STR = "<<>>" HeatBCTypes = (TemperatureBC, HeatFluxBC, ConvectionBC) @@ -92,7 +94,7 @@ ChargeSourceTypes = () ElectricBCTypes = (VoltageBC, CurrentBC, InsulatingBC) -AnalysisSpecType = ElectricalAnalysisType +AnalysisSpecType = Union[ElectricalAnalysisType, UnsteadyHeatAnalysis] class TCADAnalysisTypes(str, Enum): @@ -834,6 +836,30 @@ def estimate_charge_mesh_size(cls, values): ) return values + @pd.root_validator(skip_on_failure=True) + def check_temperature_monitor_in_unsteady_cases(cls, values): + """Make sure that the temperature monitor is unstructured in unsteady cases.""" + + analysis_type = values.get("analysis_spec") + if isinstance(analysis_type, UnsteadyHeatAnalysis): + monitors = values.get("monitors") + for mnt in monitors: + if isinstance(mnt, TemperatureMonitor): + if not mnt.unstructured: + raise SetupError( + f"Unsteady simulations require the temperature monitor '{mnt.name}' to be unstructured." + ) + # additionaly check that the SolidSpec has capacitance defined + # NOTE: not sure this is needed. Is capacitance a required field in SolidMedium? + structures = values.get("structures") + for structure in structures: + if isinstance(structure.medium.heat, SolidMedium): + if structure.medium.heat_spec.capacity is None: + raise SetupError( + f"Unsteady simulations require the medium '{structure.medium.name}' to have a capacitance defined." + ) + return values + @equal_aspect @add_ax_if_none def plot_property( @@ -1629,8 +1655,14 @@ def _get_simulation_types(self) -> list[TCADAnalysisTypes]: # NOTE: for the time being, if a simulation has SemiconductorMedium # then we consider it of being a 'TCADAnalysisTypes.CHARGE' - if self._check_if_semiconductor_present(self.structures): - return [TCADAnalysisTypes.CHARGE] + if isinstance(self.analysis_spec, ElectricalAnalysisType): + if self._check_if_semiconductor_present(self.structures): + return [TCADAnalysisTypes.CHARGE] + + # check if unsteady heat + # NOTE: this won't work + if isinstance(self.analysis_spec, UnsteadyHeatAnalysis): + return [TCADAnalysisTypes.HEAT] heat_source_present = any(isinstance(s, HeatSourceTypes) for s in self.sources) From 893f6fe47db4a43de4795123148452b8ae8ca5fb Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Thu, 17 Apr 2025 17:35:08 +0200 Subject: [PATCH 2/3] Added tests --- tests/test_components/test_heat_charge.py | 58 +++++++++++++++++++++++ tests/test_data/test_datasets.py | 14 +++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index 2f6fe22e63..ef80c768b2 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -1559,3 +1559,61 @@ def test_fossum(): """Check that fossum model can be defined.""" _ = td.FossumCarrierLifetime(tau_300=3.3e-6, alpha_T=-0.5, N0=7.1e15, A=1, B=0, C=1, alpha=1) + + +def test_unsteady_parameters(): + """Test that unsteady parameters are set correctly.""" + + _ = td.UnsteadyHeatAnalysis( + initial_temperature=300, + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + ) + + # test non-positive initial temperature raises error + with pytest.raises(pd.ValidationError): + _ = td.UnsteadyHeatAnalysis( + initial_temperature=0, + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + ) + + # test negative time step raises error + with pytest.raises(pd.ValidationError): + _ = td.UnsteadyHeatAnalysis( + initial_temperature=10, + unsteady_spec=td.UnsteadySpec(time_step=-0.1, total_time_steps=1, output_fr=2), + ) + + # test negative total time steps raises error + with pytest.raises(pd.ValidationError): + _ = td.UnsteadyHeatAnalysis( + initial_temperature=10, + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=-1, output_fr=2), + ) + # test negative output frequency raises error + with pytest.raises(pd.ValidationError): + _ = td.UnsteadyHeatAnalysis( + initial_temperature=10, + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=-2), + ) + + +def test_unsteady_heat_analysis(heat_simulation): + """Test that the validators for unsteady heat analysis are working.""" + + unsteady_analysis_spec = td.UnsteadyHeatAnalysis( + initial_temperature=300, + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + ) + + temp_mnt = td.TemperatureMonitor( + center=(0, 0, 0), size=(td.inf, td.inf, td.inf), name="temperature", unstructured=True + ) + + # this should work since the monitor is unstructured + unsteady_sim = heat_simulation.updated_copy( + analysis_spec=unsteady_analysis_spec, monitors=[temp_mnt] + ) + + with pytest.raises(pd.ValidationError): + temp_mnt = temp_mnt.updated_copy(unstructured=False) + _ = unsteady_sim.updated_copy(monitors=[temp_mnt]) diff --git a/tests/test_data/test_datasets.py b/tests/test_data/test_datasets.py index e6797ac9b7..527b5f599e 100644 --- a/tests/test_data/test_datasets.py +++ b/tests/test_data/test_datasets.py @@ -10,7 +10,7 @@ np.random.seed(4) -@pytest.mark.parametrize("dataset_type_ind", [0, 1]) +@pytest.mark.parametrize("dataset_type_ind", [0, 1, 2]) @pytest.mark.parametrize("ds_name", ["test123", None]) def test_triangular_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): import tidy3d as td @@ -26,6 +26,11 @@ def test_triangular_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): values_type = td.IndexedVoltageDataArray extra_dims = {"voltage": [0, 1, 2]} + if dataset_type_ind == 2: + dataset_type = td.TriangularGridDataset + values_type = td.IndexedTimeDataArray + extra_dims = {"t": [0, 1, 2]} + # basic create tri_grid_points = td.PointDataArray( [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], @@ -322,7 +327,7 @@ def operation(arr): assert result.name == ds_name -@pytest.mark.parametrize("dataset_type_ind", [0, 1]) +@pytest.mark.parametrize("dataset_type_ind", [0, 1, 2]) @pytest.mark.parametrize("ds_name", ["test123", None]) def test_tetrahedral_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): import tidy3d as td @@ -338,6 +343,11 @@ def test_tetrahedral_dataset(tmp_path, ds_name, dataset_type_ind, no_vtk=False): values_type = td.IndexedVoltageDataArray extra_dims = {"voltage": [0, 1, 2]} + if dataset_type_ind == 2: + dataset_type = td.TetrahedralGridDataset + values_type = td.IndexedTimeDataArray + extra_dims = {"t": [0, 1, 2]} + # basic create tet_grid_points = td.PointDataArray( [ From c27caa6bf310b5de7370b04182104c3777ccb533 Mon Sep 17 00:00:00 2001 From: Marc Bolinches Date: Tue, 29 Apr 2025 11:32:38 +0200 Subject: [PATCH 3/3] output fr change --- tests/test_components/test_heat_charge.py | 26 ++++++++++--------- .../tcad/analysis/heat_simulation_type.py | 7 ----- tidy3d/components/tcad/monitors/heat.py | 11 ++++++++ .../components/tcad/simulation/heat_charge.py | 1 - 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/test_components/test_heat_charge.py b/tests/test_components/test_heat_charge.py index ef80c768b2..0c880e9959 100644 --- a/tests/test_components/test_heat_charge.py +++ b/tests/test_components/test_heat_charge.py @@ -1566,34 +1566,28 @@ def test_unsteady_parameters(): _ = td.UnsteadyHeatAnalysis( initial_temperature=300, - unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1), ) # test non-positive initial temperature raises error with pytest.raises(pd.ValidationError): _ = td.UnsteadyHeatAnalysis( initial_temperature=0, - unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1), ) # test negative time step raises error with pytest.raises(pd.ValidationError): _ = td.UnsteadyHeatAnalysis( initial_temperature=10, - unsteady_spec=td.UnsteadySpec(time_step=-0.1, total_time_steps=1, output_fr=2), + unsteady_spec=td.UnsteadySpec(time_step=-0.1, total_time_steps=1), ) # test negative total time steps raises error with pytest.raises(pd.ValidationError): _ = td.UnsteadyHeatAnalysis( initial_temperature=10, - unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=-1, output_fr=2), - ) - # test negative output frequency raises error - with pytest.raises(pd.ValidationError): - _ = td.UnsteadyHeatAnalysis( - initial_temperature=10, - unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=-2), + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=-1), ) @@ -1602,11 +1596,15 @@ def test_unsteady_heat_analysis(heat_simulation): unsteady_analysis_spec = td.UnsteadyHeatAnalysis( initial_temperature=300, - unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1, output_fr=2), + unsteady_spec=td.UnsteadySpec(time_step=0.1, total_time_steps=1), ) temp_mnt = td.TemperatureMonitor( - center=(0, 0, 0), size=(td.inf, td.inf, td.inf), name="temperature", unstructured=True + center=(0, 0, 0), + size=(td.inf, td.inf, td.inf), + name="temperature", + unstructured=True, + interval=2, ) # this should work since the monitor is unstructured @@ -1617,3 +1615,7 @@ def test_unsteady_heat_analysis(heat_simulation): with pytest.raises(pd.ValidationError): temp_mnt = temp_mnt.updated_copy(unstructured=False) _ = unsteady_sim.updated_copy(monitors=[temp_mnt]) + + with pytest.raises(pd.ValidationError): + temp_mnt = temp_mnt.updated_copy(unstructured=True, interval=0) + _ = unsteady_sim.updated_copy(monitors=[temp_mnt]) diff --git a/tidy3d/components/tcad/analysis/heat_simulation_type.py b/tidy3d/components/tcad/analysis/heat_simulation_type.py index 9d11047c43..b9f9dea905 100644 --- a/tidy3d/components/tcad/analysis/heat_simulation_type.py +++ b/tidy3d/components/tcad/analysis/heat_simulation_type.py @@ -32,13 +32,6 @@ class UnsteadySpec(Tidy3dBaseModel): description="Specifies the total number of time steps run during the simulation.", ) - output_fr: pd.PositiveInt = pd.Field( - 1, - title="Output frequency", - description="Determines how often output files will be written. I.e., an output " - "file will be written every 'output_fr' time steps.", - ) - class UnsteadyHeatAnalysis(Tidy3dBaseModel): """ diff --git a/tidy3d/components/tcad/monitors/heat.py b/tidy3d/components/tcad/monitors/heat.py index bacb84ec2a..88d8afc666 100644 --- a/tidy3d/components/tcad/monitors/heat.py +++ b/tidy3d/components/tcad/monitors/heat.py @@ -1,7 +1,18 @@ """Objects that define how data is recorded from simulation.""" +import pydantic.v1 as pd + from tidy3d.components.tcad.monitors.abstract import HeatChargeMonitor class TemperatureMonitor(HeatChargeMonitor): """Temperature monitor.""" + + interval: pd.PositiveInt = pd.Field( + 1, + title="Interval", + description="Sampling rate of the monitor: number of time steps between each measurement. " + "Set ``interval`` to 1 for the highest possible resolution in time. " + "Higher integer values down-sample the data by measuring every ``interval`` time steps. " + "This can be useful for reducing data storage as needed by the application.", + ) diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 99e2c7d087..b36f145a0f 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1660,7 +1660,6 @@ def _get_simulation_types(self) -> list[TCADAnalysisTypes]: return [TCADAnalysisTypes.CHARGE] # check if unsteady heat - # NOTE: this won't work if isinstance(self.analysis_spec, UnsteadyHeatAnalysis): return [TCADAnalysisTypes.HEAT]