Skip to content

Commit 56de805

Browse files
committed
Refactor time limit computation, making it a bit easier to find and follow
1 parent f7862b9 commit 56de805

File tree

1 file changed

+40
-44
lines changed

1 file changed

+40
-44
lines changed

problemtools/verifyproblem.py

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,15 +1764,17 @@ def __str__(self) -> str:
17641764
return 'submissions'
17651765

17661766
def check_submission(
1767-
self, sub, context: Context, expected_verdict: Verdict, timelim: float, timelim_low: float, timelim_high: float
1767+
self, sub, context: Context, expected_verdict: Verdict, timelim: float, timelim_high: float
17681768
) -> SubmissionResult:
17691769
desc = f'{expected_verdict} submission {sub}'
17701770
partial = False
17711771
if expected_verdict == 'PAC':
1772-
# For partially accepted solutions, use the low timelim instead of the real one,
1773-
# to make sure we have margin in both directions.
17741772
expected_verdict = 'AC'
17751773
partial = True
1774+
# For partially accepted, we don't want to use them to lower bound the time limit, but we do want
1775+
# to warn if they're slow enough that they would have affected the time limit, had they been used
1776+
# to compute it.
1777+
timelim_low = timelim / self.problem.metadata.limits.time_multipliers.ac_to_time_limit
17761778
else:
17771779
timelim_low = timelim
17781780

@@ -1827,28 +1829,34 @@ def start_background_work(self, context: Context) -> None:
18271829
for sub in self._submissions[acr]:
18281830
context.submit_background_work(lambda s: s.compile(), sub)
18291831

1832+
def _compute_time_limit(self, fixed_limit: float | None, lower_bound_runtime: float | None) -> tuple[float, float]:
1833+
if fixed_limit is None and lower_bound_runtime is None:
1834+
# 5 minutes is our currently hard coded upper bound for what to allow when we don't know the time limit yet
1835+
return 300.0, 300.0
1836+
1837+
limits = self.problem.metadata.limits
1838+
if fixed_limit is not None:
1839+
timelim = fixed_limit
1840+
else:
1841+
assert lower_bound_runtime is not None, 'Assert to keep mypy happy'
1842+
exact_timelim = lower_bound_runtime * limits.time_multipliers.ac_to_time_limit
1843+
timelim = max(1, math.ceil(exact_timelim / limits.time_resolution)) * limits.time_resolution
1844+
1845+
return timelim, timelim * limits.time_multipliers.time_limit_to_tle
1846+
18301847
def check(self, context: Context) -> bool:
18311848
if self._check_res is not None:
18321849
return self._check_res
18331850
self._check_res = True
18341851

18351852
limits = self.problem.metadata.limits
1836-
time_multiplier = limits.time_multipliers.ac_to_time_limit
1837-
safety_margin = limits.time_multipliers.time_limit_to_tle
1853+
ac_to_time_limit = limits.time_multipliers.ac_to_time_limit
18381854

1839-
timelim_margin_lo = 300.0 # 5 minutes
1840-
timelim_margin = 300.0
1841-
timelim = 300.0
1855+
fixed_limit: float | None = context.fixed_timelim if context.fixed_timelim is not None else limits.time_limit
1856+
lower_bound_runtime: float | None = None # The runtime of the slowest submission used to lower bound the time limit.
18421857

1843-
if limits.time_limit is not None:
1844-
timelim = limits.time_limit
1845-
timelim_margin = timelim * safety_margin
1846-
if context.fixed_timelim is not None: # It's weird to set this both in limits and in context, but if so, context wins
1847-
self.warning(
1848-
'There is a fixed time limit in problem.yaml, and you also provided one on the command line. Using command line.'
1849-
)
1850-
timelim = context.fixed_timelim
1851-
timelim_margin = timelim * safety_margin
1858+
if limits.time_limit is not None and context.fixed_timelim is not None:
1859+
self.warning('There is a fixed time limit in problem.yaml, and you provided one on command line. Using command line.')
18521860

