58def run_cpp_once(bin_path: Path, R: np.ndarray, dist: str =
"gaussian", seed: int =
None) -> np.ndarray:
60 args = [str(bin_path), dist]
62 args.append(str(int(seed)))
64 proc = subprocess.run(args, input=payload, capture_output=
True, text=
True)
65 if proc.returncode != 0:
66 raise RuntimeError(f
"Exécution C++ a échoué (code {proc.returncode}): {proc.stderr.strip()}")
69 vals = [float(tok)
for tok
in proc.stdout.strip().
split()]
71 raise RuntimeError(f
"Sortie C++ non numérique:\n{proc.stdout}\nERR:\n{proc.stderr}")
72 return np.array(vals, dtype=float)
75def sample_many(bin_path: Path, R: np.ndarray, n_samples: int, seed_base: int, dist: str =
"gaussian") -> np.ndarray:
76 """Appelle le binaire n_samples fois (seed = seed_base + i) et empile les réalisations."""
77 ys: List[np.ndarray] = []
78 for i
in range(n_samples):
79 y =
run_cpp_once(bin_path, R, dist=dist, seed=seed_base + i)
85 emp_corr = np.corrcoef(Y, rowvar=
False)
87 means = Y.mean(axis=0)
88 stds = Y.std(axis=0, ddof=1)
90 "samples": Y.shape[0],
92 "max|corr_error|": float(np.max(np.abs(err))),
93 "mean|corr_error|": float(np.mean(np.abs(err))),
94 "max|mean|": float(np.max(np.abs(means))),
95 "max|std-1|": float(np.max(np.abs(stds - 1.0))),
99def plot_case(save_dir: Path, name: str, Y: np.ndarray, R: np.ndarray):
100 save_dir.mkdir(parents=
True, exist_ok=
True)
101 emp_corr = np.corrcoef(Y, rowvar=
False)
104 plt.imshow(emp_corr, vmin=-1, vmax=1)
105 plt.title(f
"Empirical correlation – {name}")
108 plt.savefig(save_dir / f
"{name}_emp_corr.png", dpi=160)
112 plt.imshow(R, vmin=-1, vmax=1)
113 plt.title(f
"Target correlation – {name}")
116 plt.savefig(save_dir / f
"{name}_target_corr.png", dpi=160)
119 subset = Y[: min(4000, len(Y)), :]
121 plt.scatter(subset[:, 0], subset[:, 1], s=5)
122 plt.title(f
"Scatter y1 vs y2 – {name}")
126 plt.savefig(save_dir / f
"{name}_scatter_y1_y2.png", dpi=160)
129 xs = np.linspace(Y[:, 0].min(), Y[:, 0].max(), 400)
130 pdf = 1.0 / np.sqrt(2 * np.pi) * np.exp(-0.5 * xs * xs)
132 plt.hist(Y[:, 0], bins=60, density=
True)
134 plt.title(f
"Histogram(y1) + N(0,1) – {name}")
136 plt.ylabel(
"density")
138 plt.savefig(save_dir / f
"{name}_hist_y1.png", dpi=160)
144 cases: List[Tuple[str, np.ndarray]] = []
145 requested = [c.strip().lower()
for c
in args.cases.split(
",")]
149 cases.append((
"identity", np.eye(args.n_dim, dtype=float)))
150 elif c.startswith(
"equicorr"):
152 cases.append((f
"equicorr_rho_{rho}".replace(
".",
"_"),
equicorr(args.n_dim, rho)))
154 R = np.array([[1.0, args.block_rho, 0.0],
155 [args.block_rho, 1.0, 0.0],
156 [0.0, 0.0, 1.0]], dtype=float)
157 cases.append((f
"block_rho12_{args.block_rho}".replace(
".",
"_"), R))
160 cases.append((f
"near_singular_rho_{rho}".replace(
".",
"_"),
equicorr(args.n_dim, rho)))
162 if not args.custom_matrix:
163 raise ValueError(
"--cases inclut 'custom' mais --custom-matrix est vide.")
165 cases.append((
"custom", R))
167 raise ValueError(f
"Cas inconnu: {c}")
172 p = argparse.ArgumentParser(description=
"Testeur Python pour le générateur C++ 'correlated_rng'.")
173 p.add_argument(
"--bin", type=str, default=
"./correlated_rng",
174 help=
"Chemin du binaire C++ (défaut: ./correlated_rng).")
175 p.add_argument(
"--cpp", type=str, default=
"",
176 help=
"Chemin du fichier source C++ (optionnel). S'il est fourni et que --bin n'existe pas, on compile.")
177 p.add_argument(
"--dist", type=str, default=
"gaussian", help=
"Nom de distribution à passer au binaire.")
178 p.add_argument(
"--samples", type=int, default=15000, help=
"Nombre d'échantillons par cas.")
179 p.add_argument(
"--seed-base", type=int, default=12345, help=
"Seed de base (on utilise seed_base+i).")
180 p.add_argument(
"--cases", type=str, default=
"identity,equicorr,block,near",
181 help=
"Liste de cas séparés par des virgules parmi: identity,equicorr,block,near,custom")
182 p.add_argument(
"--n-dim", type=int, default=3, help=
"Dimension pour identity/equicorr/near.")
183 p.add_argument(
"--rho", type=float, default=0.5, help=
"Rho pour equicorr.")
184 p.add_argument(
"--near-rho", type=float, default=0.999, help=
"Rho pour near (quasi-singulier).")
185 p.add_argument(
"--block-rho", type=float, default=0.8, help=
"Corrélation entre y1 et y2 dans le cas 'block'.")
186 p.add_argument(
"--custom-matrix", type=str, default=
"", help=
"Chemin vers une matrice custom (format C++).")
187 p.add_argument(
"--out-dir", type=str, default=
"./cpp_test_out", help=
"Dossier de sortie (CSV + PNG).")
188 p.add_argument(
"--no-plots", action=
"store_true", help=
"Ne pas générer de figures.")
189 args = p.parse_args()
191 bin_path = Path(args.bin)
192 cpp_path = Path(args.cpp)
if args.cpp
else None
193 out_dir = Path(args.out_dir)
194 out_dir.mkdir(parents=
True, exist_ok=
True)
196 if not bin_path.exists()
and cpp_path
is not None:
199 if not bin_path.exists():
200 raise FileNotFoundError(
201 f
"Executable not found: {bin_path}\n"
202 f
"→ Compile-le (g++ -std=c++17 -O2 correlated_rng.cpp -o correlated_rng)\n"
203 f
"ou passe --cpp pour compiler automatiquement."
209 for idx, (name, R)
in enumerate(cases, 1):
210 print(f
"[{idx}/{len(cases)}] Cas '{name}' : génération de {args.samples} échantillons …")
211 Y =
sample_many(bin_path, R, n_samples=args.samples, seed_base=args.seed_base, dist=args.dist)
213 row = {
"case": name, **stats}
216 np.save(out_dir / f
"{name}_samples.npy", Y)
217 np.savetxt(out_dir / f
"{name}_target_R.txt", R, fmt=
"%.6f")
218 np.savetxt(out_dir / f
"{name}_empirical_corr.txt", np.corrcoef(Y, rowvar=
False), fmt=
"%.6f")
220 if not args.no_plots:
223 df = pd.DataFrame(rows).set_index(
"case")
224 csv_path = out_dir /
"summary.csv"
225 df.to_csv(csv_path, float_format=
"%.6g")
227 print(
"\n=== Résumé ===")
228 print(df.to_string())
229 print(f
"\nRésultats enregistrés dans: {out_dir.resolve()}")
230 print(f
"Résumé CSV: {csv_path.resolve()}")
std::vector< std::string > split(const std::string &s, char delimiter)
Splits a string into parts using a single-character delimiter.