[docs]classProgressBar:"""A progress bar that logs timing info when not in a terminal. This class wraps tqdm to provide progress display in terminals and structured logging when running in non-terminal environments (e.g., batch jobs, logs, pipes). Parameters ---------- total : int Total number of iterations. initial : int, optional Initial counter value. Default: 0. desc : str, optional Description prefix. Default: "Progress". disable : bool, optional Force disable progress bar and logging. Default: None (auto-detect). progress_interval : int, optional Log progress every N iterations when not in terminal. Default: min(100, total//10). position : int, optional Line offset for tqdm display. Default: None. Example ------- >>> with ProgressBar(total=100, desc="Simulation") as pbar: ... for i in range(100): ... pbar.update(1) """def__init__(self,total:int,initial:int=0,desc:str="Progress",disable:bool|None=None,progress_interval:int|None=None,position:int|None=None,):self.total=totalself.initial=initialself.desc=descself._n=initialself.progress_interval=progress_intervalifprogress_intervalisnotNoneelsemin(100,max(1,total//10))self._is_terminal=is_terminal()self.disable=disable# Create tqdm instance with appropriate outputifself._is_terminalandnotself.disable:# Terminal: normal displayself.pbar=tqdm(total=total,initial=initial,disable=False,desc=desc,position=position,)elifnotself.disable:# Non-terminal: redirect to StringIO so tqdm updates internal state# but doesn't display. This preserves EMA smoothing for rate calc.self._dummy_file=StringIO()self.pbar=tqdm(total=total,initial=initial,disable=False,file=self._dummy_file,desc=desc,position=position,)self._last_log_step=initialelse:# Disabled: no tqdm at allself.pbar=None
[docs]defupdate(self,n:int=1):"""Update progress by n steps."""ifself.disableorself.pbarisNone:self._n+=nreturnself._n+=nself.pbar.update(n)# Non-terminal: log progress at intervals using tqdm's format_dictifnotself._is_terminal:if(self._n-self._last_log_step)>=self.progress_interval:self._log_progress()self._last_log_step=self._n
@staticmethoddef_format_time(seconds:float)->str:"""Format seconds into HH:MM:SS. Parameters ---------- seconds : float Time in seconds. Returns ------- str Formatted time string as HH:MM:SS. """total_seconds=int(seconds)hours=total_seconds//3600minutes=(total_seconds%3600)//60secs=total_seconds%60returnf"{hours:d}:{minutes:02d}:{secs:02d}"@staticmethoddef_format_rate(rate:float)->str:"""Format rate adaptively as steps/s or s/step. Parameters ---------- rate : float Rate in steps per second. Returns ------- str Formatted rate string with appropriate unit. """ifrate<=0:return"-- step/s"ifrate>=1:returnf"{rate:.2f} step/s"else:returnf"{1/rate:.2f} s/step"def_log_progress(self):"""Log current progress with timing info from tqdm."""ifself.pbarisNone:returnfmt=self.pbar.format_dictrate=fmt.get("rate")or0elapsed=fmt.get("elapsed",0)remaining=(self.total-self._n)/rateifrate>0else0logger.info(f"{self.desc}: {self._n}/{self.total} "f"({100*self._n/self.total:.1f}%) | "f"Elapsed: {self._format_time(elapsed)} | "f"Remaining: {self._format_time(remaining)} | "f"Speed: {self._format_rate(rate)}")
[docs]defclose(self):"""Close the progress bar and log final status."""ifself.disableorself.pbarisNone:returnifnotself._is_terminalandself._n>self._last_log_step:self._log_progress()self.pbar.close()