348def sobol_indices(recs, design_rows, p_keys, observable, n_boot=200, seed=0,
349 dfp_tol=None, require_converged=False, at_dose=None):
350 """Saltelli/Jansen estimators with pairwise deletion.
352 S_j = mean_i f(B_i) [ f(AB_i^j) - f(A_i) ] / V (Saltelli 2010)
353 S_T_j = mean_i [ f(A_i) - f(AB_i^j) ]^2 / (2 V) (Jansen 1999)
357 for d
in design_rows:
358 idx[(d[
"base_idx"], d[
"matrix"], d[
"param_j"])] = d[
"row_id"]
359 bases = sorted({d[
"base_idx"]
for d
in design_rows})
362 rid = idx.get((b, m, j))
366 if not usable(r, dfp_tol
if dfp_tol
is not None else DFP_TOL_DEFAULT,
367 require_converged, at_dose):
373 return v
if np.isfinite(v)
else None
375 fA, fB, ok_base = {}, {}, []
377 a_, b_ = val(b,
"A", -1), val(b,
"B", -1)
378 if a_
is not None and b_
is not None:
379 fA[b], fB[b] = a_, b_
384 allf = np.array([fA[b]
for b
in ok_base] + [fB[b]
for b
in ok_base])
385 V = float(np.var(allf, ddof=1))
386 rng = np.random.default_rng(seed)
388 for j, key
in enumerate(p_keys):
389 rows = [(fA[b], fB[b], val(b,
"AB", j))
for b
in ok_base]
390 rows = [(a_, b_, ab)
for (a_, b_, ab)
in rows
if ab
is not None]
393 res[key] = {
"S":
None,
"ST":
None,
"n_eff": n,
394 "S_lo":
None,
"S_hi":
None}
396 A = np.array([r[0]
for r
in rows])
397 B = np.array([r[1]
for r
in rows])
398 AB = np.array([r[2]
for r
in rows])
399 S = float(np.mean(B * (AB - A)) / V)
400 ST = float(np.mean((A - AB) ** 2) / (2.0 * V))
403 for _
in range(n_boot):
404 k = rng.integers(0, n, n)
405 Vb = np.var(np.concatenate([A[k], B[k]]), ddof=1)
407 bs.append(np.mean(B[k] * (AB[k] - A[k])) / Vb)
408 lo, hi = (float(np.percentile(bs, 2.5)),
409 float(np.percentile(bs, 97.5)))
if bs
else (
None,
None)
410 res[key] = {
"S": S,
"ST": ST,
"n_eff": n,
"S_lo": lo,
"S_hi": hi}
411 return {
"V": V,
"n_base_ok": len(ok_base),
"indices": res}
415 global DFP_TOL_DEFAULT
416 ap = argparse.ArgumentParser(description=__doc__,
417 formatter_class=argparse.RawDescriptionHelpFormatter)
418 ap.add_argument(
"--design", type=Path, required=
True)
419 ap.add_argument(
"--results", type=Path, default=HERE /
"results")
420 ap.add_argument(
"--out", type=Path, default=HERE /
"report")
421 ap.add_argument(
"--n-boot", type=int, default=200)
422 ap.add_argument(
"--dfp-tol", type=float, default=DFP_TOL_DEFAULT,
423 help=
"delta_FP admissibility bar, applied HERE not in the "
424 "worker, so it can be changed without re-running "
425 "anything (plan S11(h)). Only has an effect together "
426 "with --require-converged. Default 1e-2.")
427 ap.add_argument(
"--require-converged", action=
"store_true",
428 help=
"restore the pre-2026-08-05 rule: reject rows whose "
429 "size tail is truncated (pile/top-bin) or whose "
430 "delta_FP exceeds --dfp-tol. OFF by default -- "
431 "truncation is recorded, not gated (plan S11(q)). Run "
432 "the report BOTH ways: if the parameter RANKING is the "
433 "same, truncation did not buy the ranking anything, "
434 "which is the claim the default policy rests on.")
435 ap.add_argument(
"--at-dose", type=float, default=
None,
436 help=
"screen every row at this COMMON dose, read off the "
437 "per-row `at_dose` ladder, instead of at the end of "
438 "each trajectory. This is what lets a timed-out row "
439 "contribute: it enters every rung it reached and is "
440 "absent above that, pairwise. Without it, rows that "
441 "stopped early are excluded, because their "
442 "observables sit at a different dose from everyone "
443 "else's and dose moves them far harder than any "
444 "numerical choice (plan S12(s)).")
445 a = ap.parse_args(argv)
446 DFP_TOL_DEFAULT = a.dfp_tol
447 RC = a.require_converged
450 meta = json.loads(a.design.with_suffix(
".meta.json").read_text(encoding=
"utf-8"))
451 p_keys = meta[
"parameters"]
452 lines = a.design.read_text(encoding=
"utf-8").strip().splitlines()
453 cols = lines[0].split(
",")
456 v = dict(zip(cols, ln.split(
",")))
457 design.append({
"row_id": int(v[
"row_id"]),
"condition": v[
"condition"],
458 "base_idx": int(v[
"base_idx"]),
"matrix": v[
"matrix"],
459 "param_j": int(v[
"param_j"])})
461 meta_p = a.design.with_suffix(
'.meta.json')
462 dmeta = json.loads(meta_p.read_text(encoding=
'utf-8'))
if meta_p.exists()
else {}
463 recs =
load_results(a.results, dmeta.get(
'design_sha256'))
464 check_provenance(recs)
467 have = sum(1
for d
in design
if d[
"row_id"]
in recs)
468 ok = sum(1
for d
in design
if usable(recs.get(d[
"row_id"]), a.dfp_tol, RC, AD))
469 failed = sum(1
for d
in design
470 if d[
"row_id"]
in recs
and recs[d[
"row_id"]].get(
"solver_rc"))
471 inadm = have - ok - failed
472 print(f
"\n policy: {'require-converged (pre-2026-08-05)' if RC else 'truncation RECORDED, NOT GATED (plan S11(q))'}"
473 f
"{f'; screened at COMMON dose {AD:g} dpa' if AD is not None else '; screened at END OF RUN'}")
474 print(f
" coverage: {have}/{total} rows present, {ok} admissible, "
475 f
"{inadm} inadmissible, {failed} failed, {total-have} MISSING")
482 print(f
" dose ladder (rows reaching each rung, of {have} present):")
483 print(
" " +
" ".join(f
"{k}:{v}" for k, v
in cov.items()))
484 n_starved = sum(1
for r
in recs.values()
if r.get(
"starved"))
485 if AD
is None and n_starved:
487 best = max(float(k)
for k, v
in cov.items()
if v == max(cov.values()))
488 print(f
" *** {n_starved} timed-out row(s) EXCLUDED. Re-run with "
489 f
"--at-dose {best:g} to include them at a common dose.")
493 print(f
" *** {n_starved} timed-out row(s) EXCLUDED, and these rows "
494 f
"carry NO dose ladder -- they predate plan S12(s), so "
495 f
"--at-dose cannot recover them.")
501 alts = [t
for t
in (1e-2, 1e-3, 1e-4, 1e-6)
if t != a.dfp_tol]
503 f
"{t:.0e}: {sum(1 for d in design if usable(recs.get(d['row_id']), t, True))}"
504 for t
in sorted([a.dfp_tol] + alts, reverse=
True))
505 print(f
" admissible vs delta_FP bar {counts} (in use: {a.dfp_tol:.0e})")
510 pool = [recs[d[
"row_id"]]
for d
in design
511 if usable(recs.get(d[
"row_id"]), a.dfp_tol,
False, AD)]
513 trunc = sum(1
for r
in pool
if not grid_ok(r)[0])
514 hi_dfp = sum(1
for r
in pool
if abs(r.get(
"delta_FP")
or 0) >= a.dfp_tol)
515 print(f
" of the {len(pool)} rows used: {trunc} ({100*trunc/len(pool):.0f}%) "
516 f
"have a truncated size tail, {hi_dfp} ({100*hi_dfp/len(pool):.0f}%) "
517 f
"have delta_FP >= {a.dfp_tol:g}")
518 print(f
" -> re-run with --require-converged and compare the RANKING, "
521 miss = sorted(d[
"row_id"]
for d
in design
if d[
"row_id"]
not in recs)
522 by_mach = defaultdict(int)
525 print(f
" missing row_ids mod 4: {dict(by_mach)} "
526 f
"(a whole residue class means a machine never reported)")
532 reasons = defaultdict(int)
533 for r
in recs.values():
534 if r.get(
"solver_rc")
or usable(r, a.dfp_tol, RC, AD):
539 reasons[
"dose-starved (use --at-dose)" if AD
is None
540 else f
"did not reach {AD:g} dpa"] += 1
545 elif abs(r.get(
"delta_FP")
or 0) >= a.dfp_tol:
546 reasons[f
"delta_FP>={a.dfp_tol:g}"] += 1
548 print(
" inadmissible because:")
549 for k, v
in sorted(reasons.items(), key=
lambda t: -t[1]):
550 print(f
" {v:5d} {k}")
552 a.out.mkdir(parents=
True, exist_ok=
True)
558 print(
"\n *** rows come from MORE THAN ONE equations mode - they are "
559 "not poolable; separate them before estimating.")
561 print(f
"\n equations mode = {mode}: withholding {len(blocked)} "
562 f
"observable(s) whose closure is not validated in this mode:")
563 print(f
" {', '.join(sorted(blocked))}")
564 print(
" Screening on them would produce indices about the numerics, "
566 print(
" (<100> in bin_moment: mean_n_100 +144% vs a converged discrete "
567 "reference, 2026-08-02.)")
570 for cond
in meta[
"conditions"]:
571 dc = [d
for d
in design
if d[
"condition"] == cond]
573 r = sobol_indices(recs, dc, p_keys, obs, n_boot=a.n_boot,
574 dfp_tol=a.dfp_tol, require_converged=RC,
577 print(f
" {cond:>4s} {obs:16s} -- too few usable base points")
579 print(f
"\n {cond} / {obs} Var={r['V']:.4g} "
580 f
"base points usable {r['n_base_ok']}/{meta['N']}")
581 ranked = sorted(r[
"indices"].items(),
582 key=
lambda kv: -(kv[1][
"ST"]
or -1))
583 for key, v
in ranked[:6]:
586 flag =
" <-- n_eff LOW" if v[
"n_eff"] < 0.5 * meta[
"N"]
else ""
587 print(f
" {key:18s} S={v['S']:7.3f} "
588 f
"[{v['S_lo']:6.3f},{v['S_hi']:6.3f}] "
589 f
"ST={v['ST']:7.3f} n_eff={v['n_eff']:3d}{flag}")
590 for key, v
in r[
"indices"].items():
591 rows_out.append({
"condition": cond,
"observable": obs,
592 "parameter": key, **v})
594 csv = a.out /
"T2_sobol_indices.csv"
595 with csv.open(
"w", encoding=
"utf-8", newline=
"")
as fh:
596 fh.write(
"condition,observable,parameter,S,S_lo,S_hi,ST,n_eff\n")
599 return "" if r[k]
is None else f
"{r[k]:.6g}"
600 fh.write(f
"{r['condition']},{r['observable']},{r['parameter']},"
601 f
"{g('S')},{g('S_lo')},{g('S_hi')},{g('ST')},{r['n_eff']}\n")
602 print(f
"\n wrote {csv} ({len(rows_out)} rows)")
603 print(
"\n Read n_eff before reading any index: an S_i^T computed from a")
604 print(
" handful of base points is not a screening result. Parameters whose")
605 print(
" n_eff collapsed are telling you their prior region is unintegrable")
606 print(
" at this tolerance -- itself a finding (plan Tier-2 failure handling).")