18531861
for verdict in Submissions._VERDICTS:
18541862
acr = verdict[0]
@@ -1873,44 +1881,32 @@ def check(self, context: Context) -> bool:
18731881
self.error(f'Compile error for {acr} submission {sub}', additional_info=msg)
18741882
continue
18751883

1876-
res = self.check_submission(sub, context, acr, timelim, timelim_margin_lo, timelim_margin)
1884+
timelim, timelim_high = self._compute_time_limit(fixed_limit, lower_bound_runtime)
1885+
res = self.check_submission(sub, context, acr, timelim, timelim_high)
18771886
runtimes.append(res.runtime)
18781887

18791888
if acr == 'AC':
18801889
if len(runtimes) > 0:
1881-
max_runtime = max(runtimes)
1882-
exact_timelim = max_runtime * time_multiplier
1883-
max_runtime_str = f'{max_runtime:.3f}'
1884-
timelim = math.ceil(exact_timelim / limits.time_resolution) * limits.time_resolution
1885-
timelim_margin = timelim * safety_margin
1886-
# timelim_margin_lo is a bit weird. We use it for partially_accepted, which we want to be roughly as fast as accepted
1887-
# solutions. Setting it to the rounded timelim / time_multiplier means that we check that none of the submissions we
1888-
# apply this to would have affected the timelim if we based it on them.
1889-
timelim_margin_lo = timelim / time_multiplier
1890-
else:
1891-
max_runtime_str = None
1890+
lower_bound_runtime = max(runtimes)
1891+
1892+
# Helper function to format numbers with at most 3 decimals and dealing with None
1893+
def _f_n(number: float | None) -> str:
1894+
return f'{round(number, 3):g}' if number is not None else '-'
18921895

1893-
if limits.time_limit is not None:
1894-
if max_runtime > limits.time_limit / time_multiplier:
1896+
if fixed_limit is not None and lower_bound_runtime is not None:
1897+
if lower_bound_runtime * ac_to_time_limit > fixed_limit:
18951898
self.error(
1896-
f'Time limit set to {limits.time_limit}, but slowest AC runs in {max_runtime_str} which is within a factor {time_multiplier}.'
1899+
f'Time limit fixed to {_f_n(fixed_limit)}, but slowest AC runs in {_f_n(lower_bound_runtime)} which is within a factor {_f_n(ac_to_time_limit)}.'
18971900
)
1898-
if not math.isclose(limits.time_limit, timelim):
1901+
tl_from_subs, _ = self._compute_time_limit(None, lower_bound_runtime)
1902+
if not math.isclose(fixed_limit, tl_from_subs):
18991903
self.msg(
1900-
f' Solutions give timelim of {timelim} seconds, but will use provided fixed limit of {limits.time_limit} seconds instead'
1904+
f' Solutions give timelim of {_f_n(tl_from_subs)} seconds, but will use provided fixed limit of {_f_n(fixed_limit)} seconds instead'
19011905
)
1902-
timelim = limits.time_limit
1903-
timelim_margin = timelim * safety_margin
1904-
1905-
if context.fixed_timelim is not None and not math.isclose(context.fixed_timelim, timelim):
1906-
self.msg(
1907-
f' Solutions give timelim of {timelim} seconds, but will use provided fixed limit of {context.fixed_timelim} seconds instead'
1908-
)
1909-
timelim = context.fixed_timelim
1910-
timelim_margin = timelim * safety_margin
19111906

1907+
timelim, timelim_margin = self._compute_time_limit(fixed_limit, lower_bound_runtime)
19121908
self.msg(
1913-
f' Slowest AC runtime: {max_runtime_str}, setting timelim to {timelim} secs, safety margin to {timelim_margin} secs'
1909+
f' Slowest AC runtime: {_f_n(lower_bound_runtime)}, setting timelim to {_f_n(timelim)} secs, safety margin to {_f_n(timelim_margin)} secs'
19141910
)
19151911
self.problem._set_timelim(timelim)
19161912

0 commit comments

Comments
 (0)