Skip to content

Commit 071a239

Browse files
committed
Update everest monitoring code
- Replace/remove the realization numbers for finished/failed realizations with just their count, because these numbers are only used internally by everest and do not correspond with the everest model realization numbers. - Add the numbers for failed realizations and for failed perturbations to the monitoring output for each finalized batch.
1 parent 3a4a976 commit 071a239

10 files changed

Lines changed: 196 additions & 113 deletions

File tree

src/ert/run_models/event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class EverestBatchResultEvent(BaseModel, extra="forbid"):
5151
everest_event: Literal["OPTIMIZATION_RESULT",]
5252
result_type: Literal["FunctionResult", "GradientResult"]
5353
results: dict[str, Any] | None = None
54+
failures: dict[int, list[int]] | None = None
5455

5556

5657
class RunModelTimeEvent(RunModelEvent):

src/ert/run_models/everest_run_model.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -626,23 +626,25 @@ def _handle_optimizer_results(self, results: tuple[Results, ...]) -> None:
626626
target_ensemble.save_batch_dataframes(dataframes=batch_dict)
627627
target_ensemble.update_improvement_flag(is_improvement=False)
628628

629-
for r in results:
629+
for result in results:
630630
batches = (
631631
self._experiment.ensembles_with_function_results
632-
if isinstance(r, FunctionResults)
632+
if isinstance(result, FunctionResults)
633633
else self._experiment.ensembles_with_gradient_results
634634
)
635-
ens = next((ens for ens in batches if ens.iteration == r.batch_id), None)
635+
ens = next(
636+
(ens for ens in batches if ens.iteration == result.batch_id), None
637+
)
636638
if ens is None:
637639
continue
638640

639641
results_dict: dict[str, Any] | None = None
640-
if isinstance(r, FunctionResults):
642+
if isinstance(result, FunctionResults):
641643
results_dict = {}
642644
if ens.realization_controls is not None:
643645
results_dict |= {
644646
"controls": ens.realization_controls.drop(
645-
"realization", "simulation_id"
647+
"batch_id", "realization", "simulation_id"
646648
).to_dicts()[0],
647649
}
648650

@@ -719,14 +721,25 @@ def _handle_optimizer_results(self, results: tuple[Results, ...]) -> None:
719721
"perturbation_constraints": perturbation_gradient_dicts
720722
}
721723

724+
failures = defaultdict(list)
725+
if ens.everest_realization_info is not None:
726+
for r in ens.everest_realization_info:
727+
if ens.has_failure(r):
728+
m = ens.everest_realization_info[r]["model_realization"]
729+
p = ens.everest_realization_info[r]["perturbation"]
730+
failures[m].append(p)
731+
722732
self.send_event(
723733
EverestBatchResultEvent(
724-
batch=r.batch_id,
734+
batch=result.batch_id,
725735
everest_event="OPTIMIZATION_RESULT",
726-
result_type="FunctionResult"
727-
if isinstance(r, FunctionResults)
728-
else "GradientResult",
736+
result_type=(
737+
"FunctionResult"
738+
if isinstance(result, FunctionResults)
739+
else "GradientResult"
740+
),
729741
results=results_dict,
742+
failures={k: v for k, v in failures.items() if v} or None,
730743
)
731744
)
732745

src/everest/bin/utils.py

Lines changed: 59 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from collections.abc import Generator, Sequence
99
from contextlib import contextmanager
1010
from dataclasses import dataclass, field
11-
from itertools import groupby
1211
from pathlib import Path
1312
from textwrap import dedent
1413
from typing import Any, ClassVar
@@ -40,6 +39,7 @@
4039
wait_for_server_to_stop,
4140
)
4241
from everest.strings import EVEREST, OPT_PROGRESS_ID, SIM_PROGRESS_ID
42+
from everest.util import format_list
4343

4444
JOB_SUCCESS = "Finished"
4545
JOB_RUNNING = "Running"
@@ -139,25 +139,6 @@ def _get_max_width(sequence: list[Any]) -> int:
139139
return max(len(item) for item in sequence)
140140

141141

142-
def _format_list(values: Sequence[int]) -> str:
143-
"""Formats a sequence of integers into a comma separated string of ranges.
144-
145-
For instance: {1, 3, 4, 5, 7, 8, 10} -> "1, 3-5, 7-8, 10"
146-
"""
147-
grouped = (
148-
tuple(y for _, y in x)
149-
for _, x in groupby(enumerate(sorted(values)), lambda x: x[0] - x[1])
150-
)
151-
return ", ".join(
152-
(
153-
"-".join([str(sub_group[0]), str(sub_group[-1])])
154-
if len(sub_group) > 1
155-
else str(sub_group[0])
156-
)
157-
for sub_group in grouped
158-
)
159-
160-
161142
@dataclass
162143
class JobProgress:
163144
name: str
@@ -177,7 +158,7 @@ class JobProgress:
177158
JOB_FAILURE: ansi.RED,
178159
}
179160

