agentpace 0.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentpace/__init__.py +3 -0
- agentpace/__main__.py +8 -0
- agentpace/_bundled/DATA-LICENSE +121 -0
- agentpace/_bundled/example-plan.json +11 -0
- agentpace/_bundled/run.schema.json +100 -0
- agentpace/_bundled/runs/4546788e7930eccd-2026-W40-2026-W45.jsonl +3219 -0
- agentpace/_bundled/runs/4546788e7930eccd-2026-W44-2026-W45.jsonl +211 -0
- agentpace/_bundled/turn.schema.json +30 -0
- agentpace/adapters/__init__.py +31 -0
- agentpace/adapters/base.py +74 -0
- agentpace/adapters/claude_code.py +265 -0
- agentpace/adapters/cline.py +449 -0
- agentpace/adapters/codex.py +293 -0
- agentpace/adapters/gemini_cli.py +332 -0
- agentpace/adapters/opencode.py +397 -0
- agentpace/backtest.py +801 -0
- agentpace/cache.py +115 -0
- agentpace/charts.py +191 -0
- agentpace/claims.py +164 -0
- agentpace/classify.py +54 -0
- agentpace/cli.py +754 -0
- agentpace/estimate.py +694 -0
- agentpace/forecasts.py +284 -0
- agentpace/ingest.py +337 -0
- agentpace/paths.py +62 -0
- agentpace/pseudonym.py +51 -0
- agentpace/py.typed +0 -0
- agentpace/render.py +152 -0
- agentpace/report.py +502 -0
- agentpace/schema.py +131 -0
- agentpace/share.py +130 -0
- agentpace/site.py +133 -0
- agentpace/turns.py +282 -0
- agentpace/validate.py +115 -0
- agentpace-0.9.2.dist-info/METADATA +324 -0
- agentpace-0.9.2.dist-info/RECORD +39 -0
- agentpace-0.9.2.dist-info/WHEEL +4 -0
- agentpace-0.9.2.dist-info/entry_points.txt +2 -0
- agentpace-0.9.2.dist-info/licenses/LICENSE +21 -0
agentpace/backtest.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
"""Honest evaluation: every forecast is made from strictly earlier or separate data.
|
|
2
|
+
|
|
3
|
+
Whole sessions move between train and test, because rows inside one session are far from
|
|
4
|
+
independent. A random row split would leak them across the line, so it is deliberately absent.
|
|
5
|
+
|
|
6
|
+
Two tracks exist because stored `size` is measured after a run. The pre-run track is the real
|
|
7
|
+
forecast and the headline. The oracle-size track is an upper bound and is labeled as one.
|
|
8
|
+
|
|
9
|
+
The default method is chosen by a rule fixed before any result was seen: the lowest
|
|
10
|
+
session-weighted pinball loss on the time split, with ties inside the bootstrap interval going to
|
|
11
|
+
the simpler candidate. Every other number is reported whether it flatters the project or not.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import random
|
|
18
|
+
import statistics
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from collections.abc import Callable, Sequence
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from agentpace import estimate as est
|
|
25
|
+
from agentpace.schema import Row
|
|
26
|
+
|
|
27
|
+
MIN_TRAIN_ROWS = 200
|
|
28
|
+
MIN_PROJECT_ROWS = 30
|
|
29
|
+
CELL_DRAWS = 4000
|
|
30
|
+
PLAN_DRAWS = 1000
|
|
31
|
+
BOOTSTRAPS = 2000
|
|
32
|
+
SEED = 20261107
|
|
33
|
+
MINUTES_PER_ROUND = 3
|
|
34
|
+
|
|
35
|
+
# Frozen. Selection uses time folds up to this week and never re-runs on later ones. Later weeks
|
|
36
|
+
# form the locked holdout, which scores the shipped method on data it was never chosen on.
|
|
37
|
+
SELECTION_LAST_WEEK = "2026-W45"
|
|
38
|
+
# Registered on 2026-11-07, after a local run with Codex rows pooled in had been seen (DESIGN
|
|
39
|
+
# section 11). Selection and the headline numbers score only this harness, the population the
|
|
40
|
+
# rule was frozen on. Every harness still trains the estimator and is reported on its own.
|
|
41
|
+
SELECTION_HARNESS = "claude-code"
|
|
42
|
+
LOCKED_MIN_SESSIONS = 20 # sessions with at least est.MIN_SESSION_RUNS completed runs
|
|
43
|
+
|
|
44
|
+
# Assumptions, not measurements. They stand in for what a person might guess.
|
|
45
|
+
HUMAN_HOURS = {
|
|
46
|
+
"bugfix": 4.0,
|
|
47
|
+
"docs": 2.0,
|
|
48
|
+
"feature": 8.0,
|
|
49
|
+
"migration": 16.0,
|
|
50
|
+
"other": 2.0,
|
|
51
|
+
"refactor": 6.0,
|
|
52
|
+
"review": 1.0,
|
|
53
|
+
"tests": 3.0,
|
|
54
|
+
}
|
|
55
|
+
ROUNDS = {
|
|
56
|
+
"bugfix": 4,
|
|
57
|
+
"docs": 2,
|
|
58
|
+
"feature": 8,
|
|
59
|
+
"migration": 12,
|
|
60
|
+
"other": 3,
|
|
61
|
+
"refactor": 8,
|
|
62
|
+
"review": 2,
|
|
63
|
+
"tests": 4,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
BASELINES = ("human-guess", "fixed-rounds", "rounds-oracle", "global-median")
|
|
67
|
+
ERROR_METRICS = ("mape", "mdape", "male", "pinball")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class Forecast:
|
|
72
|
+
p50: float
|
|
73
|
+
p80: float
|
|
74
|
+
p90: float
|
|
75
|
+
actual: float
|
|
76
|
+
session: str
|
|
77
|
+
harness: str = "claude-code"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Fold:
|
|
82
|
+
name: str
|
|
83
|
+
train: list[Row]
|
|
84
|
+
test: list[Row]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _completed(rows: Sequence[Row]) -> list[Row]:
|
|
88
|
+
return [row for row in rows if row["outcome"] == "completed"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _sessions(rows: Sequence[Row]) -> dict[str, list[Row]]:
|
|
92
|
+
groups: dict[str, list[Row]] = defaultdict(list)
|
|
93
|
+
for row in rows:
|
|
94
|
+
groups[row["batch"]].append(row)
|
|
95
|
+
return groups
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def session_weeks(rows: Sequence[Row]) -> dict[str, str]:
|
|
99
|
+
"""A session belongs to the week of its first run, so it never straddles a split."""
|
|
100
|
+
return {
|
|
101
|
+
batch: min(members, key=lambda r: (r["offset_s"], r["week"]))["week"]
|
|
102
|
+
for batch, members in _sessions(rows).items()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def time_folds(rows: Sequence[Row]) -> list[Fold]:
|
|
107
|
+
"""Rolling origin: train on all earlier weeks, test on one week."""
|
|
108
|
+
weeks = session_weeks(rows)
|
|
109
|
+
folds = []
|
|
110
|
+
for week in sorted(set(weeks.values())):
|
|
111
|
+
train = [row for row in rows if weeks[row["batch"]] < week]
|
|
112
|
+
test = [row for row in rows if weeks[row["batch"]] == week]
|
|
113
|
+
if len(_completed(train)) >= MIN_TRAIN_ROWS and _completed(test):
|
|
114
|
+
folds.append(Fold(week, train, test))
|
|
115
|
+
return folds
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def group_folds(rows: Sequence[Row], field: str, minimum: int) -> list[Fold]:
|
|
119
|
+
"""Leave one group out."""
|
|
120
|
+
groups = sorted({row[field] for row in rows if field in row})
|
|
121
|
+
folds = []
|
|
122
|
+
for group in groups:
|
|
123
|
+
test = [row for row in rows if row.get(field) == group]
|
|
124
|
+
train = [row for row in rows if row.get(field) != group]
|
|
125
|
+
if len(test) >= minimum and len(_completed(train)) >= MIN_TRAIN_ROWS:
|
|
126
|
+
folds.append(Fold(group, train, test))
|
|
127
|
+
return folds
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _candidate(method: str, use_size: bool) -> Callable[[Fold, random.Random], list[Forecast]]:
|
|
131
|
+
def predict(fold: Fold, rng: random.Random) -> list[Forecast]:
|
|
132
|
+
model = est.fit(fold.train, method, use_size)
|
|
133
|
+
cache: dict[tuple[str, ...], tuple[float, float, float]] = {}
|
|
134
|
+
forecasts = []
|
|
135
|
+
for row in _completed(fold.test):
|
|
136
|
+
query = {name: row[name] for name in ("task_kind", "risk", "model", "size")}
|
|
137
|
+
key = tuple(query.values())
|
|
138
|
+
if key not in cache:
|
|
139
|
+
cache[key] = est.cell_quantiles(model, query, CELL_DRAWS, rng)
|
|
140
|
+
p50, p80, p90 = cache[key]
|
|
141
|
+
forecasts.append(
|
|
142
|
+
Forecast(p50, p80, p90, row["duration_s"], row["batch"], row["harness"])
|
|
143
|
+
)
|
|
144
|
+
return forecasts
|
|
145
|
+
|
|
146
|
+
return predict
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _point(guess: Callable[[Row, float], float]) -> Callable[[Fold, random.Random], list[Forecast]]:
|
|
150
|
+
"""A single-number baseline. The same number stands in for every quantile."""
|
|
151
|
+
|
|
152
|
+
def predict(fold: Fold, _: random.Random) -> list[Forecast]:
|
|
153
|
+
ordered = sorted(row["duration_s"] for row in _completed(fold.train))
|
|
154
|
+
median = est.quantile(ordered, 0.5)
|
|
155
|
+
forecasts = []
|
|
156
|
+
for row in _completed(fold.test):
|
|
157
|
+
value = guess(row, median)
|
|
158
|
+
forecasts.append(
|
|
159
|
+
Forecast(value, value, value, row["duration_s"], row["batch"], row["harness"])
|
|
160
|
+
)
|
|
161
|
+
return forecasts
|
|
162
|
+
|
|
163
|
+
return predict
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _baselines() -> dict[str, Callable[[Fold, random.Random], list[Forecast]]]:
|
|
167
|
+
return {
|
|
168
|
+
"human-guess": _point(lambda row, _: HUMAN_HOURS[row["task_kind"]] * 3600),
|
|
169
|
+
"fixed-rounds": _point(lambda row, _: ROUNDS[row["task_kind"]] * MINUTES_PER_ROUND * 60.0),
|
|
170
|
+
"rounds-oracle": _point(
|
|
171
|
+
lambda row, _: max(row["tool_calls"], 1) * MINUTES_PER_ROUND * 60.0
|
|
172
|
+
),
|
|
173
|
+
"global-median": _point(lambda _, median: median),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _pinball(forecast: Forecast, predicted: float, q: float) -> float:
|
|
178
|
+
gap = forecast.actual - predicted
|
|
179
|
+
return max(q * gap, (q - 1) * gap)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def pinball_sum(forecast: Forecast) -> float:
|
|
183
|
+
return (
|
|
184
|
+
_pinball(forecast, forecast.p50, 0.5)
|
|
185
|
+
+ _pinball(forecast, forecast.p80, 0.8)
|
|
186
|
+
+ _pinball(forecast, forecast.p90, 0.9)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _weighted_median(pairs: list[tuple[float, float]]) -> float:
|
|
191
|
+
pairs = sorted(pairs)
|
|
192
|
+
half = sum(weight for _, weight in pairs) / 2
|
|
193
|
+
running = 0.0
|
|
194
|
+
for value, weight in pairs:
|
|
195
|
+
running += weight
|
|
196
|
+
if running >= half:
|
|
197
|
+
return value
|
|
198
|
+
return pairs[-1][0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def metrics(forecasts: Sequence[Forecast], weights: Sequence[float]) -> dict[str, float]:
|
|
202
|
+
"""Accuracy, calibration and sharpness. Width sits beside coverage, because a method can
|
|
203
|
+
earn perfect coverage with absurdly wide ranges."""
|
|
204
|
+
total = sum(weights)
|
|
205
|
+
|
|
206
|
+
def mean(values: Sequence[float]) -> float:
|
|
207
|
+
return sum(v * w for v, w in zip(values, weights, strict=True)) / total
|
|
208
|
+
|
|
209
|
+
percentage = [abs(f.p50 - f.actual) / f.actual for f in forecasts]
|
|
210
|
+
return {
|
|
211
|
+
"mape": 100 * mean(percentage),
|
|
212
|
+
"mdape": 100 * _weighted_median(list(zip(percentage, weights, strict=True))),
|
|
213
|
+
"male": mean([abs(math.log(f.p50 / f.actual)) for f in forecasts]),
|
|
214
|
+
"coverage50": 100 * mean([float(f.actual <= f.p50) for f in forecasts]),
|
|
215
|
+
"coverage80": 100 * mean([float(f.actual <= f.p80) for f in forecasts]),
|
|
216
|
+
"coverage90": 100 * mean([float(f.actual <= f.p90) for f in forecasts]),
|
|
217
|
+
"pinball": mean([pinball_sum(f) for f in forecasts]),
|
|
218
|
+
"width": _weighted_median(
|
|
219
|
+
[(f.p90 / f.p50, w) for f, w in zip(forecasts, weights, strict=True)]
|
|
220
|
+
),
|
|
221
|
+
# Added in 0.2.1 for the portable prompt. The same forecasts, no new random draw.
|
|
222
|
+
"width80": _weighted_median(
|
|
223
|
+
[(f.p80 / f.p50, w) for f, w in zip(forecasts, weights, strict=True)]
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def both_weightings(forecasts: Sequence[Forecast]) -> dict[str, dict[str, float]]:
|
|
229
|
+
sizes: dict[str, int] = defaultdict(int)
|
|
230
|
+
for forecast in forecasts:
|
|
231
|
+
sizes[forecast.session] += 1
|
|
232
|
+
return {
|
|
233
|
+
"row": metrics(forecasts, [1.0] * len(forecasts)),
|
|
234
|
+
"session": metrics(forecasts, [1.0 / sizes[f.session] for f in forecasts]),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def session_means(
|
|
239
|
+
forecasts: Sequence[Forecast], value: Callable[[Forecast], float]
|
|
240
|
+
) -> dict[str, float]:
|
|
241
|
+
"""One value per session, so a session-weighted metric is the plain mean of these."""
|
|
242
|
+
groups: dict[str, list[float]] = defaultdict(list)
|
|
243
|
+
for forecast in forecasts:
|
|
244
|
+
groups[forecast.session].append(value(forecast))
|
|
245
|
+
return {session: statistics.fmean(values) for session, values in groups.items()}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def session_losses(forecasts: Sequence[Forecast]) -> dict[str, float]:
|
|
249
|
+
"""The primary metric, one value per session."""
|
|
250
|
+
return session_means(forecasts, pinball_sum)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _covered(level: str) -> Callable[[Forecast], float]:
|
|
254
|
+
return lambda f: 100.0 * (f.actual <= getattr(f, level))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def difference_interval(
|
|
258
|
+
a: dict[str, float], b: dict[str, float], rng: random.Random
|
|
259
|
+
) -> tuple[float, float]:
|
|
260
|
+
"""95% cluster bootstrap interval for mean(a) - mean(b), resampling whole sessions."""
|
|
261
|
+
sessions = sorted(a)
|
|
262
|
+
gaps = [a[session] - b[session] for session in sessions]
|
|
263
|
+
means = sorted(statistics.fmean(rng.choices(gaps, k=len(gaps))) for _ in range(BOOTSTRAPS))
|
|
264
|
+
return est.quantile(means, 0.025), est.quantile(means, 0.975)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def mean_interval(values: dict[str, float], rng: random.Random) -> list[float]:
|
|
268
|
+
"""95% cluster bootstrap interval for the mean of one value per session."""
|
|
269
|
+
return list(difference_interval(values, dict.fromkeys(values, 0.0), rng))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def intervals(
|
|
273
|
+
forecasts: dict[str, list[Forecast]], winner: str, rivals: Sequence[str], rng: random.Random
|
|
274
|
+
) -> dict[str, Any]:
|
|
275
|
+
"""Session-bootstrap intervals for the shipped method's numbers, and for each rival's
|
|
276
|
+
pinball loss minus ours. A gap interval above zero means the rival really is worse."""
|
|
277
|
+
ours = forecasts[winner]
|
|
278
|
+
losses = session_losses(ours)
|
|
279
|
+
result: dict[str, Any] = {
|
|
280
|
+
level: mean_interval(session_means(ours, _covered(f"p{level[-2:]}")), rng)
|
|
281
|
+
for level in ("coverage50", "coverage80", "coverage90")
|
|
282
|
+
}
|
|
283
|
+
result["pinball"] = mean_interval(losses, rng)
|
|
284
|
+
result["rival_minus_ours"] = {
|
|
285
|
+
name: list(difference_interval(session_losses(forecasts[name]), losses, rng))
|
|
286
|
+
for name in rivals
|
|
287
|
+
}
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def select(forecasts: dict[str, list[Forecast]], rng: random.Random) -> dict[str, Any]:
|
|
292
|
+
"""Apply the pre-registered rule. `est.METHODS` is ordered simplest first."""
|
|
293
|
+
losses = {method.name: session_losses(forecasts[method.name]) for method in est.METHODS}
|
|
294
|
+
means = {name: statistics.fmean(values.values()) for name, values in losses.items()}
|
|
295
|
+
best = min(means, key=lambda name: means[name])
|
|
296
|
+
intervals = {name: difference_interval(losses[name], losses[best], rng) for name in means}
|
|
297
|
+
winner = next(
|
|
298
|
+
name for name in means if name == best or intervals[name][0] <= 0 <= intervals[name][1]
|
|
299
|
+
)
|
|
300
|
+
return {
|
|
301
|
+
"primary_metric": "session-weighted pinball loss, summed over 0.5, 0.8 and 0.9",
|
|
302
|
+
"loss": means,
|
|
303
|
+
"difference_from_best": {name: list(intervals[name]) for name in means},
|
|
304
|
+
"lowest_loss": best,
|
|
305
|
+
"winner": winner,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _forecast_folds(
|
|
310
|
+
folds: Sequence[Fold], use_size: bool, seed: int
|
|
311
|
+
) -> dict[str, list[list[Forecast]]]:
|
|
312
|
+
"""Forecasts per method and per fold. Each method draws from one stream across the folds in
|
|
313
|
+
order, so appending later folds never changes the forecasts for earlier ones."""
|
|
314
|
+
predictors = {m.name: _candidate(m.name, use_size) for m in est.METHODS}
|
|
315
|
+
predictors.update(_baselines())
|
|
316
|
+
forecasts: dict[str, list[list[Forecast]]] = {}
|
|
317
|
+
for name, predict in predictors.items():
|
|
318
|
+
rng = random.Random(f"{seed}:{name}:{use_size}")
|
|
319
|
+
forecasts[name] = [predict(fold, rng) for fold in folds]
|
|
320
|
+
return forecasts
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _pooled(per_fold: dict[str, list[list[Forecast]]], picked: range) -> dict[str, list[Forecast]]:
|
|
324
|
+
return {name: [item for i in picked for item in folds[i]] for name, folds in per_fold.items()}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _evaluate(
|
|
328
|
+
folds: Sequence[Fold], use_size: bool, seed: int
|
|
329
|
+
) -> tuple[dict[str, list[Forecast]], dict[str, dict[str, dict[str, float]]]]:
|
|
330
|
+
forecasts = _pooled(_forecast_folds(folds, use_size, seed), range(len(folds)))
|
|
331
|
+
return forecasts, {name: both_weightings(items) for name, items in forecasts.items()}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def by_week(
|
|
335
|
+
folds: Sequence[Fold], per_fold: dict[str, list[list[Forecast]]], winner: str, last: str
|
|
336
|
+
) -> list[dict[str, Any]]:
|
|
337
|
+
"""The shipped method fold by fold, because a pooled coverage can hide weeks that miss in
|
|
338
|
+
both directions."""
|
|
339
|
+
weeks = []
|
|
340
|
+
for i, fold in enumerate(folds):
|
|
341
|
+
if not per_fold[winner][i]:
|
|
342
|
+
continue # a later week may hold no session of the selection harness
|
|
343
|
+
ours = both_weightings(per_fold[winner][i])
|
|
344
|
+
median = both_weightings(per_fold["global-median"][i])["session"]
|
|
345
|
+
weeks.append(
|
|
346
|
+
{
|
|
347
|
+
"week": fold.name,
|
|
348
|
+
"period": "selection" if fold.name <= last else "locked",
|
|
349
|
+
"sessions": len({f.session for f in per_fold[winner][i]}),
|
|
350
|
+
"rows": len(per_fold[winner][i]),
|
|
351
|
+
"coverage80_session": ours["session"]["coverage80"],
|
|
352
|
+
"coverage80_row": ours["row"]["coverage80"],
|
|
353
|
+
"width": ours["session"]["width"],
|
|
354
|
+
"pinball": ours["session"]["pinball"],
|
|
355
|
+
"median_pinball": median["pinball"],
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
return weeks
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def plan_forecasts(
|
|
362
|
+
folds: Sequence[Fold], method: str, seed: int, harness: str | None = None
|
|
363
|
+
) -> dict[str, list[Forecast]]:
|
|
364
|
+
"""Treat each held-out session as a plan and predict how long it took, end to end.
|
|
365
|
+
|
|
366
|
+
Real dependencies are not recorded, so the critical path falls back to the longest run, and
|
|
367
|
+
the observed peak concurrency stands in for the cap. That peak is itself an outcome, so this
|
|
368
|
+
tests the throughput and session terms, not dependency planning.
|
|
369
|
+
|
|
370
|
+
With `harness`, only that harness's sessions are scored. Training keeps every row.
|
|
371
|
+
"""
|
|
372
|
+
ours: list[Forecast] = []
|
|
373
|
+
naive: list[Forecast] = []
|
|
374
|
+
for fold in folds:
|
|
375
|
+
model = est.fit(fold.train, method)
|
|
376
|
+
median = est.quantile(sorted(r["duration_s"] for r in _completed(fold.train)), 0.5)
|
|
377
|
+
for batch, members in sorted(_sessions(_completed(fold.test)).items()):
|
|
378
|
+
if len(members) < est.MIN_SESSION_RUNS:
|
|
379
|
+
continue
|
|
380
|
+
if harness is not None and members[0]["harness"] != harness:
|
|
381
|
+
continue
|
|
382
|
+
tasks = [
|
|
383
|
+
est.Task(str(i), row["task_kind"], row["risk"], model=row["model"])
|
|
384
|
+
for i, row in enumerate(members)
|
|
385
|
+
]
|
|
386
|
+
cap = max(row["parallel_agents"] for row in members)
|
|
387
|
+
plan = est.estimate(model, tasks, cap, PLAN_DRAWS, seed)
|
|
388
|
+
first = min(row["offset_s"] for row in members)
|
|
389
|
+
actual = max(row["offset_s"] + row["duration_s"] for row in members) - first
|
|
390
|
+
actual = max(actual, 1)
|
|
391
|
+
ours.append(Forecast(plan.p50, plan.p80, plan.p90, actual, batch))
|
|
392
|
+
guess = median * len(members) / cap
|
|
393
|
+
naive.append(Forecast(guess, guess, guess, actual, batch))
|
|
394
|
+
return {"agentpace": ours, "sum-of-medians": naive}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _losses(
|
|
398
|
+
scheme: str, track: str, results: dict[str, dict[str, dict[str, float]]], winner: str
|
|
399
|
+
) -> list[dict[str, Any]]:
|
|
400
|
+
"""Every place a baseline beat the shipped method. Generated, so it cannot be edited out."""
|
|
401
|
+
entries = []
|
|
402
|
+
ours = results[winner]
|
|
403
|
+
for weighting in ("row", "session"):
|
|
404
|
+
for baseline in BASELINES:
|
|
405
|
+
theirs = results[baseline][weighting]
|
|
406
|
+
for metric in ERROR_METRICS:
|
|
407
|
+
if theirs[metric] < ours[weighting][metric]:
|
|
408
|
+
entries.append(
|
|
409
|
+
{
|
|
410
|
+
"split": scheme,
|
|
411
|
+
"track": track,
|
|
412
|
+
"weighting": weighting,
|
|
413
|
+
"metric": metric,
|
|
414
|
+
"baseline": baseline,
|
|
415
|
+
"baseline_value": theirs[metric],
|
|
416
|
+
"agentpace_value": ours[weighting][metric],
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
gap_theirs = abs(theirs["coverage80"] - 80)
|
|
420
|
+
gap_ours = abs(ours[weighting]["coverage80"] - 80)
|
|
421
|
+
if gap_theirs < gap_ours:
|
|
422
|
+
entries.append(
|
|
423
|
+
{
|
|
424
|
+
"split": scheme,
|
|
425
|
+
"track": track,
|
|
426
|
+
"weighting": weighting,
|
|
427
|
+
"metric": "coverage80",
|
|
428
|
+
"baseline": baseline,
|
|
429
|
+
"baseline_value": theirs["coverage80"],
|
|
430
|
+
"agentpace_value": ours[weighting]["coverage80"],
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
return entries
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def dataset_summary(rows: Sequence[Row]) -> dict[str, Any]:
|
|
437
|
+
completed = _completed(rows)
|
|
438
|
+
sessions = _sessions(rows)
|
|
439
|
+
largest = sorted((len(members) for members in sessions.values()), reverse=True)
|
|
440
|
+
project_sizes: dict[str, int] = defaultdict(int)
|
|
441
|
+
for row in rows:
|
|
442
|
+
project_sizes[row.get("project", "")] += 1
|
|
443
|
+
return {
|
|
444
|
+
"rows": len(rows),
|
|
445
|
+
"completed": len(completed),
|
|
446
|
+
"contributors": len({row["contributor"] for row in rows}),
|
|
447
|
+
"projects": len({row["project"] for row in rows if "project" in row}),
|
|
448
|
+
"sessions": len(sessions),
|
|
449
|
+
"weeks": sorted({row["week"] for row in rows}),
|
|
450
|
+
"top_project_share": 100 * max(project_sizes.values()) / len(rows),
|
|
451
|
+
"top_three_session_share": 100 * sum(largest[:3]) / len(rows),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def harness_summary(
|
|
456
|
+
rows: Sequence[Row], forecasts: dict[str, list[Forecast]], winner: str
|
|
457
|
+
) -> dict[str, Any]:
|
|
458
|
+
"""Each harness on its own, from forecasts already drawn, so no random number is used.
|
|
459
|
+
|
|
460
|
+
The estimator does not use the harness. This shows how each one fares before anyone
|
|
461
|
+
pools them, which is the check a harness term would have to pass."""
|
|
462
|
+
completed = _completed(rows)
|
|
463
|
+
dataset = {
|
|
464
|
+
harness: {
|
|
465
|
+
"rows": sum(row["harness"] == harness for row in rows),
|
|
466
|
+
"completed": sum(row["harness"] == harness for row in completed),
|
|
467
|
+
"sessions": len({row["batch"] for row in rows if row["harness"] == harness}),
|
|
468
|
+
}
|
|
469
|
+
for harness in sorted({row["harness"] for row in rows})
|
|
470
|
+
}
|
|
471
|
+
selection: dict[str, Any] = {}
|
|
472
|
+
for harness in dataset:
|
|
473
|
+
ours = [f for f in forecasts[winner] if f.harness == harness]
|
|
474
|
+
median = [f for f in forecasts["global-median"] if f.harness == harness]
|
|
475
|
+
if not ours:
|
|
476
|
+
continue
|
|
477
|
+
mine, theirs = both_weightings(ours), both_weightings(median)
|
|
478
|
+
selection[harness] = {
|
|
479
|
+
"sessions": len({f.session for f in ours}),
|
|
480
|
+
"rows": len(ours),
|
|
481
|
+
"pinball": mine["session"]["pinball"],
|
|
482
|
+
"median_pinball": theirs["session"]["pinball"],
|
|
483
|
+
"coverage80_session": mine["session"]["coverage80"],
|
|
484
|
+
"coverage80_row": mine["row"]["coverage80"],
|
|
485
|
+
"width": mine["session"]["width"],
|
|
486
|
+
}
|
|
487
|
+
return {"dataset": dataset, "selection_folds": selection}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def harness_methods(forecasts: dict[str, list[Forecast]]) -> dict[str, Any]:
|
|
491
|
+
"""Every candidate and the global median, per harness, at task level. The forward test
|
|
492
|
+
for a harness the selection does not score: from forecasts already drawn, so no random
|
|
493
|
+
number is used."""
|
|
494
|
+
names = [method.name for method in est.METHODS] + ["global-median"]
|
|
495
|
+
result: dict[str, Any] = {}
|
|
496
|
+
for harness in sorted({f.harness for f in forecasts[names[0]]}):
|
|
497
|
+
scores = {}
|
|
498
|
+
for name in names:
|
|
499
|
+
items = [f for f in forecasts[name] if f.harness == harness]
|
|
500
|
+
found = both_weightings(items)["session"]
|
|
501
|
+
scores[name] = {
|
|
502
|
+
"pinball": found["pinball"],
|
|
503
|
+
"coverage80": found["coverage80"],
|
|
504
|
+
"width": found["width"],
|
|
505
|
+
}
|
|
506
|
+
items = [f for f in forecasts[names[0]] if f.harness == harness]
|
|
507
|
+
result[harness] = {
|
|
508
|
+
"sessions": len({f.session for f in items}),
|
|
509
|
+
"rows": len(items),
|
|
510
|
+
"methods": scores,
|
|
511
|
+
}
|
|
512
|
+
return result
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _project_order(
|
|
516
|
+
folds: Sequence[Fold], per_fold: dict[str, list[list[Forecast]]], winner: str
|
|
517
|
+
) -> list[int]:
|
|
518
|
+
"""Fold indexes, most held-out sessions first, then most rows, then pseudonym, so the
|
|
519
|
+
order never depends on how folds were listed. Folds with no completed run are left out."""
|
|
520
|
+
sizes = {
|
|
521
|
+
i: (len({f.session for f in per_fold[winner][i]}), len(per_fold[winner][i]), fold.name)
|
|
522
|
+
for i, fold in enumerate(folds)
|
|
523
|
+
if per_fold[winner][i]
|
|
524
|
+
}
|
|
525
|
+
return sorted(sizes, key=lambda i: (-sizes[i][0], -sizes[i][1], sizes[i][2]))
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def by_project(
|
|
529
|
+
folds: Sequence[Fold], per_fold: dict[str, list[list[Forecast]]], winner: str
|
|
530
|
+
) -> list[dict[str, Any]]:
|
|
531
|
+
"""Each held-out project on its own, from forecasts already drawn, so no random number is
|
|
532
|
+
used. Listed in rank order and never by pseudonym: the rank is the label."""
|
|
533
|
+
result = []
|
|
534
|
+
for i in _project_order(folds, per_fold, winner):
|
|
535
|
+
ours, median = per_fold[winner][i], per_fold["global-median"][i]
|
|
536
|
+
mine, theirs = both_weightings(ours), both_weightings(median)
|
|
537
|
+
result.append(
|
|
538
|
+
{
|
|
539
|
+
"harness": ", ".join(sorted({row["harness"] for row in folds[i].test})),
|
|
540
|
+
"sessions": len({f.session for f in ours}),
|
|
541
|
+
"rows": len(ours),
|
|
542
|
+
"pinball": mine["session"]["pinball"],
|
|
543
|
+
"median_pinball": theirs["session"]["pinball"],
|
|
544
|
+
"coverage80_session": mine["session"]["coverage80"],
|
|
545
|
+
"coverage80_row": mine["row"]["coverage80"],
|
|
546
|
+
"width": mine["session"]["width"],
|
|
547
|
+
}
|
|
548
|
+
)
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def without_largest(
|
|
553
|
+
folds: Sequence[Fold], per_fold: dict[str, list[list[Forecast]]], winner: str
|
|
554
|
+
) -> dict[str, Any]:
|
|
555
|
+
"""The project split's session-weighted pinball with and without the project holding the
|
|
556
|
+
most held-out sessions, so one project's vote is visible. From forecasts already drawn."""
|
|
557
|
+
order = _project_order(folds, per_fold, winner)
|
|
558
|
+
largest = order[0]
|
|
559
|
+
candidates = [method.name for method in est.METHODS]
|
|
560
|
+
names = [*candidates, "global-median"]
|
|
561
|
+
|
|
562
|
+
def loss(name: str, kept: Sequence[int]) -> float:
|
|
563
|
+
items = [f for i in kept for f in per_fold[name][i]]
|
|
564
|
+
return both_weightings(items)["session"]["pinball"]
|
|
565
|
+
|
|
566
|
+
rest = [i for i in order if i != largest]
|
|
567
|
+
without = {name: loss(name, rest) for name in names}
|
|
568
|
+
with_it = {name: loss(name, order) for name in candidates}
|
|
569
|
+
return {
|
|
570
|
+
"project": 1,
|
|
571
|
+
"harness": ", ".join(sorted({row["harness"] for row in folds[largest].test})),
|
|
572
|
+
"sessions": len({f.session for f in per_fold[winner][largest]}),
|
|
573
|
+
"of_sessions": len({f.session for i in order for f in per_fold[winner][i]}),
|
|
574
|
+
"loss": without,
|
|
575
|
+
"lowest": min(candidates, key=lambda name: without[name]),
|
|
576
|
+
"lowest_with_it": min(candidates, key=lambda name: with_it[name]),
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def project_rows(
|
|
581
|
+
folds: Sequence[Fold], per_fold: dict[str, list[list[Forecast]]], winner: str
|
|
582
|
+
) -> dict[str, Any]:
|
|
583
|
+
"""Each project alone and, when at least two have completed runs, the split without the
|
|
584
|
+
largest. With only one, dropping it would leave nothing to score."""
|
|
585
|
+
rows = by_project(folds, per_fold, winner)
|
|
586
|
+
result: dict[str, Any] = {"by_project": rows}
|
|
587
|
+
if len(rows) >= 2:
|
|
588
|
+
result["without_largest"] = without_largest(folds, per_fold, winner)
|
|
589
|
+
return result
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _locked_holdout(
|
|
593
|
+
locked: Sequence[Fold],
|
|
594
|
+
forecasts: dict[str, list[Forecast]],
|
|
595
|
+
winner: str,
|
|
596
|
+
seed: int,
|
|
597
|
+
harness: str | None = None,
|
|
598
|
+
everyone: dict[str, list[Forecast]] | None = None,
|
|
599
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
600
|
+
"""Score the shipped method on weeks after selection. Nothing here feeds back into it.
|
|
601
|
+
|
|
602
|
+
The headline scores `harness`, as selection does. `everyone` adds each harness at task
|
|
603
|
+
level, whatever the headline status, so a harness selection never scored is tested here."""
|
|
604
|
+
plans = plan_forecasts(locked, winner, seed, harness)
|
|
605
|
+
qualifying = len(plans["agentpace"])
|
|
606
|
+
base: dict[str, Any] = {
|
|
607
|
+
"folds": [fold.name for fold in locked],
|
|
608
|
+
"min_sessions": LOCKED_MIN_SESSIONS,
|
|
609
|
+
"qualifying_sessions": qualifying,
|
|
610
|
+
}
|
|
611
|
+
if everyone is not None and everyone[winner]:
|
|
612
|
+
base["by_harness"] = harness_methods(everyone)
|
|
613
|
+
if qualifying < LOCKED_MIN_SESSIONS:
|
|
614
|
+
status = (
|
|
615
|
+
f"not enough data ({qualifying} of {LOCKED_MIN_SESSIONS} sessions with "
|
|
616
|
+
f"{est.MIN_SESSION_RUNS}+ runs)"
|
|
617
|
+
)
|
|
618
|
+
return {**base, "status": status}, []
|
|
619
|
+
names = [winner, *BASELINES]
|
|
620
|
+
results = {name: both_weightings(forecasts[name]) for name in names}
|
|
621
|
+
rng = random.Random(f"{seed}:locked-intervals")
|
|
622
|
+
locked_result = {
|
|
623
|
+
**base,
|
|
624
|
+
"status": "ok",
|
|
625
|
+
"rows": len(forecasts[winner]),
|
|
626
|
+
"tracks": {"pre-run": results},
|
|
627
|
+
"intervals": intervals(forecasts, winner, BASELINES, rng),
|
|
628
|
+
"plans": {name: both_weightings(items)["row"] for name, items in plans.items()},
|
|
629
|
+
}
|
|
630
|
+
return locked_result, _losses("locked", "pre-run", results, winner)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _of_harness(forecasts: list[Forecast], harness: str | None) -> list[Forecast]:
|
|
634
|
+
return forecasts if harness is None else [f for f in forecasts if f.harness == harness]
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _pooled_selection(
|
|
638
|
+
everyone: dict[str, list[Forecast]], harnesses: Sequence[str], seed: int
|
|
639
|
+
) -> dict[str, Any]:
|
|
640
|
+
"""The same rule over every harness at once, printed for the record and never shipped.
|
|
641
|
+
Its own random stream, so it moves no other number."""
|
|
642
|
+
if len(harnesses) < 2:
|
|
643
|
+
return {"status": f"same as above ({len(harnesses)} harness)"}
|
|
644
|
+
result = select(everyone, random.Random(f"{seed}:pooled-selection"))
|
|
645
|
+
result["status"] = "ok"
|
|
646
|
+
result["baselines"] = {
|
|
647
|
+
name: both_weightings(everyone[name])["session"]["pinball"] for name in BASELINES
|
|
648
|
+
}
|
|
649
|
+
return result
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _plan_losses(split: str, results: dict[str, dict[str, float]]) -> list[dict[str, Any]]:
|
|
653
|
+
ours = results["agentpace"]
|
|
654
|
+
theirs = results["sum-of-medians"]
|
|
655
|
+
return [
|
|
656
|
+
{
|
|
657
|
+
"split": split,
|
|
658
|
+
"track": "whole sessions",
|
|
659
|
+
"weighting": "session",
|
|
660
|
+
"metric": metric,
|
|
661
|
+
"baseline": "sum-of-medians",
|
|
662
|
+
"baseline_value": theirs[metric],
|
|
663
|
+
"agentpace_value": ours[metric],
|
|
664
|
+
}
|
|
665
|
+
for metric in ("mape", "mdape", "male", "pinball")
|
|
666
|
+
if theirs[metric] < ours[metric]
|
|
667
|
+
]
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def run(
|
|
671
|
+
rows: Sequence[Row], seed: int = SEED, selection_last_week: str = SELECTION_LAST_WEEK
|
|
672
|
+
) -> dict[str, Any]:
|
|
673
|
+
"""Everything `docs/backtest.md` reports."""
|
|
674
|
+
usable = [dict(row) for row in rows]
|
|
675
|
+
for row in usable:
|
|
676
|
+
row.setdefault("project", "")
|
|
677
|
+
every_week = time_folds(usable)
|
|
678
|
+
time = [fold for fold in every_week if fold.name <= selection_last_week]
|
|
679
|
+
locked = [fold for fold in every_week if fold.name > selection_last_week]
|
|
680
|
+
if not time:
|
|
681
|
+
raise ValueError("not enough weeks of data for a time split")
|
|
682
|
+
harnesses = sorted({row["harness"] for row in rows})
|
|
683
|
+
# The population the rule was frozen on. A dataset without it, such as one person's Codex
|
|
684
|
+
# rows, has a single harness, and selection scores all of it.
|
|
685
|
+
population = SELECTION_HARNESS if SELECTION_HARNESS in harnesses else None
|
|
686
|
+
every_fold = _forecast_folds([*time, *locked], use_size=False, seed=seed)
|
|
687
|
+
per_fold = {
|
|
688
|
+
name: [_of_harness(fold, population) for fold in folds]
|
|
689
|
+
for name, folds in every_fold.items()
|
|
690
|
+
}
|
|
691
|
+
forecasts = _pooled(per_fold, range(len(time)))
|
|
692
|
+
everyone = _pooled(every_fold, range(len(time)))
|
|
693
|
+
pre_run = {name: both_weightings(items) for name, items in forecasts.items()}
|
|
694
|
+
selection = select(forecasts, random.Random(seed))
|
|
695
|
+
selection["last_week"] = selection_last_week
|
|
696
|
+
selection["harness"] = population or "all"
|
|
697
|
+
winner = selection["winner"]
|
|
698
|
+
oracle_forecasts, _ = _evaluate(time, use_size=True, seed=seed)
|
|
699
|
+
oracle = {
|
|
700
|
+
name: both_weightings(_of_harness(items, population))
|
|
701
|
+
for name, items in oracle_forecasts.items()
|
|
702
|
+
}
|
|
703
|
+
report: dict[str, Any] = {
|
|
704
|
+
"seed": seed,
|
|
705
|
+
"dataset": dataset_summary(rows),
|
|
706
|
+
"selection": selection,
|
|
707
|
+
"pooled_selection": _pooled_selection(everyone, harnesses, seed),
|
|
708
|
+
"splits": {
|
|
709
|
+
"time": {
|
|
710
|
+
"status": "ok",
|
|
711
|
+
"folds": [fold.name for fold in time],
|
|
712
|
+
"rows": len(forecasts[winner]),
|
|
713
|
+
"tracks": {"pre-run": pre_run, "oracle-size": oracle},
|
|
714
|
+
"intervals": intervals(
|
|
715
|
+
forecasts, winner, BASELINES, random.Random(f"{seed}:intervals")
|
|
716
|
+
),
|
|
717
|
+
"by_week": by_week([*time, *locked], per_fold, winner, selection_last_week),
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
}
|
|
721
|
+
report_losses = _losses("time", "pre-run", pre_run, winner)
|
|
722
|
+
report_losses += _losses("time", "oracle-size", oracle, winner)
|
|
723
|
+
if locked:
|
|
724
|
+
later = range(len(time), len(time) + len(locked))
|
|
725
|
+
report["locked"], locked_losses = _locked_holdout(
|
|
726
|
+
locked,
|
|
727
|
+
_pooled(per_fold, later),
|
|
728
|
+
winner,
|
|
729
|
+
seed,
|
|
730
|
+
population,
|
|
731
|
+
_pooled(every_fold, later),
|
|
732
|
+
)
|
|
733
|
+
else:
|
|
734
|
+
report["locked"] = {
|
|
735
|
+
"status": f"not enough data (no weeks after {selection_last_week} yet)",
|
|
736
|
+
"folds": [],
|
|
737
|
+
"min_sessions": LOCKED_MIN_SESSIONS,
|
|
738
|
+
"qualifying_sessions": 0,
|
|
739
|
+
}
|
|
740
|
+
locked_losses = []
|
|
741
|
+
|
|
742
|
+
projects = group_folds(usable, "project", MIN_PROJECT_ROWS)
|
|
743
|
+
projects = [fold for fold in projects if fold.name]
|
|
744
|
+
if len(projects) >= 2:
|
|
745
|
+
# The same draws `_evaluate` makes, kept per fold so each project can be shown alone.
|
|
746
|
+
per_project = _forecast_folds(projects, use_size=False, seed=seed)
|
|
747
|
+
held_out = _pooled(per_project, range(len(projects)))
|
|
748
|
+
project_results = {name: both_weightings(items) for name, items in held_out.items()}
|
|
749
|
+
report["splits"]["project"] = {
|
|
750
|
+
"status": "ok",
|
|
751
|
+
"folds": len(projects),
|
|
752
|
+
"rows": len(held_out[winner]),
|
|
753
|
+
"tracks": {"pre-run": project_results},
|
|
754
|
+
**project_rows(projects, per_project, winner),
|
|
755
|
+
}
|
|
756
|
+
report_losses += _losses("project", "pre-run", project_results, winner)
|
|
757
|
+
else:
|
|
758
|
+
report["splits"]["project"] = {"status": "could not run (fewer than 2 usable projects)"}
|
|
759
|
+
|
|
760
|
+
contributors = len({row["contributor"] for row in rows})
|
|
761
|
+
if contributors >= 2:
|
|
762
|
+
groups = group_folds(usable, "contributor", 1)
|
|
763
|
+
held_out, contributor_results = _evaluate(groups, use_size=False, seed=seed)
|
|
764
|
+
report["splits"]["contributor"] = {
|
|
765
|
+
"status": "ok",
|
|
766
|
+
"folds": len(groups),
|
|
767
|
+
"rows": len(held_out[winner]),
|
|
768
|
+
"tracks": {"pre-run": contributor_results},
|
|
769
|
+
}
|
|
770
|
+
report_losses += _losses("contributor", "pre-run", contributor_results, winner)
|
|
771
|
+
else:
|
|
772
|
+
report["splits"]["contributor"] = {"status": f"could not run ({contributors} contributor)"}
|
|
773
|
+
|
|
774
|
+
if len(harnesses) >= 2:
|
|
775
|
+
groups = group_folds(usable, "harness", 1)
|
|
776
|
+
held_out, harness_results = _evaluate(groups, use_size=False, seed=seed)
|
|
777
|
+
report["splits"]["harness"] = {
|
|
778
|
+
"status": "ok",
|
|
779
|
+
"folds": len(groups),
|
|
780
|
+
"rows": len(held_out[winner]),
|
|
781
|
+
"tracks": {"pre-run": harness_results},
|
|
782
|
+
}
|
|
783
|
+
report_losses += _losses("harness", "pre-run", harness_results, winner)
|
|
784
|
+
else:
|
|
785
|
+
report["splits"]["harness"] = {"status": f"could not run ({len(harnesses)} harness)"}
|
|
786
|
+
report["harness"] = harness_summary(rows, everyone, winner)
|
|
787
|
+
|
|
788
|
+
plans = plan_forecasts(time, winner, seed, population)
|
|
789
|
+
report["plans"] = {
|
|
790
|
+
"label": "oracle-structure",
|
|
791
|
+
"sessions": len(plans["agentpace"]),
|
|
792
|
+
"results": {name: both_weightings(items)["row"] for name, items in plans.items()},
|
|
793
|
+
"intervals": intervals(
|
|
794
|
+
plans, "agentpace", ["sum-of-medians"], random.Random(f"{seed}:plan-intervals")
|
|
795
|
+
),
|
|
796
|
+
}
|
|
797
|
+
report_losses += _plan_losses("time", report["plans"]["results"])
|
|
798
|
+
if report["locked"]["status"] == "ok":
|
|
799
|
+
locked_losses += _plan_losses("locked", report["locked"]["plans"])
|
|
800
|
+
report["losses"] = report_losses + locked_losses
|
|
801
|
+
return report
|