77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""
|
|||
|
|
optimizer/budget.py
|
||
|
|
Time budget manager — tracks elapsed time and estimates remaining runs.
|
||
|
|
Updates its average-run-time estimate after every completed run.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from loguru import logger
|
||
|
|
|
||
|
|
|
||
|
|
class BudgetManager:
|
||
|
|
"""
|
||
|
|
Tracks the time budget for an optimization session.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
budget = BudgetManager(budget_minutes=60)
|
||
|
|
budget.start()
|
||
|
|
...after each run:
|
||
|
|
budget.record_run(elapsed_seconds)
|
||
|
|
if budget.is_exhausted():
|
||
|
|
break
|
||
|
|
remaining = budget.estimated_runs_remaining()
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, budget_minutes: int, initial_seconds_per_run: float = 75.0):
|
||
|
|
self.budget_seconds = budget_minutes * 60
|
||
|
|
self.avg_seconds_per_run = initial_seconds_per_run
|
||
|
|
self._start_ts: float = None
|
||
|
|
self._run_times: list[float] = []
|
||
|
|
|
||
|
|
def start(self) -> None:
|
||
|
|
self._start_ts = time.time()
|
||
|
|
logger.info(f"BudgetManager: started. Budget = {self.budget_seconds/60:.0f} min")
|
||
|
|
|
||
|
|
@property
|
||
|
|
def elapsed_seconds(self) -> float:
|
||
|
|
if self._start_ts is None:
|
||
|
|
return 0.0
|
||
|
|
return time.time() - self._start_ts
|
||
|
|
|
||
|
|
@property
|
||
|
|
def remaining_seconds(self) -> float:
|
||
|
|
return max(0.0, self.budget_seconds - self.elapsed_seconds)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def elapsed_pct(self) -> float:
|
||
|
|
return min(100.0, self.elapsed_seconds / self.budget_seconds * 100)
|
||
|
|
|
||
|
|
def record_run(self, seconds: float) -> None:
|
||
|
|
"""Call after each run completes. Updates rolling average."""
|
||
|
|
self._run_times.append(seconds)
|
||
|
|
# Rolling average (last 5 runs for responsiveness)
|
||
|
|
recent = self._run_times[-5:]
|
||
|
|
self.avg_seconds_per_run = sum(recent) / len(recent)
|
||
|
|
|
||
|
|
def estimated_runs_remaining(self) -> int:
|
||
|
|
"""How many more runs fit in the remaining budget."""
|
||
|
|
if self.avg_seconds_per_run <= 0:
|
||
|
|
return 0
|
||
|
|
return max(0, int(self.remaining_seconds / self.avg_seconds_per_run))
|
||
|
|
|
||
|
|
def is_exhausted(self) -> bool:
|
||
|
|
"""True when less than 1 average run's time remains."""
|
||
|
|
return self.remaining_seconds < self.avg_seconds_per_run
|
||
|
|
|
||
|
|
def can_fit(self, n_runs: int) -> bool:
|
||
|
|
"""True if n_runs more runs fit in remaining budget."""
|
||
|
|
return self.remaining_seconds >= n_runs * self.avg_seconds_per_run
|
||
|
|
|
||
|
|
def summary(self) -> str:
|
||
|
|
return (
|
||
|
|
f"Budget: {self.elapsed_seconds/60:.1f}/{self.budget_seconds/60:.0f} min used. "
|
||
|
|
f"Avg run: {self.avg_seconds_per_run:.0f}s. "
|
||
|
|
f"Est. remaining: {self.estimated_runs_remaining()} runs."
|
||
|
|
)
|