104 """Map u ~ U[0,1)^n onto the parameter's prior support.
106 Every family is inverse-CDF so the Sobol sequence's low-discrepancy
107 structure survives the transform -- which is the whole point of using one.
109 lo, hi, fam = float(p[
"lo"]), float(p[
"hi"]), p[
"prior"]
110 u = np.clip(u, 1e-12, 1 - 1e-12)
112 return lo + (hi - lo) * u
114 return 10.0 ** (np.log10(lo) + (np.log10(hi) - np.log10(lo)) * u)
117 return np.floor(lo + (hi + 1 - lo) * u).clip(lo, hi).astype(int)
119 mu = float(p[
"nominal"])
120 sig = (hi - lo) / 4.0
121 a, b = (lo - mu) / sig, (hi - mu) / sig
122 return truncnorm.ppf(u, a, b, loc=mu, scale=sig)
123 raise ValueError(f
"unknown prior family {fam!r} for {p['key']}")
128 """Return (rows, meta) where rows is a list of dicts.
130 Layout per base index i: A_i, B_i, then AB_i^(j) for each j.
131 AB_i^(j) is A_i with column j replaced by B_i's column j.
137 sampler = qmc.Sobol(d=2 * p, scramble=
True, seed=seed)
139 print(f
" warning: N={N} is not a power of two; Sobol balance is "
140 f
"degraded. Prefer 8/16/32/64.", file=sys.stderr)
141 draw = sampler.random(N)
142 U_A, U_B = draw[:, :p], draw[:, p:]
147 variants = [(
"A", -1, U_A[i].copy()), (
"B", -1, U_B[i].copy())]
151 variants.append((
"AB", j, ab))
152 for matrix, j, u
in variants:
153 row = {
"row_id": rid,
"matrix": matrix,
"base_idx": i,
"param_j": j}
154 for k, par
in enumerate(params):
156 row[par[
"key"]] = int(v)
if par[
"prior"] ==
"INT" else float(v)
172 ap = argparse.ArgumentParser(description=__doc__,
173 formatter_class=argparse.RawDescriptionHelpFormatter)
174 ap.add_argument(
"--tier", type=int, choices=(2, 3), required=
True)
175 ap.add_argument(
"--N", type=int, default=16, help=
"base samples (power of 2)")
176 ap.add_argument(
"--seed", type=int, default=20260801)
177 ap.add_argument(
"--conditions", nargs=
"+", default=[
"N2",
"N5",
"I1"],
178 help=
"condition ids; the design is replicated across these")
179 ap.add_argument(
"--out", type=Path, required=
True)
180 ap.add_argument(
"--spec", type=Path, default=
None)
181 ap.add_argument(
"--exclude", nargs=
"*", default=[],
182 help=
"parameters this campaign HOLDS FIXED rather than "
183 "samples. Recorded in the .meta.json so the exclusion "
184 "travels with the design. Rev 6: i_mobile v_mobile.")
185 a = ap.parse_args(argv)
191 pending = [q[
"key"]
for q
in params
if q.get(
"REVISION_PENDING")]
193 print(f
" *** {len(pending)} parameter(s) marked REVISION_PENDING: "
194 f
"{', '.join(pending)}", file=sys.stderr)
195 print(
" Their ranges predate the 2026-08-01 absorption-gate work. "
196 "Fix parameters.json before a production campaign.",
205 for cond
in a.conditions:
208 rr[
"condition"] = cond
210 rr[
"cond_row_id"] = r[
"row_id"]
214 a.out.parent.mkdir(parents=
True, exist_ok=
True)
215 cols = ([
"row_id",
"condition",
"cond_row_id",
"matrix",
"base_idx",
"param_j"]
216 + [q[
"key"]
for q
in params])
217 with a.out.open(
"w", encoding=
"utf-8", newline=
"")
as fh:
218 fh.write(
",".join(cols) +
"\n")
221 (f
"{r[c]:.17g}" if isinstance(r[c], float)
else str(r[c]))
222 for c
in cols) +
"\n")
224 digest = hashlib.sha256(a.out.read_bytes()).hexdigest()
226 "tier": a.tier,
"N": a.N,
"p": p,
"seed": a.seed,
227 "excluded": list(a.exclude),
228 "conditions": a.conditions,
229 "rows_per_condition": per_cond,
"rows_total": len(out_rows),
230 "parameters": [q[
"key"]
for q
in params],
231 "parameters_version": spec.get(
"version"),
232 "revision_pending": pending,
233 "design_sha256": digest,
235 "estimator":
"Saltelli; S_j pairs A_i with AB_i^(j), S_T_j pairs B_i with AB_i^(j)",
237 a.out.with_suffix(
".meta.json").write_text(json.dumps(meta, indent=2),
239 print(f
" wrote {a.out} ({len(out_rows)} rows = "
240 f
"{a.N}*({p}+2)*{len(a.conditions)} conditions)")
241 print(f
" sha256 {digest[:16]}... p={p} N={a.N}")
242 print(f
" per machine (4-way): ~{len(out_rows)//4} rows each")