diff --git a/schemas/EMESimulation.json b/schemas/EMESimulation.json index c70989201a..f5f1833935 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -2466,6 +2466,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -2795,6 +2805,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3114,6 +3134,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3473,6 +3503,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3625,6 +3665,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -3755,6 +3805,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4035,6 +4095,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/schemas/HeatChargeSimulation.json b/schemas/HeatChargeSimulation.json index 0ee9a79af0..a47a5c04f7 100644 --- a/schemas/HeatChargeSimulation.json +++ b/schemas/HeatChargeSimulation.json @@ -1377,6 +1377,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -1706,6 +1716,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2025,6 +2045,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2291,6 +2321,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2443,6 +2483,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -2573,6 +2623,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2820,6 +2880,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/schemas/HeatSimulation.json b/schemas/HeatSimulation.json index dda37500e5..c5e04bddd3 100644 --- a/schemas/HeatSimulation.json +++ b/schemas/HeatSimulation.json @@ -1377,6 +1377,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -1706,6 +1716,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2025,6 +2045,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2291,6 +2321,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2443,6 +2483,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -2573,6 +2623,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -2820,6 +2880,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/schemas/ModeSimulation.json b/schemas/ModeSimulation.json index 15f8e42e94..2ea7653555 100644 --- a/schemas/ModeSimulation.json +++ b/schemas/ModeSimulation.json @@ -2561,6 +2561,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -2890,6 +2900,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3209,6 +3229,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3568,6 +3598,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3720,6 +3760,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -3850,6 +3900,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4130,6 +4190,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/schemas/Simulation.json b/schemas/Simulation.json index 97ab08ff52..b3f10c60c7 100644 --- a/schemas/Simulation.json +++ b/schemas/Simulation.json @@ -2942,6 +2942,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -3432,6 +3442,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3751,6 +3771,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4263,6 +4293,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4415,6 +4455,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -4545,6 +4595,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4825,6 +4885,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/schemas/TerminalComponentModeler.json b/schemas/TerminalComponentModeler.json index 317815af3e..057c37f625 100644 --- a/schemas/TerminalComponentModeler.json +++ b/schemas/TerminalComponentModeler.json @@ -3046,6 +3046,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { @@ -3536,6 +3546,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -3855,6 +3875,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4367,6 +4397,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4519,6 +4559,16 @@ } ] }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_dataset": { "allOf": [ { @@ -4649,6 +4699,16 @@ "default": {}, "type": "object" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "eps_inf": { "anyOf": [ { @@ -4929,6 +4989,16 @@ }, "type": "array" }, + "derived_from": { + "anyOf": [ + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + } + ] + }, "frequency_range": { "items": [ { diff --git a/tests/test_components/test_perturbation_medium.py b/tests/test_components/test_perturbation_medium.py index 2eec5b4bad..332bb5fdec 100644 --- a/tests/test_components/test_perturbation_medium.py +++ b/tests/test_components/test_perturbation_medium.py @@ -362,3 +362,56 @@ def test_correct_values(dispersive): assert np.isclose(si_n + pp_large_sampled, si_index_perturb_n) assert np.isclose(si_k + pp_small_sampled, si_index_perturb_k) + + +@pytest.mark.parametrize("unstructured", [False, True]) +def test_from_medium_field(unstructured): + """Test that derived_from field is properly set when calling perturbed_copy.""" + # Setup fields to sample at + coords = {"x": [1, 2], "y": [3, 4], "z": [5, 6]} + temperature = td.SpatialDataArray(300 * np.ones((2, 2, 2)), coords=coords) + electron_density = td.SpatialDataArray(1e18 * np.ones((2, 2, 2)), coords=coords) + hole_density = td.SpatialDataArray(2e18 * np.ones((2, 2, 2)), coords=coords) + + if unstructured: + temperature = cartesian_to_unstructured(temperature, seed=7747) + electron_density = cartesian_to_unstructured(electron_density, seed=7747) + hole_density = cartesian_to_unstructured(hole_density, seed=7747) + + # Test PerturbationMedium + pp_real = td.ParameterPerturbation( + heat=td.LinearHeatPerturbation( + coeff=-0.01, + temperature_ref=300, + temperature_range=(200, 500), + ), + ) + + pmed = td.PerturbationMedium(permittivity=10, permittivity_perturbation=pp_real) + + # Test without any perturbation data (returns self) + cmed_no_perturb = pmed.perturbed_copy() + assert isinstance(cmed_no_perturb, td.PerturbationMedium) + + # Test with perturbation data (returns CustomMedium) + cmed_with_perturb = pmed.perturbed_copy(temperature, electron_density, hole_density) + assert isinstance(cmed_with_perturb, td.CustomMedium) + assert cmed_with_perturb.derived_from is pmed + assert hash(cmed_with_perturb.derived_from) == hash(pmed) + + # Test PerturbationPoleResidue + pmed_pole = td.PerturbationPoleResidue( + eps_inf=10, + poles=[(1j, 3), (2j, 4)], + eps_inf_perturbation=pp_real, + ) + + # Test without any perturbation data (returns self) + cmed_pole_no_perturb = pmed_pole.perturbed_copy() + assert isinstance(cmed_pole_no_perturb, td.PerturbationPoleResidue) + + # Test with perturbation data (returns CustomPoleResidue) + cmed_pole_with_perturb = pmed_pole.perturbed_copy(temperature, electron_density, hole_density) + assert isinstance(cmed_pole_with_perturb, td.CustomPoleResidue) + assert cmed_pole_with_perturb.derived_from is pmed_pole + assert hash(cmed_pole_with_perturb.derived_from) == hash(pmed_pole) diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 0e59c8d6a7..d6a0275de8 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -91,6 +91,7 @@ PermittivityComponent, PoleAndResidue, TensorReal, + annotate_type, ) from .validators import _warn_potential_error, validate_name_str, validate_parameter_perturbation from .viz import VisualizationSpec, add_ax_if_none @@ -875,6 +876,12 @@ class AbstractCustomMedium(AbstractMedium, ABC): "intersection interfaces with other structures.", ) + derived_from: Optional[annotate_type(PerturbationMediumType)] = pd.Field( + None, + title="Parent Medium", + description="If not ``None``, it records the parent medium from which this medium was derived.", + ) + @cached_property @abstractmethod def is_isotropic(self) -> bool: @@ -6748,7 +6755,7 @@ def perturbed_copy( electron_density: CustomSpatialDataType = None, hole_density: CustomSpatialDataType = None, interp_method: InterpMethod = "linear", - ) -> Union[Medium, CustomMedium]: + ) -> Union[PerturbationMedium, CustomMedium]: """Sample perturbations on provided heat and/or charge data and return 'CustomMedium'. Any of temperature, electron_density, and hole_density can be 'None'. If all passed arguments are 'None' then a 'Medium' object is returned. All provided fields must have @@ -6780,10 +6787,14 @@ def perturbed_copy( Returns ------- - Union[Medium, CustomMedium] + Union[PerturbationMedium, CustomMedium] Medium specification after application of heat and/or charge data. """ + # in the absence of perturbation + if all(x is None for x in [temperature, electron_density, hole_density]): + return self + new_dict = self.dict( exclude={ "permittivity_perturbation", @@ -6793,10 +6804,6 @@ def perturbed_copy( } ) - if all(x is None for x in [temperature, electron_density, hole_density]): - new_dict.pop("subpixel") - return Medium.parse_obj(new_dict) - permittivity_field = self.permittivity + ParameterPerturbation._zeros_like( temperature, electron_density, hole_density ) @@ -6836,6 +6843,7 @@ def perturbed_copy( new_dict["permittivity"] = permittivity_field new_dict["conductivity"] = conductivity_field new_dict["interp_method"] = interp_method + new_dict["derived_from"] = self return CustomMedium.parse_obj(new_dict) @@ -6959,7 +6967,7 @@ def perturbed_copy( electron_density: CustomSpatialDataType = None, hole_density: CustomSpatialDataType = None, interp_method: InterpMethod = "linear", - ) -> Union[PoleResidue, CustomPoleResidue]: + ) -> Union[PerturbationPoleResidue, CustomPoleResidue]: """Sample perturbations on provided heat and/or charge data and return 'CustomPoleResidue'. Any of temperature, electron_density, and hole_density can be 'None'. If all passed arguments are 'None' then a 'PoleResidue' object is returned. All provided fields must have @@ -6991,18 +6999,18 @@ def perturbed_copy( Returns ------- - Union[PoleResidue, CustomPoleResidue] + Union[PerturbationPoleResidue, CustomPoleResidue] Medium specification after application of heat and/or charge data. """ + # in the absence of perturbation + if all(x is None for x in [temperature, electron_density, hole_density]): + return self + new_dict = self.dict( exclude={"eps_inf_perturbation", "poles_perturbation", "perturbation_spec", "type"} ) - if all(x is None for x in [temperature, electron_density, hole_density]): - new_dict.pop("subpixel") - return PoleResidue.parse_obj(new_dict) - zeros = ParameterPerturbation._zeros_like(temperature, electron_density, hole_density) eps_inf_field = self.eps_inf + zeros @@ -7050,12 +7058,28 @@ def perturbed_copy( new_dict["eps_inf"] = eps_inf_field new_dict["poles"] = poles_field new_dict["interp_method"] = interp_method + new_dict["derived_from"] = self return CustomPoleResidue.parse_obj(new_dict) # types of mediums that can be used in Simulation and Structures +PerturbationMediumType = Union[PerturbationMedium, PerturbationPoleResidue] + + +# Update forward references for all Custom medium classes that inherit from AbstractCustomMedium +def _get_all_subclasses(cls): + """Recursively get all subclasses of a class.""" + all_subclasses = [] + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(_get_all_subclasses(subclass)) + return all_subclasses + + +for _custom_medium_cls in _get_all_subclasses(AbstractCustomMedium): + _custom_medium_cls.update_forward_refs() MediumType3D = Union[ Medium, diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 1074899775..b82b57d36d 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2216,15 +2216,38 @@ def _modal_plane_frames(self) -> list[Structure]: @cached_property def _finalized(self) -> Simulation: """Return the finalized version of the simulation setup. That is, including automatic frames around mode sources and internal absorbers, and 2d strutures converted into volumetric analogues.""" + if ( + len(self._modal_plane_frames) == 0 + and not self._contains_converted_volumetric_structures + ): + return self + return self.updated_copy( + grid_spec=GridSpec.from_grid(self.grid), + structures=self._finalized_volumetric_structures, + ) + @cached_property + def _finalized_volumetric_structures(self) -> list[Structure]: + """Volumetric structures in the simulation, including automatic frames around mode sources and internal absorbers, and 2d strutures converted into volumetric analogues.""" modal_frames = self._modal_plane_frames + if not self._contains_converted_volumetric_structures: + return list(self.structures) + modal_frames + return list(self.volumetric_structures) + modal_frames - if len(modal_frames) == 0 and not self._contains_converted_volumetric_structures: - return self - - structures = list(self.volumetric_structures) + modal_frames + @cached_property + def _finalized_optical_medium_map(self) -> dict[MediumType, pydantic.NonNegativeInt]: + """Returns dict mapping medium to index in material in finalized simulation. - return self.updated_copy(grid_spec=GridSpec.from_grid(self.grid), structures=structures) + Returns + ------- + Dict[:class:`.AbstractMedium`, int] + Mapping between distinct mediums to index in finalized simulation. + """ + medium_set = { + structure._optical_medium for structure in self._finalized_volumetric_structures + } + medium_set.add(Structure._get_optical_medium(self.medium)) + return {medium: index for index, medium in enumerate(medium_set)} def _validate_finalized(self) -> None: """Validate that after adding pec frames simulation setup is still valid.""" diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 07f19a3e06..6bd11d8e9e 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -26,6 +26,7 @@ from .geometry.base import Box, Geometry from .geometry.utils import GeometryType, validate_no_transformed_polyslabs from .grid.grid import Coords +from .material.multi_physics import MultiPhysicsMedium from .material.types import StructureMediumType from .medium import AbstractCustomMedium, CustomMedium, LossyMetalMedium, Medium, Medium2D from .monitor import FieldMonitor, PermittivityMonitor @@ -257,6 +258,16 @@ def eps_diagonal(self, frequency: float, coords: Coords) -> tuple[complex, compl return self.medium.eps_diagonal_on_grid(frequency=frequency, coords=coords) return self.medium.eps_diagonal(frequency=frequency) + @staticmethod + def _get_optical_medium(medium): + """Get optical medium.""" + return medium.optical if isinstance(medium, MultiPhysicsMedium) else medium + + @property + def _optical_medium(self) -> StructureMediumType: + """Optical medium of the structure.""" + return self._get_optical_medium(self.medium) + @pydantic.validator("medium", always=True) @skip_if_fields_missing(["geometry"]) def _check_2d_geometry(cls, val, values):