180-
def _status_string(self, max_widths: dict[str, int]) -> str:
161+
def progress_str(self, max_widths: dict[str, int]) -> str:
181162
string = []
182163
for state in [JOB_RUNNING, JOB_SUCCESS, JOB_FAILURE]:
183164
number_of_simulations = len(self.status[state])
@@ -186,27 +167,16 @@ def _status_string(self, max_widths: dict[str, int]) -> str:
186167
string.append(f"{color}{number_of_simulations:>{width}}{ansi.RESET}")
187168
return "/".join(string)
188169

189-
def progress_str(self, max_widths: dict[str, int]) -> str:
190-
msg = ""
191-
for state in [JOB_SUCCESS, JOB_FAILURE]:
192-
simulations_list = _format_list(self.status[state])
193-
width = _get_max_width([simulations_list])
194-
if width > 0:
195-
color = self.STATUS_COLOR[state]
196-
msg += f" | {color}{state}: {simulations_list:<{width}}{ansi.RESET}"
197-
198-
return self._status_string(max_widths) + msg
199-
200170

201171
class _DetachedMonitor:
202-
WIDTH = 78
172+
WIDTH = 60
203173
INDENT = 2
204174
FLOAT_FMT = ".5g"
205175

206176
def __init__(self) -> None:
207177
self._clear_lines: int = 0
208-
self._batches_done = set[int]()
209178
self._last_reported_batch: int = -1
179+
self._last_reported_opt_progress: int = -1
210180
self._snapshots: dict[int, EnsembleSnapshot] = {}
211181

212182
def update(self, status: dict[str, Any]) -> None:
@@ -215,8 +185,9 @@ def update(self, status: dict[str, Any]) -> None:
215185
opt_status = status[OPT_PROGRESS_ID]
216186
if opt_status:
217187
msg = self._get_opt_progress_single_batch(opt_status)
218-
ansi.ansi_print(msg + "\n")
219-
self._clear_lines = 0
188+
if msg:
189+
ansi.ansi_print(msg + "\n")
190+
self._clear_lines = 0
220191
if SIM_PROGRESS_ID in status:
221192
match status[SIM_PROGRESS_ID]:
222193
case EndEvent(msg=msg):
@@ -254,19 +225,6 @@ def update(self, status: dict[str, Any]) -> None:
254225
except Exception:
255226
logging.getLogger(EVEREST).debug(traceback.format_exc())
256227

