diff --git a/src/ert/run_models/everest_run_model.py b/src/ert/run_models/everest_run_model.py index 859c15bdb54..739cb7a8d6b 100644 --- a/src/ert/run_models/everest_run_model.py +++ b/src/ert/run_models/everest_run_model.py @@ -30,7 +30,7 @@ from ert.storage import open_storage from everest.config import ControlConfig, ControlVariableGuessListConfig, EverestConfig from everest.everest_storage import EverestStorage, OptimalResult -from everest.optimizer.everest2ropt import everest2ropt +from everest.optimizer.everest2ropt import everest2ropt, get_ropt_transforms from everest.simulator.everest_to_ert import everest_to_ert_config from everest.strings import EVEREST @@ -96,7 +96,10 @@ def __init__( ) self._everest_config = everest_config - self._ropt_config = everest2ropt(everest_config) + self._ropt_transforms = get_ropt_transforms(everest_config) + self._ropt_config = everest2ropt( + everest_config, transforms=self._ropt_transforms + ) self._sim_callback = simulation_callback self._opt_callback = optimization_callback @@ -191,7 +194,7 @@ def run_experiment( output_dir=Path(self._everest_config.optimization_output_dir), ) self.ever_storage.init(self._everest_config) - self.ever_storage.observe_optimizer(optimizer) + self.ever_storage.observe_optimizer(optimizer, self._ropt_transforms) # Run the optimization: optimizer_exit_code = optimizer.run().exit_code diff --git a/src/everest/everest_storage.py b/src/everest/everest_storage.py index 6dc4d43c1fb..a323ed092d3 100644 --- a/src/everest/everest_storage.py +++ b/src/everest/everest_storage.py @@ -5,6 +5,7 @@ import os import traceback from dataclasses import dataclass, field +from functools import partial from pathlib import Path from typing import Any, TypedDict, cast @@ -13,6 +14,7 @@ from ropt.enums import EventType from ropt.plan import BasicOptimizer, Event from ropt.results import FunctionResults, GradientResults, convert_to_maximize +from ropt.transforms import Transforms from everest.config import EverestConfig from everest.strings import EVEREST @@ -387,9 +389,12 @@ def read_from_output_dir(self) -> None: exp = _OptimizerOnlyExperiment(self._output_dir) self.data.read_from_experiment(exp) - def observe_optimizer(self, optimizer: BasicOptimizer) -> None: + def observe_optimizer( + self, optimizer: BasicOptimizer, transforms: Transforms + ) -> None: optimizer.add_observer( - EventType.FINISHED_EVALUATION, self._on_batch_evaluation_finished + EventType.FINISHED_EVALUATION, + partial(self._on_batch_evaluation_finished, transforms=transforms), ) optimizer.add_observer( EventType.FINISHED_OPTIMIZER_STEP, self._on_optimization_finished @@ -658,11 +663,14 @@ def _store_gradient_results(self, results: GradientResults) -> _GradientResults: "perturbation_constraints": perturbation_constraints, } - def _on_batch_evaluation_finished(self, event: Event) -> None: + def _on_batch_evaluation_finished( + self, event: Event, transforms: Transforms + ) -> None: logger.debug("Storing batch results dataframes") converted_results = tuple( - convert_to_maximize(result) for result in event.data.get("results", []) + convert_to_maximize(result).transform_back(transforms) + for result in event.data.get("results", []) ) results: list[FunctionResults | GradientResults] = [] diff --git a/src/everest/optimizer/everest2ropt.py b/src/everest/optimizer/everest2ropt.py index 1ca8853c709..8c39c25efbf 100644 --- a/src/everest/optimizer/everest2ropt.py +++ b/src/everest/optimizer/everest2ropt.py @@ -2,8 +2,9 @@ import os from typing import Any -from ropt.config.enopt import EnOptConfig +from ropt.config.enopt import EnOptConfig, EnOptContext from ropt.enums import ConstraintType, PerturbationType, VariableType +from ropt.transforms import Transforms from everest.config import ( EverestConfig, @@ -16,44 +17,20 @@ from everest.config.utils import FlattenedControls from everest.strings import EVEREST +from .transforms import ControlScaler + def _parse_controls(controls: FlattenedControls, ropt_config): control_types = [ None if type_ is None else VariableType[type_.upper()] for type_ in controls.types ] - if all(item is None for item in controls.auto_scales): - offsets = None - scales = None - else: - scales = [ - (ub - lb) / (sr[1] - sr[0]) if au else 1.0 - for au, lb, ub, sr in zip( - controls.auto_scales, - controls.lower_bounds, - controls.upper_bounds, - controls.scaled_ranges, - strict=True, - ) - ] - offsets = [ - lb - sr[0] * sc if au else 0.0 - for au, lb, sc, sr in zip( - controls.auto_scales, - controls.lower_bounds, - scales, - controls.scaled_ranges, - strict=True, - ) - ] indices = [idx for idx, is_enabled in enumerate(controls.enabled) if is_enabled] ropt_config["variables"] = { "types": None if all(item is None for item in control_types) else control_types, "initial_values": controls.initial_guesses, "lower_bounds": controls.lower_bounds, "upper_bounds": controls.upper_bounds, - "offsets": offsets, - "scales": scales, "indices": indices if indices else None, } @@ -367,7 +344,9 @@ def _parse_environment( ropt_config["gradient"]["seed"] = random_seed -def everest2ropt(ever_config: EverestConfig) -> EnOptConfig: +def everest2ropt( + ever_config: EverestConfig, transforms: Transforms | None = None +) -> EnOptConfig: """Generate a ropt configuration from an Everest one NOTE: This method is a work in progress. So far only the some of @@ -402,4 +381,21 @@ def everest2ropt(ever_config: EverestConfig) -> EnOptConfig: ropt_config=ropt_config, ) - return EnOptConfig.model_validate(ropt_config) + return EnOptConfig.model_validate( + ropt_config, + context=None if transforms is None else EnOptContext(transforms=transforms), + ) + + +def get_ropt_transforms(ever_config: EverestConfig) -> Transforms: + controls = FlattenedControls(ever_config.controls) + if any(item is not None for item in controls.auto_scales): + variable_scaler = ControlScaler( + controls.lower_bounds, + controls.upper_bounds, + controls.scaled_ranges, + controls.auto_scales, + ) + else: + variable_scaler = None + return Transforms(variables=variable_scaler) diff --git a/src/everest/optimizer/transforms.py b/src/everest/optimizer/transforms.py new file mode 100644 index 00000000000..117e0771259 --- /dev/null +++ b/src/everest/optimizer/transforms.py @@ -0,0 +1,27 @@ +from collections.abc import Sequence + +import numpy as np +from ropt.transforms import VariableScaler + + +class ControlScaler(VariableScaler): + def __init__( + self, + lower_bounds: Sequence[float], + upper_bounds: Sequence[float], + scaled_ranges: Sequence[tuple[float, float]], + auto_scales: Sequence[bool], + ) -> None: + scales = [ + (ub - lb) / (sr[1] - sr[0]) if au else 1.0 + for au, lb, ub, sr in zip( + auto_scales, lower_bounds, upper_bounds, scaled_ranges, strict=True + ) + ] + offsets = [ + lb - sr[0] * sc if au else 0.0 + for au, lb, sc, sr in zip( + auto_scales, lower_bounds, scales, scaled_ranges, strict=True + ) + ] + super().__init__(np.array(scales), np.array(offsets)) diff --git a/tests/everest/test_ropt_initialization.py b/tests/everest/test_ropt_initialization.py index 30b2f9b3a74..b3e7d559f1d 100644 --- a/tests/everest/test_ropt_initialization.py +++ b/tests/everest/test_ropt_initialization.py @@ -7,7 +7,7 @@ from everest.config import EverestConfig from everest.config_file_loader import yaml_file_to_substituted_config_dict -from everest.optimizer.everest2ropt import everest2ropt +from everest.optimizer.everest2ropt import everest2ropt, get_ropt_transforms from tests.everest.utils import relpath _CONFIG_DIR = relpath("test_data/mocked_test_case") @@ -41,7 +41,7 @@ def test_everest2ropt_controls_auto_scale(): controls = config.controls controls[0].auto_scale = True controls[0].scaled_range = [0.3, 0.7] - ropt_config = everest2ropt(config) + ropt_config = everest2ropt(config, transforms=get_ropt_transforms(config)) assert numpy.allclose(ropt_config.variables.lower_bounds, 0.3) assert numpy.allclose(ropt_config.variables.upper_bounds, 0.7) @@ -51,7 +51,7 @@ def test_everest2ropt_variables_auto_scale(): controls = config.controls controls[0].variables[1].auto_scale = True controls[0].variables[1].scaled_range = [0.3, 0.7] - ropt_config = everest2ropt(config) + ropt_config = everest2ropt(config, transforms=get_ropt_transforms(config)) assert ropt_config.variables.lower_bounds[0] == 0.0 assert ropt_config.variables.upper_bounds[0] == 0.1 assert ropt_config.variables.lower_bounds[1] == 0.3 @@ -115,7 +115,7 @@ def test_everest2ropt_controls_input_constraint_auto_scale(): scaled_coefficients = coefficients * (max_values - min_values) / 0.4 scaled_coefficients[:2, 1] = coefficients[:2, 1] * 2.0 / 0.4 - ropt_config = everest2ropt(config) + ropt_config = everest2ropt(config, transforms=get_ropt_transforms(config)) assert numpy.allclose( ropt_config.linear_constraints.coefficients, scaled_coefficients,