import sys import functools from time import perf_counter try: import tqdm except ImportError: has_tqdm = False else: has_tqdm = True from veros import logger, time, logs, runtime_settings as rs, runtime_state as rst BAR_FORMAT = ( " Current iteration: {iteration:<5} ({time:.2f}/{total:.2f}{unit} | {percentage:>4.1f}% | " "{rate:.2f}{rate_unit} | {eta:.1f}{eta_unit} left)" ) class LoggingProgressBar: """A simple progress report to logger.info Serves as a fallback where TQDM is not available or not feasible (writing to a file, in multiprocessing contexts). """ def __init__(self, total, start_time=0, start_iteration=0, time_unit="seconds"): self._start_time = start_time self._start_iteration = start_iteration self._total = total _, self._time_unit = time.format_time(total) def __enter__(self): self._start = perf_counter() self._iteration = self._start_iteration self._time = self._start_time self.flush() return self def __exit__(self, *args, **kwargs): pass def advance_time(self, amount, *args, **kwargs): self._iteration += 1 self._time += amount self.flush() def flush(self): report_time = time.convert_time(self._time, "seconds", self._time_unit) total_time = time.convert_time(self._total, "seconds", self._time_unit) if self._time > self._start_time: rate_in_seconds = (perf_counter() - self._start) / (self._time - self._start_time) else: rate_in_seconds = 0 rate_in_seconds_per_year = rate_in_seconds / time.convert_time(1, "seconds", "years") rate, rate_unit = time.format_time(rate_in_seconds_per_year) eta, eta_unit = time.format_time((self._total - self._time) * rate_in_seconds) if self._start_time < self._total: percentage = 100 * (self._time - self._start_time) / (self._total - self._start_time) else: percentage = 100 logger.info( BAR_FORMAT, time=report_time, total=total_time, unit=self._time_unit[0], percentage=percentage, iteration=self._iteration, rate=rate, rate_unit=f"{rate_unit[0]}/(model year)", eta=eta, eta_unit=eta_unit[0], ) class FancyProgressBar: """A fancy progress bar based on TQDM that stays at the bottom of the terminal.""" def __init__(self, total, start_time=0, start_iteration=0, time_unit="seconds"): self._time = self._start_time = start_time self._iteration = self._start_iteration = start_iteration self._total = total total_runlen, time_unit = time.format_time(total) self._time_unit = time_unit class _VerosTQDM(tqdm.tqdm): """Stripped down version of tqdm.tqdm We only need TQDM to handle dynamic updates to the progress indicator. """ def __init__(self, *args, **kwargs): kwargs.update(leave=True) super().__init__(*args, **kwargs) @property def format_dict(other): report_time = time.convert_time(self._time, "seconds", self._time_unit) total_time = time.convert_time(self._total, "seconds", self._time_unit) if self._start_time < self._total: percentage = 100 * (self._time - self._start_time) / (self._total - self._start_time) else: percentage = 100 d = super().format_dict if d["elapsed"] > 0: if self._time > self._start_time: rate_in_seconds = d["elapsed"] / (self._time - self._start_time) else: rate_in_seconds = 0 rate_in_seconds_per_year = rate_in_seconds / time.convert_time(1, "seconds", "years") rate, rate_unit = time.format_time(rate_in_seconds_per_year) eta, eta_unit = time.format_time((self._total - self._time) * rate_in_seconds) else: rate, rate_unit = 0, "s" eta, eta_unit = 0, "s" d.update( iteration=self._iteration, time=report_time, total=total_time, unit=self._time_unit[0], percentage=percentage, rate=rate, rate_unit=f"{rate_unit[0]}/(model year)", eta=eta, eta_unit=eta_unit[0], ) return d def format_meter(other, *args, bar_format, **kwargs): return bar_format.format(**kwargs) self._pbar = _VerosTQDM(file=sys.stdout, bar_format=BAR_FORMAT) def __enter__(self, *args, **kwargs): self._iteration = self._start_iteration self._time = self._start_time logs.setup_logging( loglevel=rs.loglevel, stream_sink=functools.partial(self._pbar.write, file=sys.stdout, end="") ) self._pbar.__enter__(*args, **kwargs) return self def __exit__(self, *args, **kwargs): logs.setup_logging(loglevel=rs.loglevel) self._pbar.__exit__(*args, **kwargs) def advance_time(self, amount): self._iteration += 1 self._time += amount self.flush() def flush(self): self._pbar.refresh() def get_progress_bar(state, use_tqdm=None): if use_tqdm is None: use_tqdm = sys.stdout.isatty() and rst.proc_num == 1 and has_tqdm if use_tqdm and not has_tqdm: raise RuntimeError("tqdm failed to import. Try `pip install tqdm` or set use_tqdm=False.") kwargs = dict( total=state.settings.runlen + float(state.variables.time), start_time=float(state.variables.time), start_iteration=int(state.variables.itt), ) if use_tqdm: pbar = FancyProgressBar(**kwargs) else: pbar = LoggingProgressBar(**kwargs) return pbar