Skip to content

Commit 0bd54e9

Browse files
committed
Use a scaler object to implement output constraint scaling
1 parent 4a2abee commit 0bd54e9

File tree

6 files changed

+142
-29
lines changed

6 files changed

+142
-29
lines changed

src/ert/run_models/everest_run_model.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from everest.everest_storage import EverestStorage, OptimalResult
3535
from everest.optimizer.everest2ropt import everest2ropt
3636
from everest.optimizer.opt_model_transforms import (
37+
ConstraintScaler,
3738
ObjectiveScaler,
3839
get_opt_model_transforms,
3940
)
@@ -223,16 +224,30 @@ def _init_transforms(self, variables: NDArray[np.float64]) -> OptModelTransforms
223224
transforms = get_opt_model_transforms(
224225
self._everest_config.controls,
225226
self._everest_config.objective_functions,
227+
self._everest_config.output_constraints,
226228
realization_weights,
227229
)
228230
# If required, initialize auto-scaling:
229231
assert isinstance(transforms.objectives, ObjectiveScaler)
230-
if transforms.objectives.has_auto_scale:
231-
objectives, _, _ = self._run_forward_model(
232+
assert transforms.nonlinear_constraints is None or isinstance(
233+
transforms.nonlinear_constraints, ConstraintScaler
234+
)
235+
if transforms.objectives.has_auto_scale or (
236+
transforms.nonlinear_constraints
237+
and transforms.nonlinear_constraints.has_auto_scale
238+
):
239+
objectives, constraints, _ = self._run_forward_model(
232240
np.repeat(np.expand_dims(variables, axis=0), nreal, axis=0),
233241
realizations,
234242
)
235-
transforms.objectives.calculate_auto_scales(objectives)
243+
if transforms.objectives.has_auto_scale:
244+
transforms.objectives.calculate_auto_scales(objectives)
245+
if (
246+
transforms.nonlinear_constraints
247+
and transforms.nonlinear_constraints.has_auto_scale
248+
):
249+
assert constraints is not None
250+
transforms.nonlinear_constraints.calculate_auto_scales(constraints)
236251
return transforms
237252

238253
def _create_optimizer(self) -> BasicOptimizer:

src/everest/optimizer/everest2ropt.py

+4-21
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ def _parse_objectives(objective_functions: list[ObjectiveFunctionConfig], ropt_c
9999

100100

101101
def _parse_input_constraints(
102-
controls: FlattenedControls,
103102
input_constraints: list[InputConstraintConfig] | None,
104103
formatted_control_names: list[str],
105104
formatted_control_names_dotdash: list[str],
@@ -160,17 +159,13 @@ def _parse_output_constraints(
160159
return
161160

162161
rhs_values: list[float] = []
163-
scales: list[float] = []
164-
auto_scale: list[bool] = []
165162
types: list[ConstraintType] = []
166163

167164
def _add_output_constraint(
168165
rhs_value: float | None, constraint_type: ConstraintType, suffix=None
169166
):
170167
if rhs_value is not None:
171168
rhs_values.append(rhs_value)
172-
scales.append(constr.scale if constr.scale is not None else 1.0)
173-
auto_scale.append(constr.auto_scale or False)
174169
types.append(constraint_type)
175170

176171
for constr in output_constraints:
@@ -181,25 +176,16 @@ def _add_output_constraint(
181176
raise RuntimeError(
182177
"output constraint error: target cannot be combined with bounds"
183178
)
179+
_add_output_constraint(target, ConstraintType.EQ)
184180
_add_output_constraint(
185-
target,
186-
ConstraintType.EQ,
181+
upper_bound, ConstraintType.LE, None if lower_bound is None else "upper"
187182
)
188183
_add_output_constraint(
189-
upper_bound,
190-
ConstraintType.LE,
191-
None if lower_bound is None else "upper",
192-
)
193-
_add_output_constraint(
194-
lower_bound,
195-
ConstraintType.GE,
196-
None if upper_bound is None else "lower",
184+
lower_bound, ConstraintType.GE, None if upper_bound is None else "lower"
197185
)
198186

199187
ropt_config["nonlinear_constraints"] = {
200188
"rhs_values": rhs_values,
201-
"scales": scales,
202-
"auto_scale": auto_scale,
203189
"types": types,
204190
}
205191

@@ -350,12 +336,9 @@ def everest2ropt(
350336
"""
351337
ropt_config: dict[str, Any] = {}
352338

353-
flattened_controls = FlattenedControls(ever_config.controls)
354-
355-
_parse_controls(flattened_controls, ropt_config)
339+
_parse_controls(FlattenedControls(ever_config.controls), ropt_config)
356340
_parse_objectives(ever_config.objective_functions, ropt_config)
357341
_parse_input_constraints(
358-
flattened_controls,
359342
ever_config.input_constraints,
360343
ever_config.formatted_control_names,
361344
ever_config.formatted_control_names_dotdash,

src/everest/optimizer/opt_model_transforms.py

+75-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,57 @@ 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+
types = np.fromiter(
113+
(
114+
flip_type(type_) if scale < 0 else type_
115+
for type_, scale in zip(types, self._scales, strict=False)
116+
),
117+
np.ubyte,
118+
)
119+
return rhs_values, types
120+
121+
def forward(self, constraints: NDArray[np.float64]) -> NDArray[np.float64]:
122+
return constraints / self._scales
123+
124+
def backward(self, constraints: NDArray[np.float64]) -> NDArray[np.float64]:
125+
return constraints * self._scales
126+
127+
def calculate_auto_scales(self, constraints: NDArray[np.float64]) -> None:
128+
auto_scales = np.abs(
129+
np.nansum(constraints * self._weights[:, np.newaxis], axis=0)
130+
)
131+
self._scales[self._auto_scales] *= auto_scales[self._auto_scales]
132+
133+
@property
134+
def has_auto_scale(self) -> bool:
135+
return bool(np.any(self._auto_scales))
136+
137+
82138
def get_opt_model_transforms(
83139
controls: list[ControlConfig],
84140
objectives: list[ObjectiveFunctionConfig],
141+
constraints: list[OutputConstraintConfig] | None,
85142
weights: list[float],
86143
) -> OptModelTransforms:
87144
flattened_controls = FlattenedControls(controls)
@@ -107,4 +164,19 @@ def get_opt_model_transforms(
107164
],
108165
weights,
109166
),
167+
nonlinear_constraints=(
168+
ConstraintScaler(
169+
[
170+
1.0 if constraint.scale is None else constraint.scale
171+
for constraint in constraints
172+
],
173+
[
174+
False if constraint.auto_scale is None else constraint.auto_scale
175+
for constraint in constraints
176+
],
177+
weights,
178+
)
179+
if constraints
180+
else None
181+
),
110182
)

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"] = "output1"
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"] = "output2"
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_opt_model_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_opt_model_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_opt_model_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_opt_model_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)