257-
def get_opt_progress(self, context_status: dict[str, Any]) -> tuple[str, int]:
258-
cli_monitor_data = context_status["cli_monitor_data"]
259-
messages = []
260-
first_batch = -1
261-
for idx, batch in enumerate(cli_monitor_data["batches"]):
262-
if batch not in self._batches_done:
263-
if first_batch < 0:
264-
first_batch = batch
265-
self._batches_done.add(batch)
266-
msg = self._get_opt_progress_batch(cli_monitor_data, batch, idx)
267-
messages.append(msg)
268-
return self._join_two_newlines(messages), first_batch
269-
270228
def _get_opt_progress_batch(
271229
self, cli_monitor_data: dict[str, Any], batch: int, idx: int
272230
) -> str:
@@ -296,29 +254,57 @@ def _get_opt_progress_batch(
296254

297255
def _get_opt_progress_single_batch(self, cli_monitor_data: dict[str, Any]) -> str:
298256
batch: int = cli_monitor_data.get("batch", 0)
299-
header = self._make_header(f"Optimization progress (Batch #{batch})")
300-
width = _get_max_width(cli_monitor_data["controls"].keys())
301-
controls = self._join_one_newline_indent(
302-
[
303-
f"{name:>{width}}: {value:{self.FLOAT_FMT}}"
304-
for name, value in cli_monitor_data["controls"].items()
305-
]
306-
)
307-
expected_objectives = cli_monitor_data["expected_objectives"]
308-
width = _get_max_width(expected_objectives.keys())
309-
objectives = self._join_one_newline_indent(
310-
[
311-
f"{name:>{width}}: {value:{self.FLOAT_FMT}}"
312-
for name, value in expected_objectives.items()
313-
]
314-
)
315-
objective_value = cli_monitor_data["objective_value"]
316-
total_objective = (
317-
f"Total normalized objective: {objective_value:{self.FLOAT_FMT}}"
318-
)
319-
return self._join_two_newlines_indent(
320-
(header, controls, objectives, total_objective)
321-
)
257+
if batch == self._last_reported_opt_progress:
258+
return ""
259+
260+
lines = [self._make_header(f"Optimization progress (Batch #{batch})")]
261+
262+
if controls := cli_monitor_data.get("controls"):
263+
width = _get_max_width(controls.keys())
264+
lines.append(
265+
self._join_one_newline_indent(
266+
[
267+
f"{name:>{width}}: {value:{self.FLOAT_FMT}}"
268+
for name, value in controls.items()
269+
]
270+
)
271+
)
272+
if expected_objectives := cli_monitor_data.get("expected_objectives"):
273+
width = _get_max_width(expected_objectives.keys())
274+
lines.append(
275+
self._join_one_newline_indent(
276+
[
277+
f"{name:>{width}}: {value:{self.FLOAT_FMT}}"
278+
for name, value in expected_objectives.items()
279+
]
280+
)
281+
)
282+
if objective_value := cli_monitor_data.get("objective_value"):
283+
lines.append(
284+
f"Total normalized objective: {objective_value:{self.FLOAT_FMT}}"
285+
)
286+
287+
if failures := cli_monitor_data.get("failures", {}):
288+
failed_lines = []
289+
if failed_functions := [r for r, p in failures.items() if -1 in p]:
290+
s = "s" if len(failed_functions) > 1 else ""
291+
failed_lines.append(
292+
f"{ansi.RED}Failed function evaluation{s} for realization{s}: "
293+
f"{format_list(failed_functions)}{ansi.RESET}"
294+
)
295+
for k, v in failures.items():
296+
if p := [item for item in v if item >= 0]:
297+
s = "s" if len(p) > 1 else ""
298+
failed_lines.append(
299+
f"{ansi.RED}Failed perturbation{s} for realization {k}: "
300+
f"{format_list(p)}{ansi.RESET}"
301+
)
302+
if failed_lines:
303+
lines.append(self._join_one_newline_indent(failed_lines))
304+
305+
self._last_reported_opt_progress = batch
306+
307+
return self._join_two_newlines_indent(lines)
322308

323309
@staticmethod
324310
def _get_progress_summary(status: dict[str, int]) -> str:
@@ -362,8 +348,7 @@ def _get_job_states(cls, snapshot: EnsembleSnapshot) -> str:
362348
if job.errors:
363349
print_lines.extend(
364350
[
365-
f"{ansi.RED}{job.name:>{width}}: Failed: {err}, "
366-
f"realizations: {_format_list(job.errors[err])}{ansi.RESET}"
351+
f"{ansi.RED}{job.name:>{width}}: {err}{ansi.RESET}"
367352
for err in job.errors
368353
]
369354
)

src/everest/detached/client.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,18 @@ def get_opt_status_from_batch_result_event(
216216

217217
assert event.batch is not None
218218

219+
if event.result_type == "FunctionResult":
220+
return {
221+
"batch": event.batch,
222+
"controls": event.results["controls"],
223+
"objective_value": event.results["total_objective_value"],
224+
"expected_objectives": event.results["objectives"],
225+
"failures": event.failures,
226+
}
227+
219228
return {
220229
"batch": event.batch,
221-
"controls": event.results["controls"],
222-
"objective_value": event.results["total_objective_value"],
223-
"expected_objectives": event.results["objectives"],
230+
"failures": event.failures,
224231
}
225232

226233

@@ -253,14 +260,13 @@ def start_monitor(
253260
message = websocket.recv(timeout=1.0)
254261
event = status_event_from_json(message)
255262
if isinstance(event, EverestBatchResultEvent):
256-
if event.result_type == "FunctionResult":
257-
callback(
258-
{
259-
OPT_PROGRESS_ID: get_opt_status_from_batch_result_event( # noqa: E501
260-
event
261-
)
262-
}
263-
)
263+
callback(
264+
{
265+
OPT_PROGRESS_ID: get_opt_status_from_batch_result_event(
266+
event
267+
)
268+
}
269+
)
264270
else:
265271
callback({SIM_PROGRESS_ID: event})
266272
except TimeoutError:

src/everest/util/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
from ropt.version import version as ropt_version
22

3+
from ._utils import format_list
4+
5+
__all__ = [
6+
"format_list",
7+
]
8+
9+
310
try:
411
from ert.shared.version import version as ert_version
512
except ImportError:

src/everest/util/_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from collections.abc import Sequence
2+
from itertools import groupby
3+
4+
5+
def format_list(values: Sequence[int]) -> str:
6+
"""Formats a sequence of integers into a comma separated string of ranges.
7+
8+
For instance: {1, 3, 4, 5, 7, 8, 10} -> "1, 3-5, 7-8, 10"
9+
"""
10+
grouped = (
11+
tuple(y for _, y in x)
12+
for _, x in groupby(enumerate(sorted(values)), lambda x: x[0] - x[1])
13+
)
14+
return ", ".join(
15+
(
16+
"-".join([str(sub_group[0]), str(sub_group[-1])])
17+
if len(sub_group) > 1
18+
else str(sub_group[0])
19+
)
20+
for sub_group in grouped
21+
)

0 commit comments

Comments
 (0)