diff --git a/test_local.sh b/test_local.sh index 76f290b1c4..eccad2532e 100755 --- a/test_local.sh +++ b/test_local.sh @@ -16,6 +16,7 @@ pytest -ra tests/test_components/test_medium.py pytest -ra tests/test_components/test_meshgenerate.py pytest -ra tests/test_components/test_mode.py pytest -ra tests/test_components/test_monitor.py +pytest -ra tests/test_components/test_parameter_perturbation.py pytest -ra tests/test_components/test_field_projection.py pytest -ra tests/test_components/test_sidewall.py pytest -ra tests/test_components/test_simulation.py diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index 8bc52dc8ae..c0dd3ec103 100644 --- a/tests/test_components/test_medium.py +++ b/tests/test_components/test_medium.py @@ -446,9 +446,33 @@ def test_perturbation_medium(): pmed = td.PerturbationMedium(permittivity=3, permittivity_perturbation=pp_real) - _ = pmed.perturbed_copy() - _ = pmed.perturbed_copy(temperature, electron_density) - _ = pmed.perturbed_copy(temperature, electron_density, hole_density) + cmed = pmed.perturbed_copy() + # regular medium if no perturbations + assert isinstance(cmed, td.Medium) + + cmed = pmed.perturbed_copy(temperature, electron_density) + cmed = pmed.perturbed_copy(temperature, electron_density, hole_density) + + # correct propagation of parameters + assert cmed.name == pmed.name + assert cmed.frequency_range == pmed.frequency_range + assert cmed.subpixel == pmed.subpixel + assert cmed.allow_gain == pmed.allow_gain + + # permittivity < 1 + with pytest.raises(pydantic.ValidationError): + _ = pmed.perturbed_copy(2 * temperature) + + # conductivity validators + pmed = td.PerturbationMedium(conductivity_perturbation=pp_real, subpixel=False) + cmed = pmed.perturbed_copy(0.9 * temperature) # positive conductivity + assert cmed.subpixel == False + with pytest.raises(pydantic.ValidationError): + _ = pmed.perturbed_copy(1.1 * temperature) # negative conductivity + + # negative conductivity but allow gain + pmed = td.PerturbationMedium(conductivity_perturbation=pp_real, allow_gain=True) + _ = pmed.perturbed_copy(1.1 * temperature) # complex perturbation with pytest.raises(pydantic.ValidationError): @@ -458,11 +482,22 @@ def test_perturbation_medium(): pmed = td.PerturbationPoleResidue( poles=[(1j, 3), (2j, 4)], poles_perturbation=[(None, pp_real), (pp_complex, None)], + subpixel=False, + allow_gain=True, ) - _ = pmed.perturbed_copy() - _ = pmed.perturbed_copy(temperature, None, hole_density) - _ = pmed.perturbed_copy(temperature, electron_density, hole_density) + cmed = pmed.perturbed_copy() + # regular medium if no perturbations + assert isinstance(cmed, td.PoleResidue) + + cmed = pmed.perturbed_copy(temperature, None, hole_density) + cmed = pmed.perturbed_copy(temperature, electron_density, hole_density) + + # correct propagation of parameters + assert cmed.name == pmed.name + assert cmed.frequency_range == pmed.frequency_range + assert cmed.subpixel == pmed.subpixel + assert cmed.allow_gain == pmed.allow_gain # mismatch between base parameter and perturbations with pytest.raises(pydantic.ValidationError): diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index ae55164ec1..83e5209586 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -2934,6 +2934,16 @@ class CustomAnisotropicMediumInternal(CustomAnisotropicMedium): class AbstractPerturbationMedium(ABC, Tidy3dBaseModel): """Abstract class for medium perturbation.""" + subpixel: bool = pd.Field( + True, + title="Subpixel averaging", + description="This value will be transferred to the resulting custom medium. That is, " + "if ``True``, the subpixel averaging will be applied to the custom medium provided " + "the corresponding ``Simulation``'s field ``subpixel`` is set to ``True`` as well. " + "If the resulting medium is not a custom medium (no perturbations), this field does not " + "have an effect.", + ) + @abstractmethod def perturbed_copy( self, @@ -2969,16 +2979,16 @@ class PerturbationMedium(Medium, AbstractPerturbationMedium): ------- >>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation >>> dielectric = PerturbationMedium( - >>> permittivity=4.0, - >>> permittivity_perturbation=ParameterPerturbation( - >>> heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001), - >>> ), - >>> name='my_medium' - >>> ) + ... permittivity=4.0, + ... permittivity_perturbation=ParameterPerturbation( + ... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001), + ... ), + ... name='my_medium', + ... ) """ permittivity_perturbation: Optional[ParameterPerturbation] = pd.Field( - ..., + None, title="Permittivity Perturbation", description="List of heat and/or charge perturbations to permittivity.", units=PERMITTIVITY, @@ -3033,12 +3043,13 @@ def perturbed_copy( Medium specification after application of heat and/or charge data. """ + new_dict = self.dict( + exclude={"permittivity_perturbation", "conductivity_perturbation", "type"} + ) + if all(x is None for x in [temperature, electron_density, hole_density]): - return Medium( - permittivity=self.permittivity, - conductivity=self.conductivity, - name=self.name, - ) + new_dict.pop("subpixel") + return Medium.parse_obj(new_dict) # pylint:disable=protected-access permittivity_field = self.permittivity + ParameterPerturbation._zeros_like( @@ -3056,12 +3067,10 @@ def perturbed_copy( temperature, electron_density, hole_density ) - return CustomMedium( - permittivity=permittivity_field, - conductivity=conductivity_field, - name=self.name, - subpixel=True, - ) + new_dict["permittivity"] = permittivity_field + new_dict["conductivity"] = conductivity_field + + return CustomMedium.parse_obj(new_dict) class PerturbationPoleResidue(PoleResidue, AbstractPerturbationMedium): @@ -3080,13 +3089,13 @@ class PerturbationPoleResidue(PoleResidue, AbstractPerturbationMedium): ------- >>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation >>> c0_perturbation = ParameterPerturbation( - >>> heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001), - >>> ) - >>> pole_res = PoleResidue( - >>> eps_inf=2.0, - >>> poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))], - >>> poles_perturbation=[(None, c0_perturbation), (None, None)], - >>> ) + ... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001), + ... ) + >>> pole_res = PerturbationPoleResidue( + ... eps_inf=2.0, + ... poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))], + ... poles_perturbation=[(None, c0_perturbation), (None, None)], + ... ) """ eps_inf_perturbation: Optional[ParameterPerturbation] = pd.Field( @@ -3146,12 +3155,11 @@ def perturbed_copy( Medium specification after application of heat and/or charge data. """ + new_dict = self.dict(exclude={"eps_inf_perturbation", "poles_perturbation", "type"}) + if all(x is None for x in [temperature, electron_density, hole_density]): - return PoleResidue( - eps_inf=self.eps_inf, - poles=self.poles, - name=self.name, - ) + new_dict.pop("subpixel") + return PoleResidue.parse_obj(new_dict) # pylint:disable=protected-access zeros = ParameterPerturbation._zeros_like(temperature, electron_density, hole_density) @@ -3172,15 +3180,15 @@ def perturbed_copy( if c_perturb is not None: c_field += c_perturb.apply_data(temperature, electron_density, hole_density) - return CustomPoleResidue( - eps_inf=eps_inf_field, - poles=poles_field, - name=self.name, - ) + new_dict["eps_inf"] = eps_inf_field + new_dict["poles"] = poles_field + + return CustomPoleResidue.parse_obj(new_dict) # types of mediums that can be used in Simulation and Structures + MediumType3D = Union[ Medium, AnisotropicMedium, diff --git a/tidy3d/components/parameter_perturbation.py b/tidy3d/components/parameter_perturbation.py index 6fc67ec356..724045defe 100644 --- a/tidy3d/components/parameter_perturbation.py +++ b/tidy3d/components/parameter_perturbation.py @@ -198,10 +198,10 @@ class LinearHeatPerturbation(HeatPerturbation): Example ------- >>> heat_perturb = LinearHeatPerturbation( - >>> temperature_ref=300, - >>> coeff=0.0001, - >>> temperature_range=[200, 500], - >>> ) + ... temperature_ref=300, + ... coeff=0.0001, + ... temperature_range=[200, 500], + ... ) """ temperature_ref: pd.NonNegativeFloat = pd.Field( @@ -269,8 +269,8 @@ class CustomHeatPerturbation(HeatPerturbation): >>> from tidy3d import HeatDataArray >>> perturbation_data = HeatDataArray([0.001, 0.002, 0.004], coords=dict(T=[250, 300, 350])) >>> heat_perturb = CustomHeatPerturbation( - >>> perturbation_values=perturbation_data - >>> ) + ... perturbation_values=perturbation_data + ... ) """ perturbation_values: HeatDataArray = pd.Field( @@ -558,13 +558,13 @@ class LinearChargePerturbation(ChargePerturbation): Example ------- >>> charge_perturb = LinearChargePerturbation( - >>> electron_ref=0, - >>> electron_coeff=0.0001, - >>> electron_range=[0, 1e19], - >>> hole_ref=0, - >>> hole_coeff=0.0002, - >>> hole_range=[0, 2e19], - >>> ) + ... electron_ref=0, + ... electron_coeff=0.0001, + ... electron_range=[0, 1e19], + ... hole_ref=0, + ... hole_coeff=0.0002, + ... hole_range=[0, 2e19], + ... ) """ electron_ref: pd.NonNegativeFloat = pd.Field( @@ -676,12 +676,12 @@ class CustomChargePerturbation(ChargePerturbation): ------- >>> from tidy3d import ChargeDataArray >>> perturbation_data = ChargeDataArray( - >>> [[0.001, 0.002, 0.004], [0.003, 0.002, 0.001], - >>> coords=dict(n=[1e16, 1e17, 1e18], p=[2e15, 2e19]), - >>> ) + ... [[0.001, 0.002, 0.004], [0.003, 0.002, 0.001]], + ... coords=dict(n=[2e15, 2e19], p=[1e16, 1e17, 1e18]), + ... ) >>> charge_perturb = CustomChargePerturbation( - >>> perturbation_values=perturbation_data - >>> ) + ... perturbation_values=perturbation_data, + ... ) """ perturbation_values: ChargeDataArray = pd.Field( @@ -812,16 +812,16 @@ class ParameterPerturbation(Tidy3dBaseModel): >>> >>> perturbation_data = HeatDataArray([0.001, 0.002, 0.004], coords=dict(T=[250, 300, 350])) >>> heat_perturb = CustomHeatPerturbation( - >>> perturbation_values=perturbation_data - >>> ) + ... perturbation_values=perturbation_data + ... ) >>> charge_perturb = LinearChargePerturbation( - >>> electron_ref=0, - >>> electron_coeff=0.0001, - >>> electron_range=[0, 1e19], - >>> hole_ref=0, - >>> hole_coeff=0.0002, - >>> hole_range=[0, 2e19], - >>> ) + ... electron_ref=0, + ... electron_coeff=0.0001, + ... electron_range=[0, 1e19], + ... hole_ref=0, + ... hole_coeff=0.0002, + ... hole_range=[0, 2e19], + ... ) >>> param_perturb = ParameterPerturbation(heat=heat_perturb, charge=charge_perturb) """ diff --git a/tox.ini b/tox.ini index 283d1d6829..26e0738eff 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,7 @@ commands = pytest -rA tests/test_components/test_meshgenerate.py pytest -rA tests/test_components/test_mode.py pytest -rA tests/test_components/test_monitor.py + pytest -rA tests/test_components/test_parameter_perturbation.py pytest -rA tests/test_components/test_field_projection.py pytest -rA tests/test_components/test_sidewall.py pytest -rA tests/test_components/test_simulation.py