88from collections .abc import Generator , Sequence
99from contextlib import contextmanager
1010from dataclasses import dataclass , field
11- from itertools import groupby
1211from pathlib import Path
1312from textwrap import dedent
1413from typing import Any , ClassVar
4039 wait_for_server_to_stop ,
4140)
4241from everest .strings import EVEREST , OPT_PROGRESS_ID , SIM_PROGRESS_ID
42+ from everest .util import format_list
4343
4444JOB_SUCCESS = "Finished"
4545JOB_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
162143class 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
201171class _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 )
0 commit comments