Skip to content

Commit 13f892c

Browse files
slurm
1 parent d2679f9 commit 13f892c

7 files changed

Lines changed: 107 additions & 19 deletions

cybench/runs/analysis/country_significance_lib.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def bootstrap_country_ai_metrics(
112112
"""Bootstrap countries; report CIs for median Δ_abs, median Δ%, and win rate."""
113113
if frame.empty:
114114
return {
115+
"median_nrmse_trad": None,
116+
"median_nrmse_ai": None,
115117
"median_delta_abs": None,
116118
"delta_abs_ci_lo": None,
117119
"delta_abs_ci_hi": None,
@@ -127,9 +129,23 @@ def bootstrap_country_ai_metrics(
127129
delta_pct = frame["delta_pct"].to_numpy(dtype=float)
128130
ai_wins = frame["ai_wins"].to_numpy(dtype=bool)
129131
n = delta_abs.size
132+
nrmse_trad = (
133+
frame["nrmse_trad"].to_numpy(dtype=float)
134+
if "nrmse_trad" in frame.columns
135+
else np.full(n, np.nan)
136+
)
137+
nrmse_ai = (
138+
frame["nrmse_ai"].to_numpy(dtype=float)
139+
if "nrmse_ai" in frame.columns
140+
else np.full(n, np.nan)
141+
)
142+
med_trad = float(np.median(nrmse_trad)) if np.isfinite(nrmse_trad).any() else None
143+
med_ai = float(np.median(nrmse_ai)) if np.isfinite(nrmse_ai).any() else None
130144

131145
if n == 1:
132146
return {
147+
"median_nrmse_trad": med_trad,
148+
"median_nrmse_ai": med_ai,
133149
"median_delta_abs": float(delta_abs[0]),
134150
"delta_abs_ci_lo": float(delta_abs[0]),
135151
"delta_abs_ci_hi": float(delta_abs[0]),
@@ -157,6 +173,8 @@ def bootstrap_country_ai_metrics(
157173
wr_lo, wr_hi = _percentile_ci(boot_wr, ci=ci)
158174

159175
return {
176+
"median_nrmse_trad": med_trad,
177+
"median_nrmse_ai": med_ai,
160178
"median_delta_abs": float(np.median(delta_abs)),
161179
"delta_abs_ci_lo": abs_lo,
162180
"delta_abs_ci_hi": abs_hi,
@@ -256,9 +274,12 @@ def bootstrap_stats_json(result: dict[str, Any]) -> dict[str, Any]:
256274

257275

258276
COUNTRY_BOOTSTRAP_NOTE = (
259-
"Best data-driven vs best traditional baseline per country (Average, Trend, LPJmL). "
260-
"NRMSE is averaged over walk-forward seeds before comparison. Countries are resampled "
261-
"with replacement; intervals are percentile 95% bootstrap CIs."
277+
"Per country: best traditional NRMSE = min(Average, Trend, LPJmL); best data-driven NRMSE = "
278+
"min(feature-engineered, sequence, foundation). NRMSE columns are medians of those "
279+
"country-level values. Median improvement (%) is the median of "
280+
"100×(traditional−data-driven)/traditional per country. Win rate: fraction of countries "
281+
"where data-driven NRMSE is lower. Seed-averaged walk-forward NRMSE; bootstrap resamples "
282+
"countries (95% percentile CIs on improvement and win rate)."
262283
)
263284

264285

@@ -325,11 +346,18 @@ def _fmt_ci(lo: float | None, hi: float | None, *, digits: int = 1, percent: boo
325346
return f"[{lo:.{digits}f}, {hi:.{digits}f}]"
326347

327348

349+
def _fmt_nrmse_pct(value: float | None) -> str:
350+
"""Format pooled NRMSE fraction as percent (matches Table 1)."""
351+
if value is None or not np.isfinite(value):
352+
return "---"
353+
return f"{100.0 * float(value):.1f}"
354+
355+
328356
def format_results_markdown_table(results: dict[str, dict[str, Any]]) -> str:
329357
"""Paper-style summary table (markdown)."""
330358
lines = [
331-
"| Crop | Median ΔNRMSE (%) | 95% CI | AI win rate | 95% CI |",
332-
"|------|-------------------|--------|-------------|--------|",
359+
"| Crop | Trad. NRMSE (%) | AI NRMSE (%) | Median improvement (%) | 95% CI | AI win rate | 95% CI |",
360+
"|------|-----------------|--------------|------------------------|--------|-------------|--------|",
333361
]
334362
for crop, res in results.items():
335363
med = res.get("median_delta_pct")
@@ -341,6 +369,8 @@ def format_results_markdown_table(results: dict[str, dict[str, Any]]) -> str:
341369
+ " | ".join(
342370
[
343371
crop.capitalize(),
372+
_fmt_nrmse_pct(res.get("median_nrmse_trad")),
373+
_fmt_nrmse_pct(res.get("median_nrmse_ai")),
344374
_fmt_num(med),
345375
_fmt_ci(lo, hi).strip("[]") if lo is not None else "---",
346376
f"{100 * wr:.0f}%" if wr is not None else "---",
@@ -363,29 +393,34 @@ def format_results_latex_table(
363393
"""LaTeX table for main paper or supplement."""
364394
if caption is None:
365395
caption = (
366-
"Country-level bootstrap summary of AI advantage over traditional baselines "
367-
"(best data-driven vs.\\ best among Average, Trend, and LPJmL per country). "
368-
"Median $\\Delta$NRMSE is reported as percent improvement; win rate is the "
369-
"fraction of countries where AI achieved lower NRMSE. Countries were resampled "
370-
"with replacement ($B=10{,}000$); intervals are percentile 95\\% bootstrap CIs."
396+
"Country-level bootstrap summary of AI advantage over traditional baselines. "
397+
"Traditional and data-driven NRMSE are medians across countries of the best "
398+
"traditional (Average, Trend, LPJmL) and best data-driven model per country. "
399+
"Median improvement (\\%) is the median of "
400+
"$100\\times(\\mathrm{NRMSE}_{\\mathrm{trad}}-\\mathrm{NRMSE}_{\\mathrm{AI}})"
401+
"/\\mathrm{NRMSE}_{\\mathrm{trad}}$ per country. Win rate: fraction of countries "
402+
"where data-driven NRMSE is lower. Bootstrap resamples countries ($B=10{,}000$); "
403+
"intervals are percentile 95\\% CIs."
371404
)
372405
body_rows: list[str] = []
373406
for crop, res in results.items():
407+
trad = _fmt_nrmse_pct(res.get("median_nrmse_trad"))
408+
ai = _fmt_nrmse_pct(res.get("median_nrmse_ai"))
374409
med = _fmt_num(res.get("median_delta_pct"))
375410
pct_ci = _fmt_ci(res.get("delta_pct_ci_lo"), res.get("delta_pct_ci_hi"))
376411
wr = _fmt_pct_rate(res.get("win_rate"))
377412
wr_ci = _fmt_ci(res.get("win_rate_ci_lo"), res.get("win_rate_ci_hi"), percent=True)
378413
body_rows.append(
379-
f"{crop.capitalize()} & {med} & {pct_ci} & {wr} & {wr_ci} \\\\"
414+
f"{crop.capitalize()} & {trad} & {ai} & {med} & {pct_ci} & {wr} & {wr_ci} \\\\"
380415
)
381416
body = "\n".join(body_rows)
382417
return f"""\\begin{{table}}[t]
383418
\\centering
384419
\\caption{{{caption}}}
385420
\\label{{{label}}}
386-
\\begin{{tabular}}{{lcccc}}
421+
\\begin{{tabular}}{{lcccccc}}
387422
\\toprule
388-
Crop & Median $\\Delta$NRMSE (\\%) & 95\\% CI & AI win rate & 95\\% CI \\\\
423+
Crop & Trad.\\ NRMSE (\\%) & AI NRMSE (\\%) & Median impr. (\\%) & 95\\% CI & AI win rate & 95\\% CI \\\\
389424
\\midrule
390425
{body}
391426
\\bottomrule

cybench/runs/slurm/benchmark_completion_lib.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,11 @@ def filter_jobs_by_models(
431431
return [job for job in jobs if job.model.lower() in allowed]
432432

433433

434+
def filter_jobs_cpu_only(jobs: list[JobRow]) -> list[JobRow]:
435+
"""Keep naive + CPU-tabular rows (needs_gpu=no); exclude torch / TabPFN / TabDPT."""
436+
return [job for job in jobs if job.needs_gpu == "no"]
437+
438+
434439
def job_row_key(job: JobRow) -> tuple[str, str, str, str, str, str, str]:
435440
return (
436441
job.crop,

cybench/runs/slurm/orchestrate_benchmark_complete.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
expand_all_country_targets,
4848
expand_target_batches,
4949
filter_jobs_by_models,
50+
filter_jobs_cpu_only,
5051
jobs_for_phase,
5152
merge_jobs_for_models,
5253
resolve_country_list,
@@ -260,6 +261,13 @@ def _process_batch(
260261
)
261262
return 0
262263

264+
if args.only_cpu:
265+
jobs = filter_jobs_cpu_only(jobs)
266+
manifest_source = f"{manifest_source} | cpu-models-only"
267+
if not jobs:
268+
print(f"[WARN] No CPU/naive jobs in manifest for {batch}", file=sys.stderr)
269+
return 0
270+
263271
if not baselines_dir.is_dir():
264272
print(
265273
f"[WARN] Baselines dir missing (all jobs treated as incomplete): {baselines_dir}",
@@ -454,6 +462,14 @@ def main(argv: list[str] | None = None) -> int:
454462
action="store_true",
455463
help="Force GPU manifest group to CPU (--cpu on submit_benchmark.sh)",
456464
)
465+
parser.add_argument(
466+
"--only-cpu",
467+
action="store_true",
468+
help=(
469+
"Assess/submit only CPU/naive models (needs_gpu=no); "
470+
"exclude tst_lf, tabdpt, TabPFN, etc."
471+
),
472+
)
457473
parser.add_argument(
458474
"--force-gpu",
459475
action="store_true",

cybench/runs/slurm/orchestrate_benchmark_complete.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Options:
3838
--submit Write retry manifest and call submit_benchmark.sh
3939
--dry-run With --submit: no sbatch
4040
--cpu Force torch/TabPFN group to CPU partition
41+
--only-cpu Complete/submit only CPU+naive models (needs_gpu=no)
4142
--force-gpu Use gpu partition even when regions < threshold
4243
--region-threshold N gpu when country has >= N regions (default: 600)
4344
@@ -80,6 +81,7 @@ LIST=false
8081
SUBMIT=false
8182
DRY_RUN=false
8283
FORCE_CPU=false
84+
ONLY_CPU=false
8385
FORCE_GPU=false
8486
REGION_THRESHOLD=""
8587

@@ -177,6 +179,10 @@ while [[ $# -gt 0 ]]; do
177179
FORCE_CPU=true
178180
shift
179181
;;
182+
--only-cpu)
183+
ONLY_CPU=true
184+
shift
185+
;;
180186
--force-gpu)
181187
FORCE_GPU=true
182188
shift
@@ -225,6 +231,7 @@ done
225231
[[ "${SUBMIT}" == true ]] && cmd+=(--submit)
226232
[[ "${DRY_RUN}" == true ]] && cmd+=(--dry-run)
227233
[[ "${FORCE_CPU}" == true ]] && cmd+=(--cpu)
234+
[[ "${ONLY_CPU}" == true ]] && cmd+=(--only-cpu)
228235
[[ "${FORCE_GPU}" == true ]] && cmd+=(--force-gpu)
229236
[[ -n "${REGION_THRESHOLD}" ]] && cmd+=(--region-threshold "${REGION_THRESHOLD}")
230237

cybench/runs/viz/model_family_radar_template.html

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,9 @@ <h2>Country-level AI advantage (bootstrap)</h2>
310310
<tr>
311311
<th>Crop</th>
312312
<th>Countries</th>
313-
<th>Median ΔNRMSE (%)</th>
313+
<th>Traditional NRMSE (%)</th>
314+
<th>Data-driven NRMSE (%)</th>
315+
<th>Median improvement (%)</th>
314316
<th>95% CI</th>
315317
<th>AI win rate</th>
316318
<th>95% CI</th>
@@ -879,6 +881,11 @@ <h2>Country-level AI advantage (bootstrap)</h2>
879881
return (DATA.crops || []).slice().sort();
880882
}
881883

884+
function formatBootstrapNrmsePct(value) {
885+
if (value == null || !Number.isFinite(value)) return "—";
886+
return (100 * Number(value)).toFixed(1);
887+
}
888+
882889
function formatBootstrapNum(value, digits = 1) {
883890
if (value == null || !Number.isFinite(value)) return "—";
884891
return Number(value).toFixed(digits);
@@ -918,14 +925,16 @@ <h2>Country-level AI advantage (bootstrap)</h2>
918925
if (!bootstrapTable) return;
919926
const tbody = bootstrapTable.querySelector("tbody");
920927
if (!currentBootstrapRows.length) {
921-
tbody.innerHTML = `<tr><td colspan="6" class="muted">No bootstrap summary for this horizon/crop.</td></tr>`;
928+
tbody.innerHTML = `<tr><td colspan="8" class="muted">No bootstrap summary for this horizon/crop.</td></tr>`;
922929
return;
923930
}
924931
tbody.innerHTML = currentBootstrapRows.map(row => {
925932
const wr = row.win_rate;
926933
return `<tr>
927934
<td>${row.crop.charAt(0).toUpperCase() + row.crop.slice(1)}</td>
928935
<td>${row.n_ai_wins ?? "—"}/${row.n_countries ?? "—"}</td>
936+
<td>${formatBootstrapNrmsePct(row.median_nrmse_trad)}</td>
937+
<td>${formatBootstrapNrmsePct(row.median_nrmse_ai)}</td>
929938
<td>${formatBootstrapNum(row.median_delta_pct)}</td>
930939
<td>${formatBootstrapCi(row.delta_pct_ci_lo, row.delta_pct_ci_hi)}</td>
931940
<td>${wr == null ? "—" : `${Math.round(100 * wr)}%`}</td>
@@ -952,21 +961,23 @@ <h2>Country-level AI advantage (bootstrap)</h2>
952961
const { hzLabel, cropLabel } = bootstrapExportMeta();
953962
const nBoot = (bootstrapPayloadRoot().n_bootstrap || 10000).toLocaleString();
954963
const bodyRows = rows.map(row => {
964+
const trad = formatBootstrapNrmsePct(row.median_nrmse_trad);
965+
const ai = formatBootstrapNrmsePct(row.median_nrmse_ai);
955966
const wr = row.win_rate == null ? "---" : `${Math.round(100 * row.win_rate)}\\%`;
956967
const wrCi = formatBootstrapCi(row.win_rate_ci_lo, row.win_rate_ci_hi, { percent: true });
957968
const pctCi = formatBootstrapCi(row.delta_pct_ci_lo, row.delta_pct_ci_hi);
958-
return `${row.crop.charAt(0).toUpperCase() + row.crop.slice(1)} & `
969+
return `${row.crop.charAt(0).toUpperCase() + row.crop.slice(1)} & ${trad} & ${ai} & `
959970
+ `${formatBootstrapNum(row.median_delta_pct)} & ${pctCi} & ${wr} & ${wrCi} \\\\`;
960971
}).join("\n");
961972
return `% CY-Bench country-level AI bootstrap table (generated from dashboard)
962973
% Requires: \\usepackage{booktabs}
963974
\\begin{table}[t]
964975
\\centering
965-
\\caption{Country-level bootstrap summary of AI advantage over traditional baselines (best data-driven vs.\\ best among Average, Trend, and LPJmL per country). Median $\\Delta$NRMSE is reported as percent improvement; win rate is the fraction of countries where AI achieved lower NRMSE. Horizon: ${escapeLatex(hzLabel)}; crops: ${escapeLatex(cropLabel)}. Countries resampled with replacement ($B=${nBoot}$); intervals are percentile 95\\% bootstrap CIs.}
976+
\\caption{Country-level bootstrap summary of AI advantage over traditional baselines. Traditional and data-driven NRMSE are medians across countries of the best traditional (Average, Trend, LPJmL) and best data-driven model per country. Median improvement (\\%) is the median of $100\\times(\\mathrm{NRMSE}_{\\mathrm{trad}}-\\mathrm{NRMSE}_{\\mathrm{AI}})/\\mathrm{NRMSE}_{\\mathrm{trad}}$ per country. Horizon: ${escapeLatex(hzLabel)}; crops: ${escapeLatex(cropLabel)}. Bootstrap resamples countries ($B=${nBoot}$).}
966977
\\label{tab:ai_country_bootstrap}
967-
\\begin{tabular}{lcccc}
978+
\\begin{tabular}{lcccccc}
968979
\\toprule
969-
Crop & Median $\\Delta$NRMSE (\\%) & 95\\% CI & AI win rate & 95\\% CI \\\\
980+
Crop & Trad.\\ NRMSE (\\%) & AI NRMSE (\\%) & Median impr. (\\%) & 95\\% CI & AI win rate & 95\\% CI \\\\
970981
\\midrule
971982
${bodyRows}
972983
\\bottomrule

tests/runs/test_benchmark_completion.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
assess_job,
1010
check_screening_years,
1111
filter_jobs_by_models,
12+
filter_jobs_cpu_only,
1213
jobs_for_phase,
1314
read_manifest,
1415
screening_complete,
@@ -111,6 +112,17 @@ def test_filter_jobs_by_models():
111112
assert filter_jobs_by_models(jobs, None) == jobs
112113

113114

115+
def test_filter_jobs_cpu_only():
116+
jobs = [
117+
JobRow("maize", "DE", "xgboost", "pandas", "yes", "yes", "no"),
118+
JobRow("maize", "DE", "lpjml_bc", "pandas", "no", "no", "no"),
119+
JobRow("maize", "DE", "tst_lf", "torch", "yes", "no", "yes"),
120+
JobRow("wheat", "DE", "tabdpt", "pandas", "yes", "yes", "yes"),
121+
]
122+
cpu = filter_jobs_cpu_only(jobs)
123+
assert cpu == jobs[:2]
124+
125+
114126
def test_jobs_for_phase_force_rerun_includes_complete():
115127
job = JobRow("maize", "BE", "lpjml_bc", "pandas", "no", "no", "no")
116128
from cybench.runs.slurm.benchmark_completion_lib import JobAssessment

tests/runs/test_country_significance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,7 @@ def test_analyze_country_ai_benefit_returns_bootstrap_fields():
9393
assert res["win_rate"] == pytest.approx(2 / 3)
9494
assert res["median_delta_pct"] is not None
9595
assert res["delta_pct_ci_lo"] is not None
96+
assert res["median_nrmse_trad"] == pytest.approx(0.20)
97+
assert res["median_nrmse_ai"] == pytest.approx(0.17)
9698
assert res["median_delta_abs"] is not None
9799
assert res["delta_abs_ci_lo"] is not None

0 commit comments

Comments
 (0)