Skip to content

Commit 6e7525d

Browse files
committed
Use a scaler object for output constraint scaling
1 parent 7453c92 commit 6e7525d

File tree

6 files changed

+145
-15
lines changed

6 files changed

+145
-15
lines changed

src/ert/run_models/everest_run_model.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from everest.everest_storage import EverestStorage, OptimalResult
5050
from everest.optimizer.everest2ropt import everest2ropt
5151
from everest.optimizer.opt_model_transforms import (
52+
ConstraintScaler,
5253
ObjectiveScaler,
5354
get_optimization_domain_transforms,
5455
)
@@ -322,20 +323,39 @@ def _init_transforms(self, variables: NDArray[np.float64]) -> OptModelTransforms
322323
transforms = get_optimization_domain_transforms(
323324
self._everest_config.controls,
324325
self._everest_config.objective_functions,
326+
self._everest_config.output_constraints,
325327
realization_weights,
326328
)
327329
# If required, initialize auto-scaling:
328330
assert isinstance(transforms.objectives, ObjectiveScaler)
329-
if transforms.objectives.has_auto_scale:
330-
objectives, _, _ = self._run_forward_model(
331+
assert transforms.nonlinear_constraints is None or isinstance(
332+
transforms.nonlinear_constraints, ConstraintScaler
333+
)
334+
if transforms.objectives.has_auto_scale or (
335+
transforms.nonlinear_constraints
336+
and transforms.nonlinear_constraints.has_auto_scale
337+
):
338+
# Run the forward model once to find the objective/constraint values
339+
# to compute the scales. This will add an ensemble/batch in the
340+
# storage that is not part of the optimization run. However, the
341+
# results may be used in the optimization via the caching mechanism.
342+
objectives, constraints, _ = self._run_forward_model(
331343
np.repeat(np.expand_dims(variables, axis=0), nreal, axis=0),
332344
realizations,
333345
)
334-
transforms.objectives.calculate_auto_scales(objectives)
346+
if transforms.objectives.has_auto_scale:
347+
transforms.objectives.calculate_auto_scales(objectives)
348+
if (
349+
transforms.nonlinear_constraints
350+
and transforms.nonlinear_constraints.has_auto_scale
351+
):
352+
assert constraints is not None
353+
transforms.nonlinear_constraints.calculate_auto_scales(constraints)
335354
return transforms
336355

337356
def _create_optimizer(self) -> BasicOptimizer:
338-
# Initialize the optimization model transforms:
357+
# Initialize the optimization model transforms. This may run one initial
358+
# ensemble for auto-scaling purposes:
339359
transforms = self._init_transforms(
340360
np.asarray(
341361
FlattenedControls(self._everest_config.controls).initial_guesses,

src/everest/optimizer/everest2ropt.py

-6
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,13 @@ def _parse_output_constraints(
159159
return
160160

161161
rhs_values: list[float] = []
162-
scales: list[float] = []
163-
auto_scale: list[bool] = []
164162
types: list[ConstraintType] = []
165163

166164
def _add_output_constraint(
167165
rhs_value: float | None, constraint_type: ConstraintType, suffix=None
168166
):
169167
if rhs_value is not None:
170168
rhs_values.append(rhs_value)
171-
scales.append(constr.scale if constr.scale is not None else 1.0)
172-
auto_scale.append(constr.auto_scale or False)
173169
types.append(constraint_type)
174170

175171
for constr in output_constraints:
@@ -190,8 +186,6 @@ def _add_output_constraint(
190186

191187
ropt_config["nonlinear_constraints"] = {
192188
"rhs_values": rhs_values,
193-
"scales": scales,
194-
"auto_scale": auto_scale,
195189
"types": types,
196190
}
197191

src/everest/optimizer/opt_model_transforms.py

+76-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22

33
import numpy as np
44
from numpy.typing import NDArray
5+
from ropt.enums import ConstraintType
56
from ropt.transforms import OptModelTransforms
6-
from ropt.transforms.base import ObjectiveTransform, VariableTransform
7-
8-
from everest.config import ControlConfig, ObjectiveFunctionConfig
7+
from ropt.transforms.base import (
8+
NonLinearConstraintTransform,
9+
ObjectiveTransform,
10+
VariableTransform,
11+
)
12+
13+
from everest.config import (
14+
ControlConfig,
15+
ObjectiveFunctionConfig,
16+
OutputConstraintConfig,
17+
)
918
from everest.config.utils import FlattenedControls
1019

1120

@@ -79,9 +88,58 @@ def has_auto_scale(self) -> bool:
7988
return bool(np.any(self._auto_scales))
8089

8190

91+
class ConstraintScaler(NonLinearConstraintTransform):
92+
def __init__(
93+
self, scales: list[float], auto_scales: list[bool], weights: list[float]
94+
) -> None:
95+
self._scales = np.asarray(scales, dtype=np.float64)
96+
self._auto_scales = np.asarray(auto_scales, dtype=np.bool_)
97+
self._weights = np.asarray(weights, dtype=np.float64)
98+
99+
def transform_rhs_values(
100+
self, rhs_values: NDArray[np.float64], types: NDArray[np.ubyte]
101+
) -> tuple[NDArray[np.float64], NDArray[np.ubyte]]:
102+
def flip_type(constraint_type: ConstraintType) -> ConstraintType:
103+
match constraint_type:
104+
case ConstraintType.GE:
105+
return ConstraintType.LE
106+
case ConstraintType.LE:
107+
return ConstraintType.GE
108+
case _:
109+
return constraint_type
110+
111+
rhs_values = rhs_values / self._scales # noqa: PLR6104
112+
# Flip inequality types if self._scales < 0 in the division above:
113+
types = np.fromiter(
114+
(
115+
flip_type(type_) if scale < 0 else type_
116+
for type_, scale in zip(types, self._scales, strict=False)
117+
),
118+
np.ubyte,
119+
)
120+
return rhs_values, types
121+
122+
def forward(self, constraints: NDArray[np.float64]) -> NDArray[np.float64]:
123+
return constraints / self._scales
124+
125+
def backward(self, constraints: NDArray[np.float64]) -> NDArray[np.float64]:
126+
return constraints * self._scales
127+
128+
def calculate_auto_scales(self, constraints: NDArray[np.float64]) -> None:
129+
auto_scales = np.abs(
130+
np.nansum(constraints * self._weights[:, np.newaxis], axis=0)
131+
)
132+
self._scales[self._auto_scales] *= auto_scales[self._auto_scales]
133+
134+
@property
135+
def has_auto_scale(self) -> bool:
136+
return bool(np.any(self._auto_scales))
137+
138+
82139
def get_optimization_domain_transforms(
83140
controls: list[ControlConfig],
84141
objectives: list[ObjectiveFunctionConfig],
142+
constraints: list[OutputConstraintConfig] | None,
85143
weights: list[float],
86144
) -> OptModelTransforms:
87145
flattened_controls = FlattenedControls(controls)
@@ -107,4 +165,19 @@ def get_optimization_domain_transforms(
107165
],
108166
weights,
109167
),
168+
nonlinear_constraints=(
169+
ConstraintScaler(
170+
[
171+
1.0 if constraint.scale is None else constraint.scale
172+
for constraint in constraints
173+
],
174+
[
175+
False if constraint.auto_scale is None else constraint.auto_scale
176+
for constraint in constraints
177+
],
178+
weights,
179+
)
180+
if constraints
181+
else None
182+
),
110183
)

tests/everest/snapshots/test_ropt_initialization/test_everest2ropt_snapshot/config_advanced.yml/ropt_config.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@
5151
"function_estimators": null,
5252
"realization_filters": null,
5353
"rhs_values": [
54-
0.1
54+
1.0
5555
],
5656
"scales": [
57-
0.1
57+
1.0
5858
],
5959
"types": [
6060
2

tests/everest/test_math_func.py

+39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
from pathlib import Path
33

4+
import numpy as np
45
import pytest
56
import yaml
67

@@ -181,3 +182,41 @@ def test_math_func_auto_scaled_objectives(copy_math_func_test_data_to_tmp):
181182
total = -(expected_p * 0.5 + expected_q * 0.25) / (0.5 + 0.25)
182183

183184
assert total == optim
185+
186+
187+
@pytest.mark.integration_test
188+
def test_math_func_auto_scaled_constraints(copy_math_func_test_data_to_tmp):
189+
config = EverestConfig.load_file("config_advanced.yml")
190+
config_dict = config.model_dump(exclude_none=True)
191+
192+
# control number of batches, no need for full convergence:
193+
config_dict["optimization"]["convergence_tolerance"] = 1e-10
194+
config_dict["optimization"]["max_batch_num"] = 3
195+
196+
# Run with auto_scaling:
197+
config_dict["environment"]["output_folder"] = "output_auto_scale"
198+
config_dict["output_constraints"][0]["auto_scale"] = True
199+
config_dict["output_constraints"][0]["scale"] = 1.0
200+
config = EverestConfig.model_validate(config_dict)
201+
run_model = EverestRunModel.create(config)
202+
evaluator_server_config = EvaluatorServerConfig()
203+
run_model.run_experiment(evaluator_server_config)
204+
result1 = run_model.result
205+
206+
# Run the equivalent without auto-scaling:
207+
config_dict["environment"]["output_folder"] = "output_manual_scale"
208+
config_dict["output_constraints"][0]["auto_scale"] = False
209+
config_dict["output_constraints"][0]["scale"] = 0.25 # x(0)
210+
# We need one batch less, no auto-scaling:
211+
config_dict["optimization"]["max_batch_num"] -= 1
212+
config = EverestConfig.model_validate(config_dict)
213+
run_model = EverestRunModel.create(config)
214+
evaluator_server_config = EvaluatorServerConfig()
215+
run_model.run_experiment(evaluator_server_config)
216+
result2 = run_model.result
217+
218+
assert result1.total_objective == pytest.approx(result2.total_objective)
219+
assert np.allclose(
220+
np.fromiter(result1.controls.values(), dtype=np.float64),
221+
np.fromiter(result2.controls.values(), dtype=np.float64),
222+
)

tests/everest/test_ropt_initialization.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def test_everest2ropt_controls_auto_scale():
4848
transforms=get_optimization_domain_transforms(
4949
config.controls,
5050
config.objective_functions,
51+
config.output_constraints,
5152
config.model.realizations_weights,
5253
),
5354
)
@@ -65,6 +66,7 @@ def test_everest2ropt_variables_auto_scale():
6566
transforms=get_optimization_domain_transforms(
6667
config.controls,
6768
config.objective_functions,
69+
config.output_constraints,
6870
config.model.realizations_weights,
6971
),
7072
)
@@ -136,6 +138,7 @@ def test_everest2ropt_controls_input_constraint_auto_scale():
136138
transforms=get_optimization_domain_transforms(
137139
config.controls,
138140
config.objective_functions,
141+
config.output_constraints,
139142
config.model.realizations_weights,
140143
),
141144
)
@@ -280,6 +283,7 @@ def test_everest2ropt_snapshot(case, snapshot):
280283
transforms=get_optimization_domain_transforms(
281284
config.controls,
282285
config.objective_functions,
286+
config.output_constraints,
283287
config.model.realizations_weights,
284288
),
285289
).model_dump()

0 commit comments

Comments
 (0)