2"""Validate the Saltelli estimator against Ishigami, whose indices are analytic.
4 f(x) = sin(x1) + a sin^2(x2) + b x3^4 sin(x1), x ~ U[-pi, pi]^3
6with a = 7, b = 0.1 the exact indices are
8 S1 = 0.3139 S2 = 0.4424 S3 = 0.0000
9 ST1 = 0.5576 ST2 = 0.4424 ST3 = 0.2437
11x3 is the interesting one: S3 = 0 exactly while ST3 = 0.244, i.e. it acts ONLY
12through its interaction with x1. An estimator that got S right but ST wrong
13would pass a naive check and then silently screen out exactly the parameters
14that matter through interactions -- which for this campaign would mean dropping
15a loop-conversion parameter that is inert alone but live in combination.
17Also tests PAIRWISE DELETION, the part that is easy to get wrong: some AB rows
18are deliberately marked unusable and the indices must stay near truth, with
19n_eff reporting the reduced count for the affected parameter only.
27from pathlib
import Path
31HERE = Path(__file__).resolve().parent
32sys.path.insert(0, str(HERE))
33from merge_and_sobol
import sobol_indices
35EXACT_S = {
"x1": 0.3139,
"x2": 0.4424,
"x3": 0.0000}
36EXACT_ST = {
"x1": 0.5576,
"x2": 0.4424,
"x3": 0.2437}
40 return np.sin(x1) + a * np.sin(x2) ** 2 + b * (x3 ** 4) * np.sin(x1)
44 spec = {
"version":
"ishigami",
"parameters": [
45 {
"id": i + 1,
"key": f
"x{i+1}",
"sheet":
"reactions",
"tier": 2,
46 "lo": -np.pi,
"hi": np.pi,
"nominal": 0.0,
"prior":
"U",
"units":
"-"}
47 for i
in range(3)],
"fixed": []}
48 td = Path(tempfile.mkdtemp())
49 (td /
"parameters.json").write_text(json.dumps(spec), encoding=
"utf-8")
51 subprocess.run([sys.executable, str(HERE /
"design.py"),
"--tier",
"2",
52 "--N", str(N),
"--seed", str(seed),
"--conditions",
"C0",
53 "--spec", str(td /
"parameters.json"),
"--out", str(out)],
54 check=
True, capture_output=
True)
55 lines = out.read_text(encoding=
"utf-8").strip().splitlines()
56 cols = lines[0].split(
",")
59 v = dict(zip(cols, ln.split(
",")))
60 rid = int(v[
"row_id"])
61 design.append({
"row_id": rid,
"condition": v[
"condition"],
62 "base_idx": int(v[
"base_idx"]),
"matrix": v[
"matrix"],
63 "param_j": int(v[
"param_j"])})
64 recs[rid] = {
"row_id": rid,
"solver_rc": 0,
"admissible":
True,
65 "y": float(
ishigami(float(v[
"x1"]), float(v[
"x2"]),
71 print(f
"\n {tag}{drop_note}")
72 print(f
" {'param':>5s} {'S':>8s} {'exact':>8s} {'err':>8s} "
73 f
"{'ST':>8s} {'exact':>8s} {'err':>8s} {'n_eff':>6s}")
75 for k
in (
"x1",
"x2",
"x3"):
77 eS, eT = abs(v[
"S"] - EXACT_S[k]), abs(v[
"ST"] - EXACT_ST[k])
78 worst = max(worst, eS, eT)
79 print(f
" {k:>5s} {v['S']:8.4f} {EXACT_S[k]:8.4f} {eS:8.4f} "
80 f
"{v['ST']:8.4f} {EXACT_ST[k]:8.4f} {eT:8.4f} {v['n_eff']:6d}")
86 print(f
"Ishigami validation of the Saltelli/Jansen estimators (N={N})")
87 design, recs =
build(N=N)
88 r = sobol_indices(recs, design, [
"x1",
"x2",
"x3"],
"y", n_boot=0)
89 w1 =
report(
"all rows usable", r)
92 rng = np.random.default_rng(3)
95 if d[
"matrix"] ==
"AB" and d[
"param_j"] == 1
and rng.random() < 0.30:
96 recs[d[
"row_id"]][
"admissible"] =
False
98 r2 = sobol_indices(recs, design, [
"x1",
"x2",
"x3"],
"y", n_boot=0)
99 w2 =
report(
"after dropping 30% of x2's AB rows", r2,
100 f
" ({killed} rows marked inadmissible)")
102 n = {k: r2[
"indices"][k][
"n_eff"]
for k
in (
"x1",
"x2",
"x3")}
103 print(f
"\n n_eff after deletion: {n}")
107 print(f
"\n FAIL: worst error {w1:.4f} > 0.02 with all rows usable"); ok =
False
109 print(f
"\n clean-case worst error {w1:.4f} < 0.02 OK")
111 print(f
" FAIL: worst error {w2:.4f} > 0.03 after deletion"); ok =
False
113 print(f
" deletion-case worst error {w2:.4f} < 0.03 OK")
114 if not (n[
"x2"] < n[
"x1"]
and n[
"x1"] == n[
"x3"] == N):
115 print(f
" FAIL: pairwise deletion leaked - only x2 should lose rows"); ok =
False
117 print(f
" pairwise deletion confined to x2 ({n['x2']}/{N}); "
118 f
"x1 and x3 untouched OK")
119 if abs(r[
"indices"][
"x3"][
"S"]) > 0.02 <= r[
"indices"][
"x3"][
"ST"]:
121 if r[
"indices"][
"x3"][
"ST"] < 0.15:
122 print(f
" FAIL: ST3 = {r['indices']['x3']['ST']:.3f}, interaction missed"); ok =
False
124 print(f
" x3 caught as interaction-only (S3~0, ST3="
125 f
"{r['indices']['x3']['ST']:.3f}) OK")
126 print(
"\n " + (
"ESTIMATOR VALIDATED" if ok
else "*** ESTIMATOR IS WRONG ***"))
127 return 0
if ok
else 1
130if __name__ ==
"__main__":
131 raise SystemExit(
main())
ishigami(x1, x2, x3, a=7.0, b=0.1)
report(tag, r, drop_note="")