diff --git a/src/everest/config/objective_function_config.py b/src/everest/config/objective_function_config.py index 72f75c2551b..6f9f4453c8e 100644 --- a/src/everest/config/objective_function_config.py +++ b/src/everest/config/objective_function_config.py @@ -1,4 +1,8 @@ -from pydantic import BaseModel, Field, PositiveFloat, field_validator +from typing import Self + +from pydantic import BaseModel, Field, PositiveFloat, field_validator, model_validator + +from ert.config.parsing import ConfigWarning class ObjectiveFunctionConfig(BaseModel, extra="forbid"): # type: ignore @@ -29,18 +33,30 @@ class ObjectiveFunctionConfig(BaseModel, extra="forbid"): # type: ignore normalization: float | None = Field( default=None, description=""" -normalization is a multiplication factor defined per objective function. - -The value of each objective function is multiplied by the related normalization value. -When optimizing with respect to multiple objective functions, it is important -that the normalization is set so that all the normalized objectives have the same order -of magnitude. Ultimately, the normalized objectives are used in computing -the weighted sum that Everest tries to optimize. + normalization key is deprecated and has been replaced with scaling """, + ) + scaling: float | None = Field( + default=None, + description=""" + scaling is a multiplication factor defined per objective function. + + The value of each objective function is multiplied by the related scaling value. + When optimizing with respect to multiple objective functions, it is important + that the scaling is set so that all the scaled objectives have the same order + of magnitude. Ultimately, the scaled objectives are used in computing + the weighted sum that Everest tries to optimize. + """, ) auto_normalize: bool | None = Field( default=None, description=""" + auto_normalize key is deprecated has been replaced with auto_scale. + """, + ) + auto_scale: bool | None = Field( + default=None, + description=""" auto_normalize can be set to true to automatically determine the normalization factor from the objective value in batch 0. @@ -61,9 +77,31 @@ class ObjectiveFunctionConfig(BaseModel, extra="forbid"): # type: ignore """, ) - @field_validator("normalization") + @field_validator("scaling") @classmethod - def validate_normalization_is_not_zero(cls, normalization): # pylint: disable=E0213 - if normalization == 0.0: + def validate_scaling_is_not_zero(cls, scaling) -> float | None: + if scaling == 0.0: raise ValueError("Normalization value cannot be zero") - return normalization + return scaling + + @model_validator(mode="after") + def deprecate_normalization(self) -> Self: + if self.normalization is not None: + ConfigWarning.deprecation_warn( + "normalization key is deprecated and has been replaced with scaling" + ) + if self.auto_normalize is not None: + ConfigWarning.deprecation_warn( + "auto_normalize key is deprecated and has been replaced with auto_scale" + ) + return self + + @model_validator(mode="after") + def make_scaling_backwards_compatible(self) -> Self: + if self.scaling is None and self.normalization is not None: + self.scaling = self.normalization + if self.scaling == 0.0: + raise ValueError("Scaling value cannot be zero") + if self.auto_scale is None and self.auto_normalize is not None: + self.auto_scale = self.auto_normalize + return self diff --git a/src/everest/everest_storage.py b/src/everest/everest_storage.py index d762f5491ef..dd67110f3b8 100644 --- a/src/everest/everest_storage.py +++ b/src/everest/everest_storage.py @@ -424,7 +424,7 @@ def init(self, everest_config: EverestConfig) -> None: "weight": pl.Series(weights / sum(weights), dtype=pl.Float64), "normalization": pl.Series( [ - 1.0 if obj.normalization is None else obj.normalization + 1.0 if obj.scaling is None else obj.scaling for obj in everest_config.objective_functions ], dtype=pl.Float64, diff --git a/src/everest/optimizer/everest2ropt.py b/src/everest/optimizer/everest2ropt.py index 20930704170..71b6b6c1e22 100644 --- a/src/everest/optimizer/everest2ropt.py +++ b/src/everest/optimizer/everest2ropt.py @@ -68,8 +68,8 @@ def _parse_objectives(objective_functions: list[ObjectiveFunctionConfig], ropt_c for objective in objective_functions: assert isinstance(objective.name, str) weights.append(objective.weight or 1.0) - scales.append(1.0 / (objective.normalization or 1.0)) - auto_scale.append(objective.auto_normalize or False) + scales.append(1.0 / (objective.scaling or 1.0)) + auto_scale.append(objective.auto_scale or False) # If any objective specifies an objective type, we have to specify # function estimators in ropt to implement these types. This is done by diff --git a/tests/everest/test_config_validation.py b/tests/everest/test_config_validation.py index f70c3650956..82a5f85e9ee 100644 --- a/tests/everest/test_config_validation.py +++ b/tests/everest/test_config_validation.py @@ -13,7 +13,7 @@ from ert.config import ConfigWarning from ert.config.parsing import ConfigValidationError -from everest.config import EverestConfig, ModelConfig +from everest.config import EverestConfig, ModelConfig, ObjectiveFunctionConfig from everest.config.control_variable_config import ControlVariableConfig from everest.config.sampler_config import SamplerConfig from everest.simulator.everest_to_ert import everest_to_ert_config @@ -1068,3 +1068,54 @@ def test_warning_empty_controls_and_objectives(controls, objectives, error_msg): objective_functions=objectives, controls=controls, ) + + +def test_deprecated_objective_function_normalization(): + with pytest.warns( + ConfigWarning, match="normalization key is deprecated .* replaced with scaling" + ): + ObjectiveFunctionConfig(name="test", normalization=10) + + +def test_deprecated_objective_function_auto_normalize(): + with pytest.warns( + ConfigWarning, + match="auto_normalize key is deprecated .* replaced with auto_scale", + ): + ObjectiveFunctionConfig(name="test", auto_normalize=True) + + +@pytest.mark.parametrize( + "normalization, scaling, auto_normalize, auto_scale", + [ + (None, None, None, None), + (0.2, None, None, None), + (0.42, 0.24, None, None), + (None, 0.24, None, None), + (None, None, True, None), + (None, None, True, False), + (None, None, None, False), + (0.42, 0.24, True, False), + ], +) +def test_objective_function_scaling_is_backward_compatible_with_scaling( + normalization, auto_normalize, scaling, auto_scale +): + o = ObjectiveFunctionConfig( + name="test", + normalization=normalization, + auto_normalize=auto_normalize, + scaling=scaling, + auto_scale=auto_scale, + ) + if scaling is None and normalization is not None: + assert o.scaling == o.normalization + else: + assert o.scaling == scaling + assert o.normalization == normalization + + if auto_scale is None and auto_normalize is not None: + assert o.auto_scale == o.auto_normalize + else: + assert o.auto_scale == auto_scale + assert o.auto_normalize == auto_normalize