RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
test_sobol.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Validate the Saltelli estimator against Ishigami, whose indices are analytic.
3
4 f(x) = sin(x1) + a sin^2(x2) + b x3^4 sin(x1), x ~ U[-pi, pi]^3
5
6with a = 7, b = 0.1 the exact indices are
7
8 S1 = 0.3139 S2 = 0.4424 S3 = 0.0000
9 ST1 = 0.5576 ST2 = 0.4424 ST3 = 0.2437
10
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.
16
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.
20
21 python test_sobol.py
22"""
23import json
24import subprocess
25import sys
26import tempfile
27from pathlib import Path
28
29import numpy as np
30
31HERE = Path(__file__).resolve().parent
32sys.path.insert(0, str(HERE))
33from merge_and_sobol import sobol_indices # noqa: E402
34
35EXACT_S = {"x1": 0.3139, "x2": 0.4424, "x3": 0.0000}
36EXACT_ST = {"x1": 0.5576, "x2": 0.4424, "x3": 0.2437}
37
38
39def ishigami(x1, x2, x3, a=7.0, b=0.1):
40 return np.sin(x1) + a * np.sin(x2) ** 2 + b * (x3 ** 4) * np.sin(x1)
41
42
43def build(N=4096, seed=7):
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")
50 out = td / "d.csv"
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(",")
57 design, recs = [], {}
58 for ln in lines[1:]:
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"]),
66 float(v["x3"])))}
67 return design, recs
68
69
70def report(tag, r, drop_note=""):
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}")
74 worst = 0.0
75 for k in ("x1", "x2", "x3"):
76 v = r["indices"][k]
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}")
81 return worst
82
83
84def main():
85 N = 4096
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)
90
91 # --- pairwise deletion: knock out 30 % of the AB rows for x2 only --------
92 rng = np.random.default_rng(3)
93 killed = 0
94 for d in design:
95 if d["matrix"] == "AB" and d["param_j"] == 1 and rng.random() < 0.30:
96 recs[d["row_id"]]["admissible"] = False # ran, but inadmissible
97 killed += 1
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)")
101
102 n = {k: r2["indices"][k]["n_eff"] for k in ("x1", "x2", "x3")}
103 print(f"\n n_eff after deletion: {n}")
104
105 ok = True
106 if w1 > 0.02:
107 print(f"\n FAIL: worst error {w1:.4f} > 0.02 with all rows usable"); ok = False
108 else:
109 print(f"\n clean-case worst error {w1:.4f} < 0.02 OK")
110 if w2 > 0.03:
111 print(f" FAIL: worst error {w2:.4f} > 0.03 after deletion"); ok = False
112 else:
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
116 else:
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"]:
120 pass
121 if r["indices"]["x3"]["ST"] < 0.15:
122 print(f" FAIL: ST3 = {r['indices']['x3']['ST']:.3f}, interaction missed"); ok = False
123 else:
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
128
129
130if __name__ == "__main__":
131 raise SystemExit(main())
build(N=4096, seed=7)
Definition test_sobol.py:43
ishigami(x1, x2, x3, a=7.0, b=0.1)
Definition test_sobol.py:39
report(tag, r, drop_note="")
Definition test_sobol.py:70