From ee3bfe1e7584ac793683143b99eb328975a98f6c Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 11:56:37 -0400 Subject: [PATCH 01/53] Split E/B and COSEBIs rendering from sealed transform products --- src/sp_validation/b_modes.py | 53 +++++++++++++++++++++++++---- workflow/rules/cosmo_val.smk | 30 ++++++++++++++-- workflow/scripts/cv_cosebis.py | 22 +----------- workflow/scripts/cv_plot_cosebis.py | 21 ++++++++++++ workflow/scripts/cv_plot_pure_eb.py | 26 ++++++++++++++ workflow/scripts/cv_pure_eb.py | 29 ---------------- 6 files changed, 123 insertions(+), 58 deletions(-) create mode 100644 workflow/scripts/cv_plot_cosebis.py create mode 100644 workflow/scripts/cv_plot_pure_eb.py diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index 083dafd1f..ea9a80ca0 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -1310,12 +1310,13 @@ def save_pure_eb_results(results, output_path): output_path : str Output .npz file path """ - # Data vectors and covariance + # Include the sealed fine/reporting vectors and variances used by the + # comparison figures, not just the transformed modes. save_dict = { - key: results[key] for key in ("theta", "left_edges", "right_edges", "cov") + key: value + for key, value in results.items() + if key not in ("pte_matrices", "npatch") and value is not None } - for key in _EB_KEYS: - save_dict[key] = results[key] # PTE matrices for key, matrix in results.get("pte_matrices", {}).items(): @@ -1329,6 +1330,40 @@ def save_pure_eb_results(results, output_path): print(f"Saved pure E/B results to {output_path}") +def load_pure_eb_results(path): + """Read a sealed plot cache without transforms or catalogue access.""" + with np.load(path, allow_pickle=False) as data: + results = {k: data[k] for k in data.files if not k.startswith("pte_matrices_")} + results["pte_matrices"] = { + k.removeprefix("pte_matrices_"): data[k] + for k in data.files + if k.startswith("pte_matrices_") + } + results["npatch"] = int(results["npatch"]) if "npatch" in results else None + return results + + +def load_cosebis_results(path): + """Read all cached scale cuts and their integration-bin edges.""" + with np.load(path, allow_pickle=False) as data: + results = {} + for lo, hi in data["scale_cuts"]: + suffix = f"_{lo}_{hi}" + # Match the writer's scalar formatting (including integer cuts). + suffix = next( + k.removeprefix("En") + for k in data.files + if k.startswith("En_") + and tuple(map(float, k[3:].split("_"))) == (lo, hi) + ) + results[lo, hi] = { + k: data[k + suffix] + for k in ("En", "Bn", "cov", "chi2_E", "chi2_B", "pte_B") + } + edges = data["left_edges"], data["right_edges"] + return results, edges + + def _cosebis_result_to_dict(r, suffix=""): """Build save dict entries from a single COSEBIs result.""" return { @@ -1341,7 +1376,7 @@ def _cosebis_result_to_dict(r, suffix=""): } -def save_cosebis_results(results, output_path, fiducial_scale_cut=None): +def save_cosebis_results(results, output_path, fiducial_scale_cut=None, edges=None): """ Save COSEBIs data vectors and covariance to .npz. @@ -1359,10 +1394,14 @@ def save_cosebis_results(results, output_path, fiducial_scale_cut=None): is_multi = all(isinstance(k, tuple) for k in results) if is_multi and fiducial_scale_cut is not None: - # Select the single best-matching scale cut + # Keep the untagged fiducial entries for summary consumers, and every + # scanned cut for the plot-only heatmap. key = find_conservative_scale_cut_key(results, fiducial_scale_cut) save_dict = _cosebis_result_to_dict(results[key]) save_dict["scale_cut"] = np.array(key) + for sc, r in results.items(): + save_dict.update(_cosebis_result_to_dict(r, f"_{sc[0]}_{sc[1]}")) + save_dict["scale_cuts"] = np.array(list(results.keys())) elif is_multi: # Save all scale cuts with tagged keys save_dict = {} @@ -1375,6 +1414,8 @@ def save_cosebis_results(results, output_path, fiducial_scale_cut=None): if "scale_cut" in results: save_dict["scale_cut"] = np.array(results["scale_cut"]) + if edges is not None: + save_dict["left_edges"], save_dict["right_edges"] = edges np.savez(output_path, **save_dict) print(f"Saved COSEBIs results to {output_path}") diff --git a/workflow/rules/cosmo_val.smk b/workflow/rules/cosmo_val.smk index b416eac1a..c6078f145 100644 --- a/workflow/rules/cosmo_val.smk +++ b/workflow/rules/cosmo_val.smk @@ -388,7 +388,6 @@ rule cv_pure_eb: output: npz=cv_pure_eb_npz("{version}"), sacc=cv_pure_eb_sacc("{version}"), - **cv_pure_eb_figures("{version}"), params: version="{version}", blind=lambda w: blind_of(w.version), @@ -416,7 +415,6 @@ rule cv_cosebis: output: npz=cv_cosebis_npz("{version}"), sacc=cv_cosebis_sacc("{version}"), - **cv_cosebis_figures("{version}"), params: version="{version}", blind=lambda w: blind_of(w.version), @@ -434,6 +432,34 @@ rule cv_cosebis: "../scripts/cv_cosebis.py" +rule cv_plot_pure_eb: + """Render pure E/B figures without rerunning the transform.""" + input: + npz=lambda w: cv_pure_eb_npz(w.version), + plot_code=str(REPO_ROOT / "src/sp_validation/b_modes.py"), + output: + **cv_pure_eb_figures("{version}"), + params: + version="{version}", + fiducial_scale_cut=CV["fiducial_scale_cut"], + script: + "../scripts/cv_plot_pure_eb.py" + + +rule cv_plot_cosebis: + """Render COSEBIs figures without rerunning the transform.""" + input: + npz=lambda w: cv_cosebis_npz(w.version), + plot_code=str(REPO_ROOT / "src/sp_validation/b_modes.py"), + output: + **cv_cosebis_figures("{version}"), + params: + version="{version}", + fiducial_scale_cut=CV["fiducial_scale_cut"], + script: + "../scripts/cv_plot_cosebis.py" + + rule cv_summarize_bmodes: """Collect B-mode PTEs (pure E/B, COSEBIs, pseudo-Cl) into one summary.""" input: diff --git a/workflow/scripts/cv_cosebis.py b/workflow/scripts/cv_cosebis.py index 0223de82a..97308d4db 100644 --- a/workflow/scripts/cv_cosebis.py +++ b/workflow/scripts/cv_cosebis.py @@ -15,9 +15,6 @@ cosebis_scan_from_xi, find_conservative_scale_cut_key, log_bin_edges, - plot_cosebis_covariance_matrix, - plot_cosebis_modes, - plot_cosebis_scale_cut_heatmap, save_cosebis_results, ) from sp_validation.cosmo_val.sacc_writers import cosebis_to_sacc @@ -44,24 +41,7 @@ fiducial_key = find_conservative_scale_cut_key(results, fiducial_scale_cut) fiducial = results[fiducial_key] -plot_cosebis_modes( - fiducial, - version, - snakemake.output["figure_modes"], - fiducial_scale_cut=fiducial_scale_cut, -) -plot_cosebis_covariance_matrix( - fiducial, version, "analytic", snakemake.output["figure_covariance"] -) -plot_cosebis_scale_cut_heatmap( - results, - edges, - version, - snakemake.output["figure_scalecut_ptes"], - fiducial_scale_cut=fiducial_scale_cut, -) - -save_cosebis_results(results, snakemake.output["npz"], fiducial_scale_cut) +save_cosebis_results(results, snakemake.output["npz"], fiducial_scale_cut, edges) s = cosebis_to_sacc({0: sacc_io.get_nz(part, 0)}, part.metadata, fiducial, fiducial_key) sacc_io.save(s, snakemake.output["sacc"], derived_from=[part]) diff --git a/workflow/scripts/cv_plot_cosebis.py b/workflow/scripts/cv_plot_cosebis.py new file mode 100644 index 000000000..7e3f10da4 --- /dev/null +++ b/workflow/scripts/cv_plot_cosebis.py @@ -0,0 +1,21 @@ +"""Render every COSEBIs figure from the already-sealed multi-cut NPZ.""" + +from sp_validation.b_modes import ( + find_conservative_scale_cut_key, + load_cosebis_results, + plot_cosebis_covariance_matrix, + plot_cosebis_modes, + plot_cosebis_scale_cut_heatmap, +) + +p = snakemake.params +results, edges = load_cosebis_results(snakemake.input.npz) +cut = tuple(p.fiducial_scale_cut) +fiducial = results[find_conservative_scale_cut_key(results, cut)] +plot_cosebis_modes(fiducial, p.version, snakemake.output.figure_modes, cut) +plot_cosebis_covariance_matrix( + fiducial, p.version, "analytic", snakemake.output.figure_covariance +) +plot_cosebis_scale_cut_heatmap( + results, edges, p.version, snakemake.output.figure_scalecut_ptes, cut +) diff --git a/workflow/scripts/cv_plot_pure_eb.py b/workflow/scripts/cv_plot_pure_eb.py new file mode 100644 index 000000000..496996990 --- /dev/null +++ b/workflow/scripts/cv_plot_pure_eb.py @@ -0,0 +1,26 @@ +"""Render pure E/B figures from the already-sealed NPZ only.""" + +from sp_validation.b_modes import ( + covariance_label, + load_pure_eb_results, + plot_eb_covariance_matrix, + plot_integration_vs_reporting, + plot_pte_2d_heatmaps, + plot_pure_eb_correlations, +) + +p = snakemake.params +results = load_pure_eb_results(snakemake.input.npz) +kwargs = dict( + fiducial_xip_scale_cut=tuple(p.fiducial_scale_cut), + fiducial_xim_scale_cut=tuple(p.fiducial_scale_cut), +) +plot_integration_vs_reporting( + results, snakemake.output.figure_integration_vs_reporting, p.version +) +plot_pure_eb_correlations(results, snakemake.output.figure_xis, p.version, **kwargs) +plot_pte_2d_heatmaps(results, p.version, snakemake.output.figure_ptes, **kwargs) +plot_eb_covariance_matrix( + results["cov"], covariance_label(results["npatch"]), + snakemake.output.figure_covariance, p.version, +) diff --git a/workflow/scripts/cv_pure_eb.py b/workflow/scripts/cv_pure_eb.py index 25ee3f729..cf561e08d 100644 --- a/workflow/scripts/cv_pure_eb.py +++ b/workflow/scripts/cv_pure_eb.py @@ -15,11 +15,6 @@ from sp_validation.b_modes import ( calculate_eb_statistics, calculate_pure_eb_correlation, - covariance_label, - plot_eb_covariance_matrix, - plot_integration_vs_reporting, - plot_pte_2d_heatmaps, - plot_pure_eb_correlations, save_pure_eb_results, ) from sp_validation.cosmo_val.sacc_writers import pure_eb_to_sacc @@ -46,30 +41,6 @@ ) results = calculate_eb_statistics(results) -plot_integration_vs_reporting( - results, snakemake.output["figure_integration_vs_reporting"], version -) -plot_pure_eb_correlations( - results, - snakemake.output["figure_xis"], - version, - fiducial_xip_scale_cut=fiducial_scale_cut, - fiducial_xim_scale_cut=fiducial_scale_cut, -) -plot_pte_2d_heatmaps( - results, - version, - snakemake.output["figure_ptes"], - fiducial_xip_scale_cut=fiducial_scale_cut, - fiducial_xim_scale_cut=fiducial_scale_cut, -) -plot_eb_covariance_matrix( - results["cov"], - covariance_label(results["npatch"]), - snakemake.output["figure_covariance"], - version, -) - save_pure_eb_results(results, snakemake.output["npz"]) sacc_io.save( From 8e989a4ef3fa2e7152722cc3bb5c9fc9b9b71bee Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 12:45:22 -0400 Subject: [PATCH 02/53] Cache catalogue and PSF diagnostics; render the full suite without compute --- .../cosmo_val/catalog_characterization.py | 82 +++--- .../cosmo_val/diagnostic_plots.py | 252 ++++++++++++++++++ .../cosmo_val/diagnostic_products.py | 45 ++++ .../cosmo_val/psf_systematics.py | 18 +- workflow/rules/cosmo_val.smk | 198 ++++++++++---- workflow/rules/covariance.smk | 14 +- workflow/rules/twopoint.smk | 3 + workflow/scripts/covariance_plot.py | 7 + workflow/scripts/cv_footprints.py | 17 +- workflow/scripts/cv_objectwise_leakage.py | 27 +- workflow/scripts/cv_plot_2pcf.py | 15 -- workflow/scripts/cv_plot_diagnostic.py | 66 +++++ workflow/scripts/cv_plot_pure_eb.py | 6 +- workflow/scripts/cv_plot_rho_stats.py | 13 - workflow/scripts/cv_plot_tau_stats.py | 18 -- workflow/scripts/cv_ratio_xi_sys_xi.py | 18 -- workflow/scripts/cv_rho_tau_fits.py | 38 +-- workflow/scripts/cv_runner.py | 35 +-- workflow/scripts/cv_weights.py | 16 +- workflow/scripts/run_rho_tau.py | 1 + 20 files changed, 667 insertions(+), 222 deletions(-) create mode 100644 src/sp_validation/cosmo_val/diagnostic_plots.py create mode 100644 src/sp_validation/cosmo_val/diagnostic_products.py create mode 100644 workflow/scripts/covariance_plot.py delete mode 100644 workflow/scripts/cv_plot_2pcf.py create mode 100644 workflow/scripts/cv_plot_diagnostic.py delete mode 100644 workflow/scripts/cv_plot_rho_stats.py delete mode 100644 workflow/scripts/cv_plot_tau_stats.py delete mode 100644 workflow/scripts/cv_ratio_xi_sys_xi.py diff --git a/src/sp_validation/cosmo_val/catalog_characterization.py b/src/sp_validation/cosmo_val/catalog_characterization.py index e2df6d8e0..e6983f878 100644 --- a/src/sp_validation/cosmo_val/catalog_characterization.py +++ b/src/sp_validation/cosmo_val/catalog_characterization.py @@ -280,25 +280,32 @@ def calculate_ellipticity_dispersion(self, tomography=False): f"Ellipticity dispersion for tomo_bin_{tomo_bin_id} = {self._ellipticity_dispersion[ver][f'tomo_bin_{tomo_bin_id}']:.4f}" ) - def plot_footprints(self): - self.print_start("Plotting footprints:") - for ver in self.versions: - self.print_magenta(ver) - - fp = cs_plots.FootprintPlotter() + def footprint_maps(self): + """Build one sparse count map per version, shared by every region.""" + from .diagnostic_products import footprint_counts - for region in fp._regions: - out_path = self._output_path(f"footprint_{ver}_{region}.png") - if os.path.exists(out_path): - self.print_done(f"Skipping footprint plot, {out_path} exists") - else: - with self.results[ver].temporarily_read_data(): - hsp_map = fp.create_hsp_map( + for ver in self.versions: + with self.results[ver].temporarily_read_data(): + yield ( + ver, + footprint_counts( self.results[ver].dat_shear["RA"], self.results[ver].dat_shear["Dec"], - ) - fp.plot_region(hsp_map, fp._regions[region], outpath=out_path) - self.print_done("Footprint plot saved to " + out_path) + ), + ) + + def plot_footprints(self): + """Notebook convenience wrapper; workflow plots use persisted maps.""" + from .diagnostic_plots import plot_footprint + + for ver, count_map in self.footprint_maps(): + plot_footprint( + count_map, + { + region: self._output_path(f"footprint_{ver}_{region}.png") + for region in cs_plots.FootprintPlotter._regions + }, + ) def plot_ellipticity(self, nbins=200): out_path = self._output_path("ell_hist.png") @@ -349,37 +356,22 @@ def plot_ellipticity(self, nbins=200): cs_plots.show() self.print_done("Ellipticity histograms saved to " + out_path) - def plot_weights(self, nbins=200): - out_path = self._output_path("weight_hist.png") - if os.path.exists(out_path): - self.print_done(f"Skipping weight histograms, {out_path} exists") - else: - self.print_start("Computing weight histograms:") - - plt.figure(figsize=(10, 7)) - for ver in self.versions: - self.print_magenta(ver) - with self.results[ver].temporarily_read_data(): - w = self._read_shear_cols(ver, "w_col") + def weight_histograms(self, nbins=200): + """Return weight-weighted counts and edges, with independent bins per version.""" + histograms = {} + for ver in self.versions: + with self.results[ver].temporarily_read_data(): + w = self._read_shear_cols(ver, "w_col") + histograms[ver] = np.histogram(w, bins=nbins, weights=w) + return histograms - plt.hist( - w, - bins=nbins, - density=False, - histtype="step", - weights=w, - label=ver, - color=self.cc[ver]["colour"], - ) + def plot_weights(self, nbins=200): + """Notebook convenience wrapper; workflow plots read cached histograms.""" + from .diagnostic_plots import plot_weights - plt.xlabel("$w$") - plt.ylabel("frequency") - plt.yscale("log") - plt.legend() - # plt.xlim([-0.01, 1.2]) - cs_plots.savefig(out_path, close_fig=False) - cs_plots.show() - self.print_done("Weight histograms saved to " + out_path) + plot_weights( + self.weight_histograms(nbins), self.cc, self._output_path("weight_hist.png") + ) def plot_separation(self, nbins=200): self.print_start("Separation histograms") diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py new file mode 100644 index 000000000..59ba45ef0 --- /dev/null +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -0,0 +1,252 @@ +"""Catalogue-free renderers for cached validation diagnostics. + +Inputs are arrays, summary dictionaries or a sparse count map, never a +CosmologyValidation instance. No renderer skips an existing output. +""" + +import matplotlib.pyplot as plt +import numpy as np +from cs_util.plots import FootprintPlotter + + +def plot_footprint(count_map, outputs): + """Render every requested region from one cached HealSparse count map.""" + plotter = FootprintPlotter() + for region, path in outputs.items(): + plotter.plot_region(count_map, plotter._regions[region], outpath=str(path)) + plt.close() + + +def plot_weights(histograms, styles, output): + """Draw weight-weighted bin sums; binning is fixed in the compute step.""" + fig, ax = plt.subplots(figsize=(10, 7)) + for ver, (counts, edges) in histograms.items(): + ax.stairs(counts, edges, label=ver, color=styles[ver]["colour"]) + ax.set(xlabel="$w$", ylabel="frequency", yscale="log") + ax.legend() + fig.savefig(output, bbox_inches="tight") + plt.close(fig) + + +def plot_objectwise_leakage(coefficients, styles, output): + """Draw cached regression and scale-dependent alpha summaries. + + Each coefficient is a (nominal value, standard deviation) pair. + """ + fig, ax = plt.subplots(figsize=(15, 15)) + summaries = { + "alpha_mean": (r"$\bar\alpha$", "-", "full"), + "alpha_1": (r"$\alpha(\theta_{\rm min})$", "--", "none"), + "alpha_0": (r"$\alpha(0)$", ":", "left"), + } + handles = [] + for ver, bins in coefficients.items(): + style = styles[ver] + for coeff in bins.values(): + x, dx = coeff["aii_mean"] + for key, (_, ls, fillstyle) in summaries.items(): + y, dy = coeff[key] + eb = ax.errorbar( + x, + y, + xerr=dx, + yerr=dy, + fmt=style["marker"], + color=style["colour"], + fillstyle=fillstyle, + ) + eb[-1][0].set_linestyle(ls) + handles.append( + plt.Line2D( + [], + [], + marker=style["marker"], + ls="", + color=style["colour"], + label=style.get("label", ver), + ) + ) + handles += [ + plt.Line2D( + [], + [], + marker="o", + ls=ls, + color="grey", + fillstyle=fs, + label=label, + ) + for label, ls, fs in summaries.values() + ] + ax.plot([-0.02, 0.02], [-0.02, 0.02], "k:", linewidth=0.5) + ax.legend(handles=handles) + ax.set( + xlabel=r"$\langle a_{ii} \rangle$ (object-wise)", + ylabel=r"$\alpha$ (scale-dependent, $\tau_0 / \rho_0$)", + ) + fig.savefig(output, bbox_inches="tight") + plt.close(fig) + + +def plot_rho_tau_fits(products, styles, contours, samples, summary): + """Render posterior contours and propagated PSF errors from cached arrays.""" + from shear_psf_leakage import plots as psf_plots + + versions = list(products) + psf_plots.plot_contours( + [products[v]["flat_samples"] for v in versions], + names=["x0", "x1", "x2"], + labels=[r"\alpha", r"\beta", r"\eta"], + savefig=str(contours), + legend_labels=versions, + legend_loc="upper right", + contour_colors=[styles[v]["colour"] for v in versions], + markers={"x0": 0, "x1": 1, "x2": 1}, + show=False, + close=True, + ) + for path, draw_samples in ((samples, True), (summary, False)): + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + for ver, data in products.items(): + theta, colour = data["theta"], styles[ver]["colour"] + for ax, component, sign in zip(axes, ("plus", "minus"), ("+", "-")): + if draw_samples: + ax.plot( + theta, + data[f"samples_{component}"][-100:].T, + color=colour, + alpha=0.3, + ) + else: + ax.plot(theta, data[f"mean_{component}"], color=colour, label=ver) + lo, hi = data[f"quantiles_{component}"] + ax.plot(theta, lo, color=colour) + ax.plot(theta, hi, color=colour) + ax.fill_between(theta, lo, hi, color=colour, alpha=0.3) + ax.set( + xscale="log", + yscale="log", + xlabel=r"$\theta$ [arcmin]", + ylabel=rf"$\xi^{{\rm PSF,sys}}_{sign}(\theta)$", + ) + if not draw_samples: + axes[0].legend() + fig.savefig(path, bbox_inches="tight") + plt.close(fig) + + +def plot_ratio_xi_sys_xi( + products, correlations, styles, output, offset=0.1, threshold=0.1 +): + """Draw PSF/signal ratios directly from sealed reporting correlations. + + Ratios aren't persisted separately: the only cosmological input stays the + existing sealed SACC. Propagating cached variances is just presentation. + """ + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + for idx, (ver, data) in enumerate(products.items()): + gg = correlations[ver] + theta = gg.meanr * (1 + offset * (idx - (len(products) - 1) / 2)) + for ax, xi, var_xi, component, sign in zip( + axes, + (gg.xip, gg.xim), + (gg.varxip, gg.varxim), + ("plus", "minus"), + ("+", "-"), + ): + mean, var = data[f"mean_{component}"], data[f"var_{component}"] + ax.errorbar( + theta, + mean / xi, + yerr=np.sqrt(var / xi**2 + mean**2 * var_xi / xi**4), + fmt=styles[ver]["marker"], + color=styles[ver]["colour"], + markersize=3, + capsize=2, + label=ver, + ) + ax.set( + xscale="log", + xlabel=r"$\theta$ [arcmin]", + ylabel=rf"$\xi^{{\rm PSF,sys}}_{sign}/\xi_{sign}$", + ) + for ax in axes: + ax.axhspan(-threshold, threshold, color="black", alpha=0.1) + ax.legend() + fig.savefig(output, bbox_inches="tight") + plt.close(fig) + + +def plot_2pcf(correlations, styles, outputs): + """Draw sealed reporting xi±, both plain and theta-weighted.""" + for times_theta, output in enumerate(outputs): + fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + for ver, gg in correlations.items(): + factor = gg.meanr if times_theta else 1 + for ax, xi, var, sign in zip( + axes, (gg.xip, gg.xim), (gg.varxip, gg.varxim), ("+", "-") + ): + ax.errorbar( + gg.meanr, + xi * factor, + yerr=np.sqrt(var) * factor, + fmt=styles[ver]["marker"], + color=styles[ver]["colour"], + label=ver, + ) + ax.set( + xscale="log", + xlabel=r"$\theta$ [arcmin]", + ylabel=(r"$\theta$ " if times_theta else "") + + rf"$\xi_{sign}(\theta)$", + ) + axes[0].legend() + fig.savefig(output, bbox_inches="tight") + plt.close(fig) + + +def plot_psf_stats(tables, styles, output, kind, covariances=()): + """Overlay cached rho/tau FITS columns, with optional tau covariance errors.""" + indices = (0, 1, 2, 3, 4, 5) if kind == "rho" else (0, 2, 5) + fig, axes = plt.subplots( + 2 if kind == "rho" else 1, + 3, + figsize=(15, 10 if kind == "rho" else 6), + squeeze=False, + ) + for i, stat in enumerate(indices): + ax = axes.flat[i] + for j, (ver, table) in enumerate(tables.items()): + theta = table["theta"] + key = f"{kind}_{stat}_p" + variance = table[f"var{key}"] + if covariances: + n = len(theta) + variance = np.diag(covariances[j])[i * n : (i + 1) * n] + ax.errorbar( + theta, + table[key], + yerr=np.sqrt(variance), + color=styles[ver]["colour"], + label=ver, + capsize=2, + ) + ax.set( + xscale="log", + xlabel=r"$\theta$ [arcmin]", + ylabel=rf"$\{kind}_{{{stat},+}}(\theta)$", + ) + ax.legend() + fig.savefig(output, bbox_inches="tight") + plt.close(fig) + + +def plot_covariance(matrix, output): + """Draw a correlation matrix from the cached analytic covariance.""" + from sp_validation.statistics import corr_from_cov + + fig, ax = plt.subplots(figsize=(9, 9)) + image = ax.imshow(corr_from_cov(matrix), vmin=-1, vmax=1, cmap="RdBu_r") + fig.colorbar(image, ax=ax, label="correlation") + fig.savefig(output, bbox_inches="tight") + plt.close(fig) diff --git a/src/sp_validation/cosmo_val/diagnostic_products.py b/src/sp_validation/cosmo_val/diagnostic_products.py new file mode 100644 index 000000000..a70fb7725 --- /dev/null +++ b/src/sp_validation/cosmo_val/diagnostic_products.py @@ -0,0 +1,45 @@ +"""Small non-cosmological diagnostic products shared by compute and plot steps.""" + +import healpy as hp +import healsparse +import numpy as np + + +def footprint_counts(ra, dec, nside_coverage=32, nside_sparse=2048): + """Bin coordinates once, using a finite HealSparse sentinel for valid pixels.""" + pixels, counts = np.unique( + hp.ang2pix(nside_sparse, ra, dec, lonlat=True, nest=True), return_counts=True + ) + count_map = healsparse.HealSparseMap.make_empty( + nside_coverage, nside_sparse, dtype=np.float32 + ) + count_map[pixels] = counts.astype(np.float32) + return count_map + + +def save_footprint(count_map, path): + """Store occupied nested pixels, counts and both HealSparse resolutions. + + A sparse NPZ avoids writing the plotter's NaN sentinel to a FITS header + (CFITSIO cannot encode it). No per-object coordinates are retained. + """ + pixels = count_map.valid_pixels + np.savez_compressed( + path, + pixels=pixels, + counts=count_map[pixels], + nside_coverage=count_map.nside_coverage, + nside_sparse=count_map.nside_sparse, + ) + + +def load_footprint(path): + """Reconstruct the sparse map from occupied pixels, without a catalogue.""" + with np.load(path, allow_pickle=False) as data: + count_map = healsparse.HealSparseMap.make_empty( + int(data["nside_coverage"]), + int(data["nside_sparse"]), + dtype=np.float32, + ) + count_map[data["pixels"]] = data["counts"] + return count_map diff --git a/src/sp_validation/cosmo_val/psf_systematics.py b/src/sp_validation/cosmo_val/psf_systematics.py index 991567fbb..ef0e004b6 100644 --- a/src/sp_validation/cosmo_val/psf_systematics.py +++ b/src/sp_validation/cosmo_val/psf_systematics.py @@ -303,7 +303,23 @@ def _objectwise_leakage_bin(self, results_obj, ver, tomo_bin_id, mix, order): else: self.print_cyan(f"Computing object-wise leakage regression, {suffix}") with results_obj.temporarily_read_data(selection=selection): - results_obj.PSF_leakage(mix=mix, order=order, suffix=suffix) + # Use the regression primitive, not PSF_leakage: that driver + # also fits unrelated 1D models and writes exploratory plots. + e, weights = results_obj.get_ellipticity_weights() + x = [ + results_obj._dat[results_obj._params[key]] + for key in ("e1_PSF_col", "e2_PSF_col") + ] + results_obj.par_best_fit = leakage.corr_2d( + x, + e, + weights=weights, + order=order, + mix=mix, + stats_file=results_obj._stats_file, + verbose=results_obj._params["verbose"], + ) + leakage.save_to_file(results_obj.par_best_fit, out_path) par_best_fit = results_obj.par_best_fit a11 = ufloat(par_best_fit["a11"].value, par_best_fit["a11"].stderr) diff --git a/workflow/rules/cosmo_val.smk b/workflow/rules/cosmo_val.smk index c6078f145..eb37511e4 100644 --- a/workflow/rules/cosmo_val.smk +++ b/workflow/rules/cosmo_val.smk @@ -22,13 +22,11 @@ # NaMaster pseudo-Cℓ) are what assembly puts in the terminal file, replacing # the estimates a part was born with. # -# Methods that only emit figures, or whose figure paths derive from internal -# handler state (rho/tau plots, rho_tau_fits, objectwise leakage, 2pcf -# overlay), declare a sentinel under COSMO_VAL/snakemake_sentinels so they stay -# DAG-trackable. Lazy cv state the original code never persists (c1/c2, -# xi_psf_sys) is either materialized to a small JSON (additive bias) or -# recomputed in the one rule that needs it — recompute is cheap next to the -# science it depends on. See workflow/scripts/cv_runner.py. +# Every compute rule writes a data product. Every figure is declared by a +# separate rule consuming those products, with no catalogue inputs or lazy cv +# state. cosmo_val_plots gathers all figures; cosmo_val_all also assembles the +# analysis products. Cosmological caches contain only already-sealed vectors. +# Plot modules are explicit inputs so edits to them invalidate figures only. CV = config["cosmo_val"] CV_VERSIONS = config["versions"] @@ -183,6 +181,31 @@ def cv_analysis_sacc(version): # The CosmologyValidation constructor kwargs every cv_runner rule passes. CV_INIT = cv_init_params(config) +CV_STYLES = { + v: {k: catalogue_entry(v).get(k, default) for k, default in + (("colour", "C0"), ("marker", "o"), ("label", v))} + for v in CV_VERSIONS +} +CV_PLOT_CODE = str(REPO_ROOT / "src/sp_validation/cosmo_val/diagnostic_plots.py") +CV_REGIONS = ("NGC", "SGC", "fullsky") +CV_PSF_PRODUCTS = [str(COSMO_VAL / f"psf_fit_{v}_{CV_BINNING}.npz") for v in CV_VERSIONS] +CV_FOOTPRINT_MAPS = [str(COSMO_VAL / f"footprint_{v}.npz") for v in CV_VERSIONS] +CV_FOOTPRINT_FIGURES = [ + str(COSMO_VAL / f"footprint_{v}_{r}.png") for v in CV_VERSIONS for r in CV_REGIONS +] + + +def cv_shear_catalogues(): + """Catalogue inputs belong to compute rules only.""" + return [str(Path(catalogue_entry(v)["subdir"]) / catalogue_entry(v)["shear"]["path"]) + for v in CV_VERSIONS] + + +def cv_tau_covariances(): + method = CV.get("cov_estimate_method", "th") + return [str(COSMO_VAL / "rho_tau_stats" / + f"cov_tau_{cv_basename(v, CV_FIDUCIAL)}_{method}.npy") + for v in CV_VERSIONS] # --------------------------------------------------------------------------- @@ -198,29 +221,35 @@ CV_INIT = cv_init_params(config) rule cv_plot_rho_stats: """Overlay rho statistics across all versions.""" input: - rho=[cv_rho_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + stats=[cv_rho_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + covariances=[], + plot_code=CV_PLOT_CODE, output: - sentinel=str(CV_SENTINELS / "plot_rho_stats.done"), + figure=str(COSMO_VAL / "rho_tau_stats/rho_stats.png"), params: - cv_init=CV_INIT, + kind="rho", + styles=CV_STYLES, resources: runtime=20, script: - "../scripts/cv_plot_rho_stats.py" + "../scripts/cv_plot_diagnostic.py" rule cv_plot_tau_stats: """Overlay tau statistics across all versions.""" input: - tau=[cv_tau_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + stats=[cv_tau_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + covariances=cv_tau_covariances(), + plot_code=CV_PLOT_CODE, output: - sentinel=str(CV_SENTINELS / "plot_tau_stats.done"), + figure=str(COSMO_VAL / "rho_tau_stats/tau_stats.png"), params: - cv_init=CV_INIT, + kind="tau", + styles=CV_STYLES, resources: runtime=20, script: - "../scripts/cv_plot_tau_stats.py" + "../scripts/cv_plot_diagnostic.py" rule cv_rho_tau_fits: @@ -228,8 +257,11 @@ rule cv_rho_tau_fits: input: rho=[cv_rho_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], tau=[cv_tau_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + covariances=cv_tau_covariances(), + rho_covariances=[str(COSMO_VAL / "rho_tau_stats" / + f"cov_rho_{cv_basename(v, CV_FIDUCIAL)}_jk.npy") for v in CV_VERSIONS], output: - sentinel=str(CV_SENTINELS / "rho_tau_fits.done"), + products=CV_PSF_PRODUCTS, params: cv_init=CV_INIT, resources: @@ -244,9 +276,11 @@ rule cv_rho_tau_fits: # --------------------------------------------------------------------------- rule cv_footprints: - """Per-version survey footprint maps.""" + """Cache per-version sparse survey count maps.""" + input: + catalogues=cv_shear_catalogues(), output: - sentinel=str(CV_SENTINELS / "footprints.done"), + maps=CV_FOOTPRINT_MAPS, params: cv_init=CV_INIT, resources: @@ -257,12 +291,14 @@ rule cv_footprints: rule cv_objectwise_leakage: - """Object-wise PSF-leakage regression vs scale-dependent alpha (all versions).""" + """Cache object-wise regression and scale-dependent alpha summaries.""" input: + catalogues=cv_shear_catalogues(), + covariances=cv_tau_covariances(), rho=[cv_rho_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], tau=[cv_tau_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], output: - sentinel=str(CV_SENTINELS / "objectwise_leakage.done"), + coefficients=str(COSMO_VAL / "leakage_coefficients.json"), params: cv_init=CV_INIT, threads: 12 @@ -274,9 +310,11 @@ rule cv_objectwise_leakage: rule cv_weights: - """Weighted shear-weight histograms across versions.""" + """Cache weighted shear-weight histogram counts and edges.""" + input: + catalogues=cv_shear_catalogues(), output: - weight_hist=str(COSMO_VAL / "weight_hist.png"), + histograms=str(COSMO_VAL / "weight_hist.npz"), params: cv_init=CV_INIT, resources: @@ -295,6 +333,61 @@ rule cv_weights: # standalone diagnostic (the original driver's cv.calculate_additive_bias() # call); the xi rule does not depend on it. +rule cv_plot_footprints: + input: + maps=CV_FOOTPRINT_MAPS, + plot_code=CV_PLOT_CODE, + output: + figures=CV_FOOTPRINT_FIGURES, + params: + kind="footprints", + styles=CV_STYLES, + regions=CV_REGIONS, + script: + "../scripts/cv_plot_diagnostic.py" + + +rule cv_plot_weights: + input: + histograms=rules.cv_weights.output.histograms, + plot_code=CV_PLOT_CODE, + output: + figure=str(COSMO_VAL / "weight_hist.png"), + params: + kind="weights", + styles=CV_STYLES, + script: + "../scripts/cv_plot_diagnostic.py" + + +rule cv_plot_objectwise_leakage: + input: + coefficients=rules.cv_objectwise_leakage.output.coefficients, + plot_code=CV_PLOT_CODE, + output: + figure=str(COSMO_VAL / "leakage_coefficients.png"), + params: + kind="objectwise_leakage", + styles=CV_STYLES, + script: + "../scripts/cv_plot_diagnostic.py" + + +rule cv_plot_rho_tau_fits: + input: + products=CV_PSF_PRODUCTS, + plot_code=CV_PLOT_CODE, + output: + contours=str(COSMO_VAL / "rho_tau_stats/contours_tau_stat.png"), + samples=str(COSMO_VAL / "rho_tau_stats/xi_psf_sys_tomography_False_sample.png"), + summary=str(COSMO_VAL / "rho_tau_stats/xi_psf_sys_tomography_False_mean_and_std.png"), + params: + kind="rho_tau_fits", + styles=CV_STYLES, + script: + "../scripts/cv_plot_diagnostic.py" + + rule cv_additive_bias: """Weighted mean ellipticity c1/c2 per version (standalone diagnostic).""" output: @@ -309,35 +402,40 @@ rule cv_additive_bias: rule cv_plot_2pcf: - """n_pairs / xi± overlay across versions, from the reporting parts.""" + """Xi± overlays across versions, from the sealed reporting parts.""" input: xi=[cv_xi_sacc(v, "reporting") for v in CV_VERSIONS], + plot_code=CV_PLOT_CODE, output: - sentinel=str(CV_SENTINELS / "plot_2pcf.done"), + figures=[str(COSMO_VAL / name) for name in + ("xi_pm_tomography_False.png", "xi_pm_theta_tomography_False.png")], params: - cv_init=CV_INIT, + kind="xi", + styles=CV_STYLES, + blinds=[blind_of(v) for v in CV_VERSIONS], resources: runtime=20, script: - "../scripts/cv_plot_2pcf.py" + "../scripts/cv_plot_diagnostic.py" rule cv_ratio_xi_sys_xi: """Ratio of PSF systematics (xi_psf_sys) to the cosmic-shear signal (xi+).""" input: xi=[cv_xi_sacc(v, "reporting") for v in CV_VERSIONS], - rho=[cv_rho_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], - tau=[cv_tau_stats(v, CV_FIDUCIAL) for v in CV_VERSIONS], + products=CV_PSF_PRODUCTS, + plot_code=CV_PLOT_CODE, output: - ratio=str(COSMO_VAL / "ratio_xi_sys_xi.png"), + figure=str(COSMO_VAL / "ratio_xi_sys_xi.png"), params: - offset=0.1, - cv_init=CV_INIT, + kind="ratio", + styles=CV_STYLES, + blinds=[blind_of(v) for v in CV_VERSIONS], resources: mem_mb=16000, runtime=120, script: - "../scripts/cv_ratio_xi_sys_xi.py" + "../scripts/cv_plot_diagnostic.py" # --------------------------------------------------------------------------- @@ -357,6 +455,7 @@ rule cv_plot_pseudo_cl: input: pseudo_cl=[cv_pseudo_cl_analysis_sacc(v) for v in CV_VERSIONS], pseudo_cl_cov=[cv_pseudo_cl_cov(v) for v in CV_VERSIONS], + plot_code=str(REPO_ROOT / "src/sp_validation/cosmo_val/pseudo_cl.py"), output: **cv_pseudo_cl_figures(), params: @@ -547,23 +646,28 @@ rule assemble_sacc_all: # Aggregate target: the whole validation suite # --------------------------------------------------------------------------- +rule cosmo_val_plots: + """Render every validation figure from cached data products.""" + input: + rules.cv_plot_rho_stats.output, + rules.cv_plot_tau_stats.output, + rules.cv_plot_rho_tau_fits.output, + rules.cv_plot_footprints.output, + rules.cv_plot_objectwise_leakage.output, + rules.cv_plot_weights.output, + rules.cv_plot_2pcf.output, + rules.cv_ratio_xi_sys_xi.output, + [path for v in CV_VERSIONS for path in cv_pure_eb_figures(v).values()], + [path for v in CV_VERSIONS for path in cv_cosebis_figures(v).values()], + list(cv_pseudo_cl_figures().values()) if CV.get("include_pseudo_cl", False) else [], + [cv_xi_cov(v).removesuffix(".dat") + "_corr_plot.pdf" for v in CV_VERSIONS], + [cv_xi_cov_integration(v).removesuffix(".dat") + "_corr_plot.pdf" for v in CV_VERSIONS], + + rule cosmo_val_all: - """Run the full CosmologyValidation diagnostic suite.""" + """Run the full suite, including every plot-only rule.""" input: - # PSF diagnostics - str(CV_SENTINELS / "footprints.done"), - str(CV_SENTINELS / "plot_rho_stats.done"), - str(CV_SENTINELS / "plot_tau_stats.done"), - str(CV_SENTINELS / "rho_tau_fits.done"), - # Shear diagnostics - str(CV_SENTINELS / "objectwise_leakage.done"), - str(COSMO_VAL / "weight_hist.png"), + rules.cosmo_val_plots.input, str(COSMO_VAL / "additive_bias.json"), - # Two-point - str(CV_SENTINELS / "plot_2pcf.done"), - str(COSMO_VAL / "ratio_xi_sys_xi.png"), - # B-modes str(COSMO_VAL / "bmode_summary.json"), - list(cv_pseudo_cl_figures().values()) if CV.get("include_pseudo_cl", False) else [], - # Terminal analysis file: the assembled {version}.sacc per version [cv_analysis_sacc(v) for v in CV_VERSIONS], diff --git a/workflow/rules/covariance.smk b/workflow/rules/covariance.smk index c30797601..84e9f14a8 100644 --- a/workflow/rules/covariance.smk +++ b/workflow/rules/covariance.smk @@ -123,7 +123,6 @@ rule covariance_ini: params: outdir=lambda w: covariance_dir(*_onecov_args(w)), out_filename=lambda w: covariance_base(*_onecov_args(w)) + ".dat", - out_plot_filename=lambda w: covariance_base(*_onecov_args(w)) + "_corr_plot.pdf", ng_value=lambda w: "1" if w.gaussian == "ng" else "0", omega_m=PLANCK18["Omega_m"], omega_v=PLANCK18["Omega_v"], @@ -173,7 +172,7 @@ directory = {params.outdir} file = {params.out_filename} style = matrix save_configs = False -corrmatrix_plot = {params.out_plot_filename} +corrmatrix_plot = False [covTHETAspace settings] theta_min = {wildcards.min_sep} @@ -287,6 +286,17 @@ rule covariance_run: """ +rule covariance_plot: + """Render the correlation matrix from cached OneCovariance output only.""" + input: + matrix=cov_output(".dat"), + plot_code=str(REPO_ROOT / "src/sp_validation/cosmo_val/diagnostic_plots.py"), + output: + figure=cov_output("_corr_plot.pdf"), + script: + "../scripts/covariance_plot.py" + + def fiducial_covariance_outputs(mask_suffix="", tomo_suffix=""): """Return the covariance matrices for the fiducial version: non-Gaussian on the reporting grid, Gaussian on the integration grid.""" diff --git a/workflow/rules/twopoint.smk b/workflow/rules/twopoint.smk index 5d6450e60..13e1a3f60 100644 --- a/workflow/rules/twopoint.smk +++ b/workflow/rules/twopoint.smk @@ -48,11 +48,14 @@ rule rho_tau_stats: output: rho_stats=str(COSMO_VAL / "rho_tau_stats/rho_stats_{version}_tomo_bin_all_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}.fits"), tau_stats=str(COSMO_VAL / "rho_tau_stats/tau_stats_{version}_tomo_bin_all_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}.fits"), + covariance=str(COSMO_VAL / ("rho_tau_stats/cov_tau_{version}_tomo_bin_all_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}_" + config.get("cosmo_val", {}).get("cov_estimate_method", "th") + ".npy")), + rho_covariance=str(COSMO_VAL / "rho_tau_stats/cov_rho_{version}_tomo_bin_all_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}_jk.npy"), # Born-as-SACC ρ/τ part, written alongside the FITS. rho_tau=str(COSMO_VAL / "rho_tau_stats/rho_tau_{version}_tomo_bin_all_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}.sacc"), threads: 48 params: ver="{version}", + cov_estimate_method=config.get("cosmo_val", {}).get("cov_estimate_method", "th"), min_sep="{min_sep}", max_sep="{max_sep}", nbins="{nbins}", diff --git a/workflow/scripts/covariance_plot.py b/workflow/scripts/covariance_plot.py new file mode 100644 index 000000000..2af67d05c --- /dev/null +++ b/workflow/scripts/covariance_plot.py @@ -0,0 +1,7 @@ +"""Plot an analytic covariance without invoking its producer.""" + +import numpy as np + +from sp_validation.cosmo_val.diagnostic_plots import plot_covariance + +plot_covariance(np.loadtxt(snakemake.input.matrix), snakemake.output.figure) diff --git a/workflow/scripts/cv_footprints.py b/workflow/scripts/cv_footprints.py index e4a1af6a4..90d360150 100644 --- a/workflow/scripts/cv_footprints.py +++ b/workflow/scripts/cv_footprints.py @@ -1,13 +1,12 @@ -"""Rule cv_footprints: plot per-version survey footprints. +"""Build one sparse count map per catalogue; no figures.""" -Leaf rule. Reads the shear catalog (RA/Dec) for each version; writes -footprint_{version}_{region}.png. Sentinel output keeps the DAG trackable -because region names are internal to FootprintPlotter. -""" +from cv_runner import make_cv, verify_outputs -from cv_runner import _unbuffer_streams, make_cv, touch_sentinels +from sp_validation.cosmo_val.diagnostic_products import save_footprint -_unbuffer_streams() cv = make_cv(snakemake) -cv.plot_footprints() -touch_sentinels(snakemake) +for (_, count_map), path in zip( + cv.footprint_maps(), snakemake.output.maps, strict=True +): + save_footprint(count_map, path) +verify_outputs(snakemake) diff --git a/workflow/scripts/cv_objectwise_leakage.py b/workflow/scripts/cv_objectwise_leakage.py index ae0f012ab..2c5701919 100644 --- a/workflow/scripts/cv_objectwise_leakage.py +++ b/workflow/scripts/cv_objectwise_leakage.py @@ -1,15 +1,20 @@ -"""Rule cv_objectwise_leakage: object-wise vs scale-dependent PSF leakage. +"""Cache regression coefficients and alpha summaries, without rendering.""" -plot_objectwise_leakage triggers calculate_objectwise_leakage, which itself -triggers calculate_scale_dependent_leakage — the whole leakage chain runs here. -Writes leakage_{version}/ products (the object-wise regression .pkl is the -durable artifact) and leakage_coefficients.png. Sentinel-tracked because the -per-version leakage product paths are built inside shear_psf_leakage. -""" +import json -from cv_runner import _unbuffer_streams, make_cv, touch_sentinels +from cv_runner import make_cv, prepare_cached_psf, verify_outputs -_unbuffer_streams() cv = make_cv(snakemake) -cv.plot_objectwise_leakage() -touch_sentinels(snakemake) +prepare_cached_psf(cv) +cv.calculate_objectwise_leakage(tomography=False) +cv.calculate_alpha_leakage_summaries(tomography=False) +coefficients = { + ver: { + bin_id: {k: [v.nominal_value, v.std_dev] for k, v in coeff.items()} + for bin_id, coeff in bins.items() + } + for ver, bins in cv.leakage_coeff.items() +} +with open(snakemake.output.coefficients, "w") as stream: + json.dump(coefficients, stream, indent=2) +verify_outputs(snakemake) diff --git a/workflow/scripts/cv_plot_2pcf.py b/workflow/scripts/cv_plot_2pcf.py deleted file mode 100644 index 5bb3ac329..000000000 --- a/workflow/scripts/cv_plot_2pcf.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Rule cv_plot_2pcf: xi± overlay across versions. - -Draws each version's reporting ξ± part (the declared inputs) for the -non-tomographic ("all", "all") pair; plot_2pcf draws the parts it is handed -rather than measuring. Writes figures under the output dir. Sentinel-tracked: -plot_2pcf emits several figures whose names are internal. -""" - -from cv_runner import _unbuffer_streams, make_cv, take_reporting_parts, touch_sentinels - -_unbuffer_streams() -cv = make_cv(snakemake) -take_reporting_parts(cv, snakemake.input["xi"]) -cv.plot_2pcf(tomography=False, show=False) -touch_sentinels(snakemake) diff --git a/workflow/scripts/cv_plot_diagnostic.py b/workflow/scripts/cv_plot_diagnostic.py new file mode 100644 index 000000000..7c7683b21 --- /dev/null +++ b/workflow/scripts/cv_plot_diagnostic.py @@ -0,0 +1,66 @@ +"""Catalogue-free plotting entry point for cached scalar/PSF/map diagnostics.""" + +import json + +import numpy as np +from astropy.io import fits + +from sp_validation import sacc_io +from sp_validation.cosmo_val import diagnostic_plots as plots +from sp_validation.cosmo_val.diagnostic_products import load_footprint + +p, inputs, outputs = snakemake.params, snakemake.input, snakemake.output +styles = dict(p.styles) +versions = list(styles) + + +def psf_products(): + products = {} + for ver, path in zip(versions, inputs.products, strict=True): + with np.load(path, allow_pickle=False) as data: + products[ver] = dict(data) + return products + + +if p.kind == "weights": + with np.load(inputs.histograms, allow_pickle=False) as data: + histograms = { + ver: (data[f"counts_{i}"], data[f"edges_{i}"]) + for i, ver in enumerate(data["versions"].tolist()) + } + plots.plot_weights(histograms, styles, outputs.figure) +elif p.kind == "footprints": + for i, (ver, path) in enumerate(zip(versions, inputs.maps, strict=True)): + plots.plot_footprint( + load_footprint(path), + { + region: outputs.figures[i * len(p.regions) + j] + for j, region in enumerate(p.regions) + }, + ) +elif p.kind == "objectwise_leakage": + with open(inputs.coefficients) as stream: + coefficients = json.load(stream) + plots.plot_objectwise_leakage(coefficients, styles, outputs.figure) +elif p.kind == "rho_tau_fits": + plots.plot_rho_tau_fits( + psf_products(), styles, outputs.contours, outputs.samples, outputs.summary + ) +elif p.kind in ("ratio", "xi"): + correlations = { + ver: sacc_io.xi_correlation(sacc_io.load(path, blind=blind), grid="reporting") + for ver, path, blind in zip(versions, inputs.xi, p.blinds, strict=True) + } + if p.kind == "ratio": + plots.plot_ratio_xi_sys_xi(psf_products(), correlations, styles, outputs.figure) + else: + plots.plot_2pcf(correlations, styles, outputs.figures) +elif p.kind in ("rho", "tau"): + tables = { + ver: fits.getdata(path) + for ver, path in zip(versions, inputs.stats, strict=True) + } + covariances = [np.load(path) for path in inputs.covariances] + plots.plot_psf_stats(tables, styles, outputs.figure, p.kind, covariances) +else: + raise ValueError(f"Unknown diagnostic: {p.kind}") diff --git a/workflow/scripts/cv_plot_pure_eb.py b/workflow/scripts/cv_plot_pure_eb.py index 496996990..fec9a0e55 100644 --- a/workflow/scripts/cv_plot_pure_eb.py +++ b/workflow/scripts/cv_plot_pure_eb.py @@ -21,6 +21,8 @@ plot_pure_eb_correlations(results, snakemake.output.figure_xis, p.version, **kwargs) plot_pte_2d_heatmaps(results, p.version, snakemake.output.figure_ptes, **kwargs) plot_eb_covariance_matrix( - results["cov"], covariance_label(results["npatch"]), - snakemake.output.figure_covariance, p.version, + results["cov"], + covariance_label(results["npatch"]), + snakemake.output.figure_covariance, + p.version, ) diff --git a/workflow/scripts/cv_plot_rho_stats.py b/workflow/scripts/cv_plot_rho_stats.py deleted file mode 100644 index a55a13777..000000000 --- a/workflow/scripts/cv_plot_rho_stats.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Rule cv_plot_rho_stats: rho-statistics overlay across versions. - -Reads rho_stats_{base}.fits for every version (declared inputs); writes -rho_stats.png into the leakage output dir. Sentinel-tracked because the figure -lands beside the first version's leakage products, not at a fixed path. -""" - -from cv_runner import _unbuffer_streams, make_cv, touch_sentinels - -_unbuffer_streams() -cv = make_cv(snakemake) -cv.plot_rho_stats(savefig="rho_stats.png", show=False) -touch_sentinels(snakemake) diff --git a/workflow/scripts/cv_plot_tau_stats.py b/workflow/scripts/cv_plot_tau_stats.py deleted file mode 100644 index a2c6f365c..000000000 --- a/workflow/scripts/cv_plot_tau_stats.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Rule cv_plot_tau_stats: tau-statistics overlay across versions. - -Reads tau_stats_{base}.fits for every version (declared inputs), with error -bars from the version's tau covariance of the configured type; writes -tau_stats.png into the leakage output dir. Sentinel-tracked (see rho_stats). -""" - -from cv_runner import _unbuffer_streams, make_cv, touch_sentinels - -_unbuffer_streams() -cv = make_cv(snakemake) -cv.plot_tau_stats( - tomography=False, - cov_type=cv.cov_estimate_method, - savefig="tau_stats.png", - show=False, -) -touch_sentinels(snakemake) diff --git a/workflow/scripts/cv_ratio_xi_sys_xi.py b/workflow/scripts/cv_ratio_xi_sys_xi.py deleted file mode 100644 index 9e1e4396c..000000000 --- a/workflow/scripts/cv_ratio_xi_sys_xi.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Rule cv_ratio_xi_sys_xi: ratio of PSF systematics to cosmic-shear signal. - -Joins two upstream chains for the non-tomographic ("all", "all") pair: each -version's reporting ξ± part and xi_psf_sys (recomputed in memory from the -rho/tau FITS via the lazy cv.xi_psf_sys property — the PSF-error fit is not -persisted). Both are declared as inputs so the DAG shows the join. Writes -ratio_xi_sys_xi.png at a fixed path (declared output). -""" - -from cv_runner import _unbuffer_streams, make_cv, take_reporting_parts, verify_outputs - -_unbuffer_streams() -cv = make_cv(snakemake) -take_reporting_parts(cv, snakemake.input["xi"]) -cv.plot_ratio_xi_sys_xi( - tomography=False, offset=snakemake.params.get("offset", 0.1), show=False -) -verify_outputs(snakemake) diff --git a/workflow/scripts/cv_rho_tau_fits.py b/workflow/scripts/cv_rho_tau_fits.py index 6e02ba10c..d2e493787 100644 --- a/workflow/scripts/cv_rho_tau_fits.py +++ b/workflow/scripts/cv_rho_tau_fits.py @@ -1,22 +1,24 @@ -"""Rule cv_rho_tau_fits: PSF-error model fit and xi_psf_sys contours. +"""Fit PSF parameters and persist all non-cosmological plot arrays.""" -Conditional in the original driver on rho_tau_method != "none". Reads -rho_stats/tau_stats FITS (declared inputs), runs the MCMC fit -(calculate_rho_tau_fits via plot_rho_tau_fits), and writes contour / -xi_psf_sys figures into the leakage output dir. Sentinel-tracked: figure paths -derive from internal handler state. The fitted xi_psf_sys it produces is -recomputed (not persisted) by the cv_ratio_xi_sys_xi rule. -""" +import numpy as np +from cv_runner import make_cv, prepare_cached_psf, verify_outputs -from cv_runner import _unbuffer_streams, make_cv, touch_sentinels - -_unbuffer_streams() cv = make_cv(snakemake) -if cv.rho_tau_method != "none": - cv.plot_rho_tau_fits( - tomography=False, - savefig_contours="contours_tau_stat.png", - savefig_xi_psf_sys="xi_psf_sys", - show=False, +prepare_cached_psf(cv) +for ver, path in zip(cv.versions, snakemake.output.products, strict=True): + params = cv.set_params_rho_tau(ver, cv.results[ver]._params, cv.cc[ver]["psf"]) + flat_samples = cv.get_samples(ver, params, "all") + plus, minus = cv.get_xi_psf_sys_samples(ver, params, "all", "all") + data = dict( + theta=cv.psf_fitter.rho_stat_handler.rho_stats["theta"], + flat_samples=flat_samples, ) -touch_sentinels(snakemake) + for component, samples in (("plus", plus), ("minus", minus)): + data[f"samples_{component}"] = samples + data[f"mean_{component}"] = samples.mean(axis=0) + data[f"var_{component}"] = samples.var(axis=0) + data[f"quantiles_{component}"] = np.quantile( + samples, [1 - cv.quantile, cv.quantile], axis=0 + ) + np.savez_compressed(path, **data) +verify_outputs(snakemake) diff --git a/workflow/scripts/cv_runner.py b/workflow/scripts/cv_runner.py index b4ffcea26..144301661 100644 --- a/workflow/scripts/cv_runner.py +++ b/workflow/scripts/cv_runner.py @@ -6,12 +6,10 @@ owns, and (3) verifies the declared outputs landed. This module carries the common machinery so each rule script stays a few readable lines. -The in-memory ``cv`` object is re-instantiated per rule. The DAG links rules -through the *real* data products each method writes (rho/tau FITS, ξ± parts, -pseudo-Cl FITS, E/B npz, leakage pkl); the lazy ``cv`` properties that aren't -persisted (``c1``/``c2``, ``xi_psf_sys``, ``rho_tau_fits``) are recomputed in -whichever rule needs them, which is cheap next to the science compute they -depend on. See common.cv_init_params for the constructor contract. +Compute rules instantiate ``cv`` independently and link through explicit +cached data products. Plot rules read those products directly; they don't +instantiate ``cv`` or enter its lazy catalogue/measurement paths. +See common.cv_init_params for the compute constructor contract. """ import os @@ -36,6 +34,22 @@ def make_cv(snakemake): return CosmologyValidation(**dict(snakemake.params["cv_init"])) +def prepare_cached_psf(cv): + """Attach FITS-only handlers; never enter the catalogue/pair-count path.""" + from shear_psf_leakage.rho_tau_stat import RhoStat, TauStat + + output = str(Path(cv.cc["paths"]["output"]) / "rho_tau_stats") + cv._rho_stat_handler = RhoStat( + output=output, treecorr_config=cv.treecorr_config, verbose=True + ) + cv._tau_stat_handler = TauStat( + catalogs=cv._rho_stat_handler.catalogs, + output=output, + treecorr_config=cv.treecorr_config, + verbose=True, + ) + + def take_reporting_parts(cv, paths): """Hand ``cv`` the reporting ξ± parts at ``paths``, one per version, in order.""" from sp_validation import sacc_io @@ -53,12 +67,3 @@ def verify_outputs(snakemake): "cosmo_val rule finished but these declared outputs are absent:\n " + "\n ".join(missing) ) - - -def touch_sentinels(snakemake): - """Create any sentinel outputs (pure-plot rules with no data product).""" - for out in snakemake.output: - path = Path(out) - if path.suffix == ".done": - path.parent.mkdir(parents=True, exist_ok=True) - path.touch() diff --git a/workflow/scripts/cv_weights.py b/workflow/scripts/cv_weights.py index 0316fe1e9..293e91d66 100644 --- a/workflow/scripts/cv_weights.py +++ b/workflow/scripts/cv_weights.py @@ -1,12 +1,12 @@ -"""Rule cv_weights: weighted shear-weight histograms across versions. +"""Cache weight-weighted bin sums and bin edges; no figures.""" -Leaf rule. Reads each version's shear weights; writes weight_hist.png at a -fixed path under the output dir (declared output). -""" +import numpy as np +from cv_runner import make_cv, verify_outputs -from cv_runner import _unbuffer_streams, make_cv, verify_outputs - -_unbuffer_streams() cv = make_cv(snakemake) -cv.plot_weights() +data = {"versions": np.asarray(cv.versions)} +for i, (counts, edges) in enumerate(cv.weight_histograms().values()): + data[f"counts_{i}"] = counts + data[f"edges_{i}"] = edges +np.savez(snakemake.output.histograms, **data) verify_outputs(snakemake) diff --git a/workflow/scripts/run_rho_tau.py b/workflow/scripts/run_rho_tau.py index d04da3f02..21712470d 100644 --- a/workflow/scripts/run_rho_tau.py +++ b/workflow/scripts/run_rho_tau.py @@ -18,6 +18,7 @@ npatch=int(params["npatch"]), catalog_config=params["cat_config"], output_dir=params["output_dir"], + cov_estimate_method=params["cov_estimate_method"], ) cv.calculate_rho_tau_stats(tomography=False) verify_outputs(snakemake) From f9acedb4740717ed7c4ff56cdc919f5ba7173771 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 12:50:06 -0400 Subject: [PATCH 03/53] Test cached renderers and fail-closed plot-only workflow; document restyling --- src/sp_validation/b_modes.py | 33 +-- src/sp_validation/tests/test_plot_products.py | 278 ++++++++++++++++++ src/sp_validation/tests/test_plot_workflow.py | 145 +++++++++ workflow/README.md | 33 +++ workflow/scripts/cv_rho_tau_fits.py | 6 + 5 files changed, 472 insertions(+), 23 deletions(-) create mode 100644 src/sp_validation/tests/test_plot_products.py create mode 100644 src/sp_validation/tests/test_plot_workflow.py diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index ea9a80ca0..d4cc548f0 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -921,28 +921,14 @@ def plot_cosebis_scale_cut_heatmap( left_edges, right_edges = edges nbins = len(left_edges) - # Initialize matrices - snrs = [np.sqrt(result["chi2_E"]) for result in cosebis_results.values()] - ptes = [result["pte_B"] for result in cosebis_results.values()] - - rows = [start for start in range(nbins) for stop in range(start, nbins)] - columns = [stop for start in range(nbins) for stop in range(start, nbins)] - + # Place only the cuts actually scanned, using their binning metadata. + # A sparse scan must not be assigned to the first N triangular cells. snr_matrix = np.full((nbins, nbins), np.nan) pte_matrix = np.full((nbins, nbins), np.nan) - for row, column, snr, pte in zip(rows, columns, snrs, ptes): - snr_matrix[row, column] = snr - pte_matrix[row, column] = pte - - # Fill matrices from COSEBIs results - for i in range(len(left_edges)): - for j in range(i, len(right_edges)): - scale_cut = (left_edges[i], right_edges[j]) - result = cosebis_results.get(scale_cut) - - if result is not None: - snr_matrix[i, j] = np.sqrt(result["chi2_E"]) - pte_matrix[i, j] = result["pte_B"] + for scale_cut, result in cosebis_results.items(): + start, stop = bins_from_scale_cut(left_edges, right_edges, scale_cut) + snr_matrix[start, stop - 1] = np.sqrt(result["chi2_E"]) + pte_matrix[start, stop - 1] = result["pte_B"] # Create 1x2 subplot layout fig, axs = plt.subplots(1, 2, figsize=(15, 7)) @@ -1348,7 +1334,6 @@ def load_cosebis_results(path): with np.load(path, allow_pickle=False) as data: results = {} for lo, hi in data["scale_cuts"]: - suffix = f"_{lo}_{hi}" # Match the writer's scalar formatting (including integer cuts). suffix = next( k.removeprefix("En") @@ -1388,8 +1373,10 @@ def save_cosebis_results(results, output_path, fiducial_scale_cut=None, edges=No output_path : str Output .npz file path fiducial_scale_cut : tuple, optional - If results has multiple scale cuts, save only this one. - If None and results has multiple scale cuts, saves all. + If given, also save untagged entries for this fiducial cut (used by + summary consumers). All scanned cuts are always retained. + edges : tuple of arrays, optional + Integration-bin left/right edges used by the scale-cut heatmap. """ is_multi = all(isinstance(k, tuple) for k in results) diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py new file mode 100644 index 000000000..6fc050391 --- /dev/null +++ b/src/sp_validation/tests/test_plot_products.py @@ -0,0 +1,278 @@ +"""Small, catalogue-free products exercise the compute/render boundary.""" + +from pathlib import Path +from types import SimpleNamespace + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from sp_validation import b_modes +from sp_validation.cosmo_val import diagnostic_plots as plots +from sp_validation.cosmo_val.catalog_characterization import ( + CatalogCharacterizationMixin, +) + +STYLES = {"synthetic": {"colour": "C0", "marker": "o"}} + + +@pytest.fixture(autouse=True) +def no_catalogues(monkeypatch): + from sp_validation import io + + def forbidden(*args, **kwargs): + raise AssertionError("A renderer attempted a catalogue read") + + monkeypatch.setattr(io, "open_entry", forbidden) + yield + plt.close("all") + + +def assert_figures(*paths): + for path in paths: + assert Path(path).stat().st_size > 100 + + +def psf_product(): + rng = np.random.default_rng(42) + data = { + "theta": np.geomspace(1, 100, 4), + "flat_samples": rng.normal(size=(1000, 3)), + } + for component in ("plus", "minus"): + samples = np.exp(rng.normal(-10, 0.1, (100, 4))) + data.update( + { + f"samples_{component}": samples, + f"mean_{component}": samples.mean(axis=0), + f"var_{component}": samples.var(axis=0), + f"quantiles_{component}": np.quantile(samples, [0.16, 0.84], axis=0), + } + ) + return data + + +def test_cached_psf_handlers_do_not_initialize_catalogues(tmp_path): + import importlib.util + + root = Path(__file__).resolve().parents[3] + spec = importlib.util.spec_from_file_location( + "plot_test_runner", root / "workflow/scripts/cv_runner.py" + ) + runner = importlib.util.module_from_spec(spec) + spec.loader.exec_module(runner) + cv = SimpleNamespace( + cc={"paths": {"output": str(tmp_path)}}, + treecorr_config={"min_sep": 1.0, "max_sep": 100.0, "nbins": 4}, + ) + runner.prepare_cached_psf(cv) + assert cv._rho_stat_handler.catalogs._output == str(tmp_path / "rho_tau_stats") + assert cv._tau_stat_handler.catalogs is cv._rho_stat_handler.catalogs + + +def test_objectwise_compute_uses_regression_not_plot_driver(tmp_path, monkeypatch): + from contextlib import nullcontext + + from sp_validation.cosmo_val.psf_systematics import PSFSystematicsMixin, leakage + + params = {k: SimpleNamespace(value=0.01, stderr=0.001) for k in ("a11", "a22")} + calls = [] + + def regress(*args, **kwargs): + calls.append(kwargs) + return params + + monkeypatch.setattr(leakage, "corr_2d", regress) + monkeypatch.setattr(leakage, "save_to_file", lambda *args: None) + obj = SimpleNamespace( + get_out_base=lambda *args, **kwargs: str(tmp_path / "fit"), + temporarily_read_data=lambda **kwargs: nullcontext(), + get_ellipticity_weights=lambda: ([np.zeros(4), np.zeros(4)], np.ones(4)), + _dat={"p1": np.zeros(4), "p2": np.zeros(4)}, + _params={"e1_PSF_col": "p1", "e2_PSF_col": "p2", "verbose": False}, + _stats_file=None, + ) + cv = PSFSystematicsMixin() + cv.print_cyan = lambda *args: None + result = cv._objectwise_leakage_bin(obj, "synthetic", "all", True, "lin") + assert len(calls) == 1 + assert result["aii_mean"].nominal_value == pytest.approx(0.01) + + +def test_weights_roundtrip_and_overwrite(tmp_path): + path = tmp_path / "weights.npz" + w = np.array([0.1, 0.2, 0.2, 0.5, 0.9]) + + # This is exactly the compute seam; no matplotlib histogram in the renderer. + class CV(CatalogCharacterizationMixin): + versions = ["synthetic"] + results = { + "synthetic": SimpleNamespace( + temporarily_read_data=lambda: __import__("contextlib").nullcontext() + ) + } + + def _read_shear_cols(self, *args): + return w + + counts, edges = CV().weight_histograms(nbins=3)["synthetic"] + assert counts.sum() == pytest.approx(w.sum()) + np.savez(path, counts=counts, edges=edges) + with np.load(path, allow_pickle=False) as cache: + hist = {"synthetic": (cache["counts"], cache["edges"])} + output = tmp_path / "weights.png" + output.write_text("stale") + plots.plot_weights(hist, STYLES, output) + assert_figures(output) + + +def test_footprint_every_region_from_one_map(tmp_path, monkeypatch): + from sp_validation.cosmo_val.diagnostic_products import ( + footprint_counts, + load_footprint, + save_footprint, + ) + + count_map = footprint_counts([150.0, 150.0, 20.0], [40.0, 40.0, 30.0], 4, 16) + assert count_map[count_map.valid_pixels].sum() == 3 + cache = tmp_path / "map.npz" + save_footprint(count_map, cache) + loaded = load_footprint(cache) + calls = [] + + def draw(self, data, region, outpath): + np.testing.assert_array_equal(data.valid_pixels, count_map.valid_pixels) + calls.append(region) + Path(outpath).write_bytes(b"rendered" * 100) + + monkeypatch.setattr(plots.FootprintPlotter, "plot_region", draw) + outputs = {r: tmp_path / f"{r}.png" for r in plots.FootprintPlotter._regions} + plots.plot_footprint(loaded, outputs) + assert calls == list(plots.FootprintPlotter._regions.values()) + assert_figures(*outputs.values()) + + +def test_scalar_diagnostics_and_covariance(tmp_path): + coeff = { + "synthetic": { + "tomo_bin_all": { + key: [0.001, 0.0001] + for key in ("aii_mean", "alpha_mean", "alpha_1", "alpha_0") + } + } + } + leakage, covariance = tmp_path / "leakage.png", tmp_path / "covariance.pdf" + plots.plot_objectwise_leakage(coeff, STYLES, leakage) + plots.plot_covariance(np.eye(4), covariance) + assert_figures(leakage, covariance) + + +def test_psf_fit_and_sealed_ratio_rendering(tmp_path): + data = psf_product() + path = tmp_path / "psf.npz" + np.savez(path, **data) + with np.load(path, allow_pickle=False) as cache: + products = {"synthetic": dict(cache)} + contours, samples, summary, ratio = [ + tmp_path / f"{x}.png" for x in ("contours", "samples", "summary", "ratio") + ] + plots.plot_rho_tau_fits(products, STYLES, contours, samples, summary) + gg = SimpleNamespace( + meanr=data["theta"], + xip=np.ones(4) * 1e-4, + xim=np.ones(4) * 1e-4, + varxip=np.ones(4) * 1e-12, + varxim=np.ones(4) * 1e-12, + ) + plots.plot_ratio_xi_sys_xi(products, {"synthetic": gg}, STYLES, ratio) + xi_paths = [tmp_path / f"xi{i}.png" for i in range(2)] + plots.plot_2pcf({"synthetic": gg}, STYLES, xi_paths) + assert_figures(contours, samples, summary, ratio, *xi_paths) + + +@pytest.mark.parametrize("kind,indices", [("rho", range(6)), ("tau", (0, 2, 5))]) +def test_psf_overlay_arrays(tmp_path, kind, indices): + table = {"theta": np.geomspace(1, 100, 4)} + for i in indices: + table[f"{kind}_{i}_p"] = np.ones(4) * 1e-6 + table[f"var{kind}_{i}_p"] = np.ones(4) * 1e-14 + output = tmp_path / f"{kind}.png" + plots.plot_psf_stats( + {"synthetic": table}, + STYLES, + output, + kind, + [np.eye(12) * 1e-14] if kind == "tau" else [], + ) + assert_figures(output) + + +def test_pure_eb_plot_cache_is_complete(tmp_path): + n = 3 + edges = np.geomspace(1, 100, n + 1) + theta = np.sqrt(edges[:-1] * edges[1:]) + result = dict( + theta=theta, + left_edges=edges[:-1], + right_edges=edges[1:], + theta_int=theta, + xip_int=np.ones(n) * 1e-5, + xim_int=np.ones(n) * 1e-5, + xip=np.ones(n) * 1e-5, + xim=np.ones(n) * 1e-5, + var_xip=np.ones(n) * 1e-12, + var_xim=np.ones(n) * 1e-12, + cov=np.eye(6 * n) * 1e-12, + npatch=None, + ) + result.update({k: np.ones(n) * 1e-6 for k in b_modes._EB_KEYS}) + result = b_modes.calculate_eb_statistics(result) + path = tmp_path / "sealed_eb.npz" + b_modes.save_pure_eb_results(result, path) + loaded = b_modes.load_pure_eb_results(path) + assert loaded["npatch"] is None + for key in ("theta_int", "xip_int", "xim_int", "xip", "var_xip", "cov"): + np.testing.assert_array_equal(result[key], loaded[key]) + outputs = [tmp_path / f"eb{i}.png" for i in range(4)] + b_modes.plot_integration_vs_reporting(loaded, outputs[0], "synthetic") + b_modes.plot_pure_eb_correlations(loaded, outputs[1], "synthetic") + b_modes.plot_pte_2d_heatmaps(loaded, "synthetic", outputs[2]) + b_modes.plot_eb_covariance_matrix( + loaded["cov"], "analytic", outputs[3], "synthetic" + ) + assert_figures(*outputs) + + +def test_cosebis_preserves_all_cuts_for_plotting(tmp_path): + edges = np.array([1.0, 2.0, 4.0, 8.0]) + results = { + (lo, hi): dict( + En=np.array([1.0, 2.0]), + Bn=np.array([0.1, 0.2]), + cov=np.eye(4), + chi2_E=5.0, + chi2_B=0.05, + pte_B=0.9, + ) + for i, lo in enumerate(edges[:-1]) + for hi in edges[i + 1 :] + } + path = tmp_path / "sealed_cosebis.npz" + b_modes.save_cosebis_results(results, path, (1.0, 8.0), (edges[:-1], edges[1:])) + loaded, loaded_edges = b_modes.load_cosebis_results(path) + assert loaded.keys() == results.keys() + np.testing.assert_array_equal(loaded_edges, (edges[:-1], edges[1:])) + with np.load(path, allow_pickle=False) as cache: + assert float(cache["pte_B"]) == 0.9 # Existing summary consumer. + outputs = [tmp_path / f"cosebis{i}.png" for i in range(3)] + b_modes.plot_cosebis_modes(loaded[1.0, 8.0], "synthetic", outputs[0]) + b_modes.plot_cosebis_covariance_matrix( + loaded[1.0, 8.0], "synthetic", "analytic", outputs[1] + ) + b_modes.plot_cosebis_scale_cut_heatmap( + loaded, loaded_edges, "synthetic", outputs[2] + ) + assert_figures(*outputs) diff --git a/src/sp_validation/tests/test_plot_workflow.py b/src/sp_validation/tests/test_plot_workflow.py new file mode 100644 index 000000000..42c25401c --- /dev/null +++ b/src/sp_validation/tests/test_plot_workflow.py @@ -0,0 +1,145 @@ +"""Dry-run the real plotting DAG with caches present and no catalogues. + +Set SPV_TEST_SNAKEMAKE to a host Snakemake executable if it isn't in the image. +The test never launches a workflow job or opens a real science product. +""" + +import importlib.util +import os +import shutil +import subprocess +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[3] +PLOT_RULES = [ + "cosmo_val_plots", + "cv_plot_rho_stats", + "cv_plot_tau_stats", + "cv_plot_rho_tau_fits", + "cv_plot_footprints", + "cv_plot_objectwise_leakage", + "cv_plot_weights", + "cv_plot_2pcf", + "cv_ratio_xi_sys_xi", + "cv_plot_pure_eb", + "cv_plot_cosebis", + "cv_plot_pseudo_cl", + "covariance_plot", +] + + +@pytest.mark.parametrize("pseudo_cl", [False, True]) +def test_cached_plot_dag_has_no_compute_jobs(tmp_path, monkeypatch, pseudo_cl): + executable = os.environ.get("SPV_TEST_SNAKEMAKE") or shutil.which("snakemake") + if not executable: + pytest.skip("Snakemake not installed; set SPV_TEST_SNAKEMAKE") + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("COSMO_VAL", str(tmp_path / "products")) + monkeypatch.setenv("COSMO_INFERENCE", str(tmp_path / "inference")) + monkeypatch.setenv("PYTHONUNBUFFERED", "1") + # common.configure expects this fixed relative cosmology file at parse time. + cosmology = tmp_path / "results/cosmology/planck18.json" + cosmology.parent.mkdir(parents=True) + shutil.copy(ROOT / "papers/cosmo_val/results/cosmology/planck18.json", cosmology) + config = yaml.safe_load((ROOT / "papers/cosmo_val/config/config.yaml").read_text()) + config["versions"] = [ + "SP_v1.4.6.3" + ] # Public fixture entry; no real input is opened. + config["cosmo_val"]["include_pseudo_cl"] = pseudo_cl + configfile = tmp_path / "config.yaml" + configfile.write_text(yaml.safe_dump(config)) + + spec = importlib.util.spec_from_file_location( + "plot_test_common", ROOT / "workflow/common.py" + ) + common = importlib.util.module_from_spec(spec) + spec.loader.exec_module(common) + # Set the path-helper context without importing Snakemake in the image; + # the subprocess below runs the real configure() in the host environment. + common.CATALOG_CONFIG = yaml.safe_load(Path(common.CAT_CONFIG).read_text()) + common.FIDUCIAL = config["fiducial"] + common.DEFAULT_MASK_SUFFIX = ( + "_masked" if config["covariance"].get("default_masked", False) else "" + ) + namespace = dict(vars(common), config=config) + # Execute only the ordinary Python path helpers, before the first rule. + twopoint = (ROOT / "workflow/rules/twopoint.smk").read_text().split("rule xi:")[0] + exec(twopoint, namespace) + diagnostics = ( + (ROOT / "workflow/rules/cosmo_val.smk") + .read_text() + .split("rule cv_plot_rho_stats:")[0] + ) + exec(diagnostics, namespace) + ns = namespace + version = config["versions"][0] + products = [ + *ns["CV_PSF_PRODUCTS"], + *ns["CV_FOOTPRINT_MAPS"], + str(common.COSMO_VAL / "weight_hist.npz"), + str(common.COSMO_VAL / "leakage_coefficients.json"), + common.cv_rho_stats(version, ns["CV_FIDUCIAL"]), + common.cv_tau_stats(version, ns["CV_FIDUCIAL"]), + *ns["cv_tau_covariances"](), + ns["cv_xi_sacc"](version, "reporting"), + ns["cv_pure_eb_npz"](version), + ns["cv_cosebis_npz"](version), + ns["cv_xi_cov"](version), + ns["cv_xi_cov_integration"](version), + ] + if pseudo_cl: + products += [ + ns["cv_pseudo_cl_analysis_sacc"](version), + ns["cv_pseudo_cl_cov"](version), + ] + for product in products: + path = Path(product) + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + + command = [ + executable, + "-s", + str(ROOT / "workflow/Snakefile"), + "--configfile", + str(configfile), + "--cores", + "1", + "--dry-run", + "--forceall", + "--workflow-profile", + "none", + "--rerun-triggers", + "mtime", + "--allowed-rules", + *PLOT_RULES, + "--", + "cosmo_val_plots", + ] + result = subprocess.run(command, text=True, capture_output=True, timeout=120) + log = result.stdout + result.stderr + assert result.returncode == 0, log + assert "cv_plot_weights" in log + assert "cv_plot_footprints" in log + assert "cv_plot_pure_eb" in log + assert "cv_plot_cosebis" in log + assert ("rule cv_plot_pseudo_cl:" in log) == pseudo_cl + for forbidden in ( + "cv_weights", + "cv_footprints", + "cv_rho_tau_fits", + "cv_cosebis", + "cv_pure_eb", + "xi", + "rho_tau_stats", + "covariance_run", + ): + assert f"rule {forbidden}:" not in log + # A plot-only restyle must fail closed, not silently rebuild from a catalogue. + (common.COSMO_VAL / "weight_hist.npz").unlink() + missing = subprocess.run(command, text=True, capture_output=True, timeout=120) + assert missing.returncode != 0 + assert "MissingInputException" in missing.stdout + missing.stderr diff --git a/workflow/README.md b/workflow/README.md index e3e15c6c6..8336d02c0 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -159,6 +159,39 @@ ln -s /n17data/cdaley/unions/code/sp_validation/cosmo_inference/data/mask **Caveat:** `rerun-triggers: code` watches rule bodies and `script:` files, not `src/`. Editing a module under `src/` does not by itself mark outputs stale — force with `-F` or `--forcerun `. +The `cosmo_val` plot rules are an exception: their plotting modules are explicit inputs, so editing those modules invalidates figures without invalidating computations. + +### Re-render the cosmo_val figures from cache + +`cosmo_val_all` runs the computations and every companion plot. +`cosmo_val_plots` gathers only figures, including the reporting/integration covariance correlation plots. +Plots read the sealed SACC/NPZ products, sparse footprint counts, weight histograms and PSF/leakage summaries; no plot rule reads a catalogue or fits a model. +Every figure is a declared output and is overwritten when its plot rule runs. + +After a full run, use the same config and output roots to render changed plots: + +```bash +snakemake --profile workflow/profiles/default -s workflow/Snakefile \ + --configfile --cores 4 cosmo_val_plots +``` + +To force every figure to be restyled, while **refusing to run any computation even if a cache is missing**, restrict the allowed rules: + +```bash +plot_rules=(cosmo_val_plots cv_plot_rho_stats cv_plot_tau_stats \ + cv_plot_rho_tau_fits cv_plot_footprints cv_plot_objectwise_leakage \ + cv_plot_weights cv_plot_2pcf cv_ratio_xi_sys_xi cv_plot_pure_eb \ + cv_plot_cosebis cv_plot_pseudo_cl covariance_plot) +snakemake --profile workflow/profiles/default -s workflow/Snakefile \ + --configfile --cores 4 --forceall \ + --allowed-rules "${plot_rules[@]}" -- cosmo_val_plots +``` + +Run this on an allocation, using the host's container/bind settings as usual. +Without the rule restriction, requesting a missing data product can schedule its producer. +A pre-split run needs one compute pass to create the new caches: old pure-E/B NPZs lack the comparison vectors and old COSEBIs NPZs contain only the fiducial cut. +That pass derives cosmological products only from the already-sealed integration SACC; no unsealed vector is added for plotting. +The objectwise rule now calls the spin-2 regression directly rather than the external all-in-one exploratory driver, so it produces the suite's leakage-coefficient comparison rather than unrelated 1D-fit/scatter side effects. To reproduce a run from the image alone, opt out: diff --git a/workflow/scripts/cv_rho_tau_fits.py b/workflow/scripts/cv_rho_tau_fits.py index d2e493787..bcdbff82a 100644 --- a/workflow/scripts/cv_rho_tau_fits.py +++ b/workflow/scripts/cv_rho_tau_fits.py @@ -12,6 +12,12 @@ data = dict( theta=cv.psf_fitter.rho_stat_handler.rho_stats["theta"], flat_samples=flat_samples, + theta_edges=np.geomspace( + cv.treecorr_config["min_sep"], + cv.treecorr_config["max_sep"], + cv.treecorr_config["nbins"] + 1, + ), + theta_unit="arcmin", ) for component, samples in (("plus", plus), ("minus", minus)): data[f"samples_{component}"] = samples From e9f254355887cb8b861fd5ebb2cc62a2cb360148 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 13:28:55 -0400 Subject: [PATCH 04/53] Require inherited blind provenance in cosmological NPZ consumers --- src/sp_validation/b_modes.py | 69 +++++++++++-------- src/sp_validation/cosmo_val/cosebis.py | 5 +- src/sp_validation/cosmo_val/pure_eb.py | 6 +- src/sp_validation/sacc_io.py | 30 ++++++-- src/sp_validation/tests/test_b_modes.py | 6 +- .../tests/test_blind_consumers.py | 16 +++++ src/sp_validation/tests/test_plot_products.py | 16 +++-- workflow/rules/cosmo_val.smk | 2 + workflow/scripts/cv_cosebis.py | 4 +- workflow/scripts/cv_plot_cosebis.py | 2 +- workflow/scripts/cv_plot_pure_eb.py | 2 +- workflow/scripts/cv_pure_eb.py | 2 +- workflow/scripts/cv_summarize_bmodes.py | 4 +- 13 files changed, 117 insertions(+), 47 deletions(-) diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index d4cc548f0..70e6f2d88 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -1285,7 +1285,7 @@ def plot_cosebis_modes(results, version, output_path, fiducial_scale_cut=None): plt.savefig(output_path, dpi=300, bbox_inches="tight") -def save_pure_eb_results(results, output_path): +def save_pure_eb_results(results, output_path, *, derived_from): """ Save pure E/B mode data vectors and covariance to .npz. @@ -1312,40 +1312,45 @@ def save_pure_eb_results(results, output_path): if results["npatch"] is not None: save_dict["npatch"] = np.array(results["npatch"]) - np.savez(output_path, **save_dict) + from . import sacc_io + + sacc_io.save_npz(save_dict, output_path, derived_from=derived_from) print(f"Saved pure E/B results to {output_path}") -def load_pure_eb_results(path): - """Read a sealed plot cache without transforms or catalogue access.""" - with np.load(path, allow_pickle=False) as data: - results = {k: data[k] for k in data.files if not k.startswith("pte_matrices_")} - results["pte_matrices"] = { - k.removeprefix("pte_matrices_"): data[k] - for k in data.files - if k.startswith("pte_matrices_") - } +def load_pure_eb_results(path, *, blind): + """Read a sealed plot cache only after checking the expected blind.""" + from . import sacc_io + + data = sacc_io.load_npz(path, blind=blind) + results = {k: v for k, v in data.items() if not k.startswith("pte_matrices_")} + results["pte_matrices"] = { + k.removeprefix("pte_matrices_"): v + for k, v in data.items() + if k.startswith("pte_matrices_") + } results["npatch"] = int(results["npatch"]) if "npatch" in results else None return results -def load_cosebis_results(path): - """Read all cached scale cuts and their integration-bin edges.""" - with np.load(path, allow_pickle=False) as data: - results = {} - for lo, hi in data["scale_cuts"]: - # Match the writer's scalar formatting (including integer cuts). - suffix = next( - k.removeprefix("En") - for k in data.files - if k.startswith("En_") - and tuple(map(float, k[3:].split("_"))) == (lo, hi) - ) - results[lo, hi] = { - k: data[k + suffix] - for k in ("En", "Bn", "cov", "chi2_E", "chi2_B", "pte_B") - } - edges = data["left_edges"], data["right_edges"] +def load_cosebis_results(path, *, blind): + """Read cached scale cuts after checking their inherited blind stamp.""" + from . import sacc_io + + data = sacc_io.load_npz(path, blind=blind) + results = {} + for lo, hi in data["scale_cuts"]: + # Match the writer's scalar formatting (including integer cuts). + suffix = next( + k.removeprefix("En") + for k in data + if k.startswith("En_") and tuple(map(float, k[3:].split("_"))) == (lo, hi) + ) + results[lo, hi] = { + k: data[k + suffix] + for k in ("En", "Bn", "cov", "chi2_E", "chi2_B", "pte_B") + } + edges = data["left_edges"], data["right_edges"] return results, edges @@ -1361,7 +1366,9 @@ def _cosebis_result_to_dict(r, suffix=""): } -def save_cosebis_results(results, output_path, fiducial_scale_cut=None, edges=None): +def save_cosebis_results( + results, output_path, fiducial_scale_cut=None, edges=None, *, derived_from +): """ Save COSEBIs data vectors and covariance to .npz. @@ -1403,7 +1410,9 @@ def save_cosebis_results(results, output_path, fiducial_scale_cut=None, edges=No if edges is not None: save_dict["left_edges"], save_dict["right_edges"] = edges - np.savez(output_path, **save_dict) + from . import sacc_io + + sacc_io.save_npz(save_dict, output_path, derived_from=derived_from) print(f"Saved COSEBIs results to {output_path}") diff --git a/src/sp_validation/cosmo_val/cosebis.py b/src/sp_validation/cosmo_val/cosebis.py index bea1df164..b2714e6e6 100644 --- a/src/sp_validation/cosmo_val/cosebis.py +++ b/src/sp_validation/cosmo_val/cosebis.py @@ -310,6 +310,9 @@ def plot_cosebis( ) save_cosebis_results( - bin_results, bin_stub + "_data.npz", fiducial_scale_cut + bin_results, + bin_stub + "_data.npz", + fiducial_scale_cut, + derived_from=[self.xi_parts[version, "integration"]], ) self._cosebis_results[version] = results_tomo diff --git a/src/sp_validation/cosmo_val/pure_eb.py b/src/sp_validation/cosmo_val/pure_eb.py index 793fa8268..96a80faa9 100644 --- a/src/sp_validation/cosmo_val/pure_eb.py +++ b/src/sp_validation/cosmo_val/pure_eb.py @@ -287,5 +287,9 @@ def plot_pure_eb( ) # Save data products and store on instance - save_pure_eb_results(version_results, out_stub + "_data.npz") + save_pure_eb_results( + version_results, + out_stub + "_data.npz", + derived_from=[self.xi_parts[version, "integration"]], + ) self._pure_eb_results[version][bin_key] = version_results diff --git a/src/sp_validation/sacc_io.py b/src/sp_validation/sacc_io.py index c4bdc5c76..f20f3b3f9 100644 --- a/src/sp_validation/sacc_io.py +++ b/src/sp_validation/sacc_io.py @@ -1010,6 +1010,31 @@ def seal(s, blind, theory=None): return out +def derived_stamp(parts): + """The common stamp inherited by a derivation, for SACC and NPZ alike.""" + stamps = {stamp(p) for p in parts} + if len(stamps) != 1: + raise ValueError(f"input parts carry different blinds: {sorted(stamps)}") + return stamps.pop() + + +def save_npz(arrays, path, *, derived_from): + """Save a cosmological array cache with the same inheritance as SACC.""" + np.savez(path, **{**arrays, STAMP_KEY: derived_stamp(derived_from)}) + + +def load_npz(path, *, blind): + """Check explicit provenance before reading any cached signal arrays.""" + from types import SimpleNamespace + + with np.load(path, allow_pickle=False) as data: + if STAMP_KEY not in data: + raise ValueError("cosmological NPZ is missing its blind stamp") + part = SimpleNamespace(metadata={STAMP_KEY: str(data[STAMP_KEY].item())}) + expect(part, blind) + return {k: data[k] for k in data.files if k != STAMP_KEY} + + def save(s, path, *, blind=None, theory=None, derived_from=None): """Write ``s`` to ``path`` (FITS) and return what was written. @@ -1028,10 +1053,7 @@ def save(s, path, *, blind=None, theory=None, derived_from=None): signal never leaves that function. """ if derived_from is not None: - stamps = {stamp(p) for p in derived_from} - if len(stamps) != 1: - raise ValueError(f"input parts carry different blinds: {sorted(stamps)}") - (name,) = stamps + name = derived_stamp(derived_from) out = s.copy() out.metadata[STAMP_KEY] = name if blind is not None: diff --git a/src/sp_validation/tests/test_b_modes.py b/src/sp_validation/tests/test_b_modes.py index 2ed8a3410..01c30aae8 100644 --- a/src/sp_validation/tests/test_b_modes.py +++ b/src/sp_validation/tests/test_b_modes.py @@ -715,7 +715,11 @@ def test_pure_eb_npz_carries_what_the_summary_reads(tmp_path): results = b_modes.calculate_eb_statistics(results) out = tmp_path / "pure_eb_data.npz" - b_modes.save_pure_eb_results(results, str(out)) + from types import SimpleNamespace + + b_modes.save_pure_eb_results( + results, str(out), derived_from=[SimpleNamespace(metadata={"blind": "none"})] + ) saved = np.load(out) for stat in ("xip_B", "xim_B", "combined"): diff --git a/src/sp_validation/tests/test_blind_consumers.py b/src/sp_validation/tests/test_blind_consumers.py index 3d3b7a8d8..57e73261b 100644 --- a/src/sp_validation/tests/test_blind_consumers.py +++ b/src/sp_validation/tests/test_blind_consumers.py @@ -11,6 +11,22 @@ SCRIPTS = Path(__file__).resolve().parents[3] / "workflow" / "scripts" +@pytest.mark.parametrize("loader", ["load_pure_eb_results", "load_cosebis_results"]) +@pytest.mark.parametrize("stamp", [None, "wrong"]) +def test_npz_consumers_refuse_missing_or_wrong_blind(tmp_path, loader, stamp): + import numpy as np + + from sp_validation import b_modes + + path = tmp_path / "cosmological.npz" + data = dict(scale_cuts=np.empty((0, 2)), left_edges=[], right_edges=[]) + if stamp is not None: + data["blind"] = stamp + np.savez(path, **data) + with pytest.raises(ValueError, match="stamp"): + getattr(b_modes, loader)(path, blind="expected") + + @pytest.fixture def part_path(tmp_path): part = sacc_io.new_sacc({0: ([0.1, 1.0], [1.0, 1.0])}) diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index 6fc050391..c5b7e2dc2 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -231,8 +231,11 @@ def test_pure_eb_plot_cache_is_complete(tmp_path): result.update({k: np.ones(n) * 1e-6 for k in b_modes._EB_KEYS}) result = b_modes.calculate_eb_statistics(result) path = tmp_path / "sealed_eb.npz" - b_modes.save_pure_eb_results(result, path) - loaded = b_modes.load_pure_eb_results(path) + parent = SimpleNamespace(metadata={"blind": "toy"}) + b_modes.save_pure_eb_results(result, path, derived_from=[parent]) + loaded = b_modes.load_pure_eb_results(path, blind="toy") + with np.load(path, allow_pickle=False) as cache: + assert cache["blind"].item() == "toy" assert loaded["npatch"] is None for key in ("theta_int", "xip_int", "xim_int", "xip", "var_xip", "cov"): np.testing.assert_array_equal(result[key], loaded[key]) @@ -261,8 +264,13 @@ def test_cosebis_preserves_all_cuts_for_plotting(tmp_path): for hi in edges[i + 1 :] } path = tmp_path / "sealed_cosebis.npz" - b_modes.save_cosebis_results(results, path, (1.0, 8.0), (edges[:-1], edges[1:])) - loaded, loaded_edges = b_modes.load_cosebis_results(path) + parent = SimpleNamespace(metadata={"blind": "toy"}) + b_modes.save_cosebis_results( + results, path, (1.0, 8.0), (edges[:-1], edges[1:]), derived_from=[parent] + ) + loaded, loaded_edges = b_modes.load_cosebis_results(path, blind="toy") + with np.load(path, allow_pickle=False) as cache: + assert cache["blind"].item() == "toy" assert loaded.keys() == results.keys() np.testing.assert_array_equal(loaded_edges, (edges[:-1], edges[1:])) with np.load(path, allow_pickle=False) as cache: diff --git a/workflow/rules/cosmo_val.smk b/workflow/rules/cosmo_val.smk index eb37511e4..6a9a41587 100644 --- a/workflow/rules/cosmo_val.smk +++ b/workflow/rules/cosmo_val.smk @@ -540,6 +540,7 @@ rule cv_plot_pure_eb: **cv_pure_eb_figures("{version}"), params: version="{version}", + blind=lambda w: blind_of(w.version), fiducial_scale_cut=CV["fiducial_scale_cut"], script: "../scripts/cv_plot_pure_eb.py" @@ -554,6 +555,7 @@ rule cv_plot_cosebis: **cv_cosebis_figures("{version}"), params: version="{version}", + blind=lambda w: blind_of(w.version), fiducial_scale_cut=CV["fiducial_scale_cut"], script: "../scripts/cv_plot_cosebis.py" diff --git a/workflow/scripts/cv_cosebis.py b/workflow/scripts/cv_cosebis.py index 97308d4db..1f58a304a 100644 --- a/workflow/scripts/cv_cosebis.py +++ b/workflow/scripts/cv_cosebis.py @@ -41,7 +41,9 @@ fiducial_key = find_conservative_scale_cut_key(results, fiducial_scale_cut) fiducial = results[fiducial_key] -save_cosebis_results(results, snakemake.output["npz"], fiducial_scale_cut, edges) +save_cosebis_results( + results, snakemake.output["npz"], fiducial_scale_cut, edges, derived_from=[part] +) s = cosebis_to_sacc({0: sacc_io.get_nz(part, 0)}, part.metadata, fiducial, fiducial_key) sacc_io.save(s, snakemake.output["sacc"], derived_from=[part]) diff --git a/workflow/scripts/cv_plot_cosebis.py b/workflow/scripts/cv_plot_cosebis.py index 7e3f10da4..0d2085a50 100644 --- a/workflow/scripts/cv_plot_cosebis.py +++ b/workflow/scripts/cv_plot_cosebis.py @@ -9,7 +9,7 @@ ) p = snakemake.params -results, edges = load_cosebis_results(snakemake.input.npz) +results, edges = load_cosebis_results(snakemake.input.npz, blind=p.blind) cut = tuple(p.fiducial_scale_cut) fiducial = results[find_conservative_scale_cut_key(results, cut)] plot_cosebis_modes(fiducial, p.version, snakemake.output.figure_modes, cut) diff --git a/workflow/scripts/cv_plot_pure_eb.py b/workflow/scripts/cv_plot_pure_eb.py index fec9a0e55..b0c2b9a1d 100644 --- a/workflow/scripts/cv_plot_pure_eb.py +++ b/workflow/scripts/cv_plot_pure_eb.py @@ -10,7 +10,7 @@ ) p = snakemake.params -results = load_pure_eb_results(snakemake.input.npz) +results = load_pure_eb_results(snakemake.input.npz, blind=p.blind) kwargs = dict( fiducial_xip_scale_cut=tuple(p.fiducial_scale_cut), fiducial_xim_scale_cut=tuple(p.fiducial_scale_cut), diff --git a/workflow/scripts/cv_pure_eb.py b/workflow/scripts/cv_pure_eb.py index cf561e08d..50a65fbcf 100644 --- a/workflow/scripts/cv_pure_eb.py +++ b/workflow/scripts/cv_pure_eb.py @@ -41,7 +41,7 @@ ) results = calculate_eb_statistics(results) -save_pure_eb_results(results, snakemake.output["npz"]) +save_pure_eb_results(results, snakemake.output["npz"], derived_from=[part]) sacc_io.save( pure_eb_to_sacc( diff --git a/workflow/scripts/cv_summarize_bmodes.py b/workflow/scripts/cv_summarize_bmodes.py index 8175e0d6b..f91648d28 100644 --- a/workflow/scripts/cv_summarize_bmodes.py +++ b/workflow/scripts/cv_summarize_bmodes.py @@ -26,7 +26,7 @@ for i, version in enumerate(p["versions"]): row = {} - pure_eb = np.load(snakemake.input["pure_eb"][i]) + pure_eb = sacc_io.load_npz(snakemake.input["pure_eb"][i], blind=p["blinds"][i]) # The bins the PTE matrices are indexed on: the snapped reporting edges. edges = (pure_eb["left_edges"], pure_eb["right_edges"]) for stat in ("xip_B", "xim_B", "combined"): @@ -41,7 +41,7 @@ # The COSEBIs .npz is written at the fiducial cut, so its PTE is the one # this table wants. - cosebis = np.load(snakemake.input["cosebis"][i]) + cosebis = sacc_io.load_npz(snakemake.input["cosebis"][i], blind=p["blinds"][i]) row["COSEBIS"] = float(cosebis["pte_B"]) cov_methods.add("COSEBIs: analytic (OneCovariance ξ± through the COSEBIs kernel)") From 04dc4c60faeda05bc987960df9b1982c312d60fb Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 13:40:09 -0400 Subject: [PATCH 05/53] Persist COSEBIs selected bins and place heatmaps from those indices --- src/sp_validation/b_modes.py | 14 +++-- src/sp_validation/tests/test_plot_products.py | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index 70e6f2d88..0e57210cd 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -501,6 +501,7 @@ def cosebis_scan_from_xi( "pte_B": 1 - stats.chi2.cdf(chi2_B, nmodes), "scale_cut": (min_theta, max_theta), "inds": inds, + "bin_indices": np.array([start_bin, stop_bin]), } print(f"COSEBIs results [{min_theta:.1f}-{max_theta:.1f} arcmin]:") @@ -925,8 +926,8 @@ def plot_cosebis_scale_cut_heatmap( # A sparse scan must not be assigned to the first N triangular cells. snr_matrix = np.full((nbins, nbins), np.nan) pte_matrix = np.full((nbins, nbins), np.nan) - for scale_cut, result in cosebis_results.items(): - start, stop = bins_from_scale_cut(left_edges, right_edges, scale_cut) + for result in cosebis_results.values(): + start, stop = result["bin_indices"] snr_matrix[start, stop - 1] = np.sqrt(result["chi2_E"]) pte_matrix[start, stop - 1] = result["pte_B"] @@ -983,10 +984,10 @@ def plot_cosebis_scale_cut_heatmap( # Add fiducial scale cut cross-hatching if provided if fiducial_scale_cut is not None: - min_scale, max_scale = fiducial_scale_cut - start_bin, stop_bin = bins_from_edges( - left_edges, right_edges, min_scale, max_scale + fiducial_key = find_conservative_scale_cut_key( + cosebis_results, fiducial_scale_cut ) + start_bin, stop_bin = cosebis_results[fiducial_key]["bin_indices"] if stop_bin > start_bin and start_bin < nbins and stop_bin > 0: # Add cross-hatching at the fiducial scale cut matrix element @@ -1348,7 +1349,7 @@ def load_cosebis_results(path, *, blind): ) results[lo, hi] = { k: data[k + suffix] - for k in ("En", "Bn", "cov", "chi2_E", "chi2_B", "pte_B") + for k in ("En", "Bn", "cov", "chi2_E", "chi2_B", "pte_B", "bin_indices") } edges = data["left_edges"], data["right_edges"] return results, edges @@ -1363,6 +1364,7 @@ def _cosebis_result_to_dict(r, suffix=""): f"chi2_E{suffix}": np.array(r["chi2_E"]), f"chi2_B{suffix}": np.array(r["chi2_B"]), f"pte_B{suffix}": np.array(r["pte_B"]), + f"bin_indices{suffix}": np.asarray(r["bin_indices"]), } diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index c5b7e2dc2..2b109c49f 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -249,6 +249,58 @@ def test_pure_eb_plot_cache_is_complete(tmp_path): assert_figures(*outputs) +def test_cosebis_transform_caches_selected_bins(tmp_path, monkeypatch): + from cosmo_numba.B_modes import cosebis + + class SyntheticKernel: + def __init__(self, **kwargs): + pass + + def cosebis_from_xipm(self, theta, xip, xim, **kwargs): + return np.ones(2), np.zeros(2) + + def cosebis_covariance_from_xipm_covariance(self, theta, cov): + return np.eye(4) + + monkeypatch.setattr(cosebis, "COSEBIS", SyntheticKernel) + edges = b_modes.log_bin_edges(0.08, 300, 1000) + theta = np.sqrt(edges[0] * edges[1]) + results = b_modes.cosebis_scan_from_xi( + theta, + np.zeros(1000), + np.zeros(1000), + np.eye(2000), + *edges, + nmodes=2, + scale_cuts=[(12, 83)], + ) + np.testing.assert_array_equal(results[12, 83]["bin_indices"], [609, 843]) + path = tmp_path / "cuts.npz" + b_modes.save_cosebis_results( + results, + path, + (12, 83), + edges, + derived_from=[SimpleNamespace(metadata={"blind": "toy"})], + ) + loaded, _ = b_modes.load_cosebis_results(path, blind="toy") + np.testing.assert_array_equal(loaded[12, 83]["bin_indices"], [609, 843]) + + +def test_cosebis_heatmap_uses_stored_inward_bins(tmp_path, monkeypatch): + # Nearest edges would put this at (0, 2); the transform selected (1, 2). + result = {(1.1, 7.9): dict(chi2_E=4.0, pte_B=0.7, bin_indices=[1, 2])} + edges = (np.array([1.0, 2.0, 4.0]), np.array([2.0, 4.0, 8.0])) + b_modes.plot_cosebis_scale_cut_heatmap( + result, edges, "synthetic", tmp_path / "cut.png", (1.1, 7.9) + ) + ax = plt.gcf().axes[1] + matrix = ax.images[0].get_array() + assert matrix[1, 1] == pytest.approx(0.7) + assert np.ma.count(matrix) == 1 + assert ax.patches[0].get_xy() == (1, 1) + + def test_cosebis_preserves_all_cuts_for_plotting(tmp_path): edges = np.array([1.0, 2.0, 4.0, 8.0]) results = { @@ -259,9 +311,10 @@ def test_cosebis_preserves_all_cuts_for_plotting(tmp_path): chi2_E=5.0, chi2_B=0.05, pte_B=0.9, + bin_indices=np.array([i, j]), ) for i, lo in enumerate(edges[:-1]) - for hi in edges[i + 1 :] + for j, hi in enumerate(edges[i + 1 :], start=i + 1) } path = tmp_path / "sealed_cosebis.npz" parent = SimpleNamespace(metadata={"blind": "toy"}) From 83a0aff7ea2acba7413b815e900526d51df45697 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 13:53:05 -0400 Subject: [PATCH 06/53] Require every plot cache directly even when figures already exist --- src/sp_validation/tests/test_plot_workflow.py | 46 +++++++++++++++++-- workflow/rules/cosmo_val.smk | 21 +++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/sp_validation/tests/test_plot_workflow.py b/src/sp_validation/tests/test_plot_workflow.py index 42c25401c..0aee0c3cc 100644 --- a/src/sp_validation/tests/test_plot_workflow.py +++ b/src/sp_validation/tests/test_plot_workflow.py @@ -138,8 +138,44 @@ def test_cached_plot_dag_has_no_compute_jobs(tmp_path, monkeypatch, pseudo_cl): "covariance_run", ): assert f"rule {forbidden}:" not in log - # A plot-only restyle must fail closed, not silently rebuild from a catalogue. - (common.COSMO_VAL / "weight_hist.npz").unlink() - missing = subprocess.run(command, text=True, capture_output=True, timeout=120) - assert missing.returncode != 0 - assert "MissingInputException" in missing.stdout + missing.stderr + # Existing PNGs must never hide a missing cache when allowed-rules prunes + # a plot job. Populate *all* figures, then remove each cache in turn. + figures = [ + *ns["CV_FOOTPRINT_FIGURES"], + *ns["cv_pure_eb_figures"](version).values(), + *ns["cv_cosebis_figures"](version).values(), + *[ + str(common.COSMO_VAL / name) + for name in ( + "weight_hist.png", + "leakage_coefficients.png", + "ratio_xi_sys_xi.png", + "xi_pm_tomography_False.png", + "xi_pm_theta_tomography_False.png", + "rho_tau_stats/rho_stats.png", + "rho_tau_stats/tau_stats.png", + "rho_tau_stats/contours_tau_stat.png", + "rho_tau_stats/xi_psf_sys_tomography_False_sample.png", + "rho_tau_stats/xi_psf_sys_tomography_False_mean_and_std.png", + ) + ], + ns["cv_xi_cov"](version).removesuffix(".dat") + "_corr_plot.pdf", + ns["cv_xi_cov_integration"](version).removesuffix(".dat") + "_corr_plot.pdf", + ] + if pseudo_cl: + figures += [ + str(common.COSMO_VAL / f"cell_{name}.png") for name in ("ee", "eb", "bb") + ] + for figure in figures: + path = Path(figure) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("existing figure") + for product in products: + path = Path(product) + path.unlink() + missing = subprocess.run(command, text=True, capture_output=True, timeout=120) + log = missing.stdout + missing.stderr + assert missing.returncode != 0, f"Missing cache {product} was ignored:\n{log}" + assert "MissingInputException" in log + assert "existing figure" == Path(figures[0]).read_text() + path.touch() diff --git a/workflow/rules/cosmo_val.smk b/workflow/rules/cosmo_val.smk index 6a9a41587..2f0194e36 100644 --- a/workflow/rules/cosmo_val.smk +++ b/workflow/rules/cosmo_val.smk @@ -664,6 +664,27 @@ rule cosmo_val_plots: list(cv_pseudo_cl_figures().values()) if CV.get("include_pseudo_cl", False) else [], [cv_xi_cov(v).removesuffix(".dat") + "_corr_plot.pdf" for v in CV_VERSIONS], [cv_xi_cov_integration(v).removesuffix(".dat") + "_corr_plot.pdf" for v in CV_VERSIONS], + # Direct dependencies are intentional. With --allowed-rules Snakemake + # can prune a plot whose figure exists even when its cache is missing. + # The aggregate must validate caches independently of those plot jobs. + caches=[ + *rules.cv_plot_rho_stats.input.stats, + *rules.cv_plot_tau_stats.input.stats, + *rules.cv_plot_tau_stats.input.covariances, + *rules.cv_plot_rho_tau_fits.input.products, + *rules.cv_plot_footprints.input.maps, + rules.cv_plot_objectwise_leakage.input.coefficients, + rules.cv_plot_weights.input.histograms, + *rules.cv_plot_2pcf.input.xi, + *[cv_pure_eb_npz(v) for v in CV_VERSIONS], + *[cv_cosebis_npz(v) for v in CV_VERSIONS], + *[cv_xi_cov(v) for v in CV_VERSIONS], + *[cv_xi_cov_integration(v) for v in CV_VERSIONS], + *([cv_pseudo_cl_analysis_sacc(v) for v in CV_VERSIONS] + if CV.get("include_pseudo_cl", False) else []), + *([cv_pseudo_cl_cov(v) for v in CV_VERSIONS] + if CV.get("include_pseudo_cl", False) else []), + ], rule cosmo_val_all: From ec68adff644e2d0d00188e205a1e490e352a7900 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 14:04:29 -0400 Subject: [PATCH 07/53] Remove retired reporting-parts seam and test NPZ consumers instead --- .../tests/test_blind_consumers.py | 22 +++---------------- workflow/scripts/cv_runner.py | 9 -------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/sp_validation/tests/test_blind_consumers.py b/src/sp_validation/tests/test_blind_consumers.py index 57e73261b..2cb4fcb7f 100644 --- a/src/sp_validation/tests/test_blind_consumers.py +++ b/src/sp_validation/tests/test_blind_consumers.py @@ -13,7 +13,8 @@ @pytest.mark.parametrize("loader", ["load_pure_eb_results", "load_cosebis_results"]) @pytest.mark.parametrize("stamp", [None, "wrong"]) -def test_npz_consumers_refuse_missing_or_wrong_blind(tmp_path, loader, stamp): +@pytest.mark.parametrize("expected", ["none", "expected"]) +def test_npz_consumers_refuse_missing_or_wrong_blind(tmp_path, loader, stamp, expected): import numpy as np from sp_validation import b_modes @@ -24,7 +25,7 @@ def test_npz_consumers_refuse_missing_or_wrong_blind(tmp_path, loader, stamp): data["blind"] = stamp np.savez(path, **data) with pytest.raises(ValueError, match="stamp"): - getattr(b_modes, loader)(path, blind="expected") + getattr(b_modes, loader)(path, blind=expected) @pytest.fixture @@ -36,23 +37,6 @@ def part_path(tmp_path): return path -@pytest.mark.parametrize("expected", ["none", "toy"]) -def test_reporting_parts(part_path, monkeypatch, expected): - monkeypatch.syspath_prepend(str(SCRIPTS)) - from cv_runner import take_reporting_parts - - cv = SimpleNamespace( - versions=["v"], xi_parts={}, blind=lambda v: blinding.Blind(expected) - ) - if expected == "toy": - with pytest.raises(ValueError, match="stamped none.*toy"): - take_reporting_parts(cv, [part_path]) - assert not cv.xi_parts - else: - take_reporting_parts(cv, [part_path]) - assert sacc_io.stamp(cv.xi_parts["v", "reporting"]) == "none" - - @pytest.mark.parametrize( "script, input_key, reader", [ diff --git a/workflow/scripts/cv_runner.py b/workflow/scripts/cv_runner.py index 144301661..dda72778f 100644 --- a/workflow/scripts/cv_runner.py +++ b/workflow/scripts/cv_runner.py @@ -50,15 +50,6 @@ def prepare_cached_psf(cv): ) -def take_reporting_parts(cv, paths): - """Hand ``cv`` the reporting ξ± parts at ``paths``, one per version, in order.""" - from sp_validation import sacc_io - - for version, path in zip(cv.versions, paths, strict=True): - part = sacc_io.load(path, blind=cv.blind(version)) - cv.xi_parts[version, "reporting"] = part - - def verify_outputs(snakemake): """Fail loudly if any declared output is missing after the method ran.""" missing = [str(o) for o in snakemake.output if not Path(o).exists()] From 36f345bfb1365a985511b5610ce72440ca055805 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 24 Oct 2026 14:19:03 -0400 Subject: [PATCH 08/53] Keep notebook COSEBIs caches on their selected integration grid --- src/sp_validation/cosmo_val/cosebis.py | 9 ++-- src/sp_validation/tests/test_plot_products.py | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/sp_validation/cosmo_val/cosebis.py b/src/sp_validation/cosmo_val/cosebis.py index b2714e6e6..efce2cfc0 100644 --- a/src/sp_validation/cosmo_val/cosebis.py +++ b/src/sp_validation/cosmo_val/cosebis.py @@ -297,13 +297,13 @@ def plot_cosebis( plot_results, version, var_method, bin_stub + "_covariance.png" ) + # Stored bin indices refer to the integration grid, including when + # the requested cuts were generated from a coarser reporting grid. + edges = log_bin_edges(min_sep_int, max_sep_int, nbins_int) if multiple_scale_cuts and len(bin_results) > 1: - binning = self._binning(min_sep, max_sep, nbins) plot_cosebis_scale_cut_heatmap( bin_results, - log_bin_edges( - binning["min_sep"], binning["max_sep"], binning["nbins"] - ), + edges, version, bin_stub + "_scalecut_ptes.png", fiducial_scale_cut=fiducial_scale_cut, @@ -313,6 +313,7 @@ def plot_cosebis( bin_results, bin_stub + "_data.npz", fiducial_scale_cut, + edges, derived_from=[self.xi_parts[version, "integration"]], ) self._cosebis_results[version] = results_tomo diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index 2b109c49f..9c547c6ed 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -301,6 +301,50 @@ def test_cosebis_heatmap_uses_stored_inward_bins(tmp_path, monkeypatch): assert ax.patches[0].get_xy() == (1, 1) +def test_notebook_cosebis_cache_and_heatmap_use_integration_edges( + tmp_path, monkeypatch +): + from sp_validation.cosmo_val import cosebis as module + + class CV(module.CosebisMixin): + versions = ["synthetic"] + cc = {"paths": {"output": str(tmp_path)}} + treecorr_config = {"npatch": 1} + xi_parts = { + ("synthetic", "integration"): SimpleNamespace(metadata={"blind": "toy"}) + } + _cosebis_results = {} + + def _binning(self, *args): + return dict(min_sep=1.0, max_sep=8.0, nbins=3) + + edges = b_modes.log_bin_edges(1.0, 8.0, 10) + results = { + cut: dict( + En=np.ones(2), + Bn=np.zeros(2), + cov=np.eye(4), + chi2_E=2.0, + chi2_B=0.0, + pte_B=1.0, + bin_indices=indices, + ) + for cut, indices in [((1.0, 8.0), [0, 10]), ((1.0, 4.0), [0, 6])] + } + monkeypatch.setattr(module, "plot_cosebis_modes", lambda *args, **kwargs: None) + monkeypatch.setattr(module, "plot_cosebis_covariance_matrix", lambda *args: None) + + def heatmap(data, used_edges, *args, **kwargs): + np.testing.assert_array_equal(used_edges, edges) + + monkeypatch.setattr(module, "plot_cosebis_scale_cut_heatmap", heatmap) + CV().plot_cosebis(results=results, min_sep_int=1.0, max_sep_int=8.0, nbins_int=10) + _, cached_edges = b_modes.load_cosebis_results( + next(tmp_path.glob("*.npz")), blind="toy" + ) + np.testing.assert_array_equal(cached_edges, edges) + + def test_cosebis_preserves_all_cuts_for_plotting(tmp_path): edges = np.array([1.0, 2.0, 4.0, 8.0]) results = { From 62bf4261ec4546b5a329d049407071081481128e Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:16:54 -0400 Subject: [PATCH 09/53] fix(plots): compact cached COSEBIs cuts and unclip mode amplitudes --- src/sp_validation/b_modes.py | 57 ++++++++----------- src/sp_validation/tests/test_plot_products.py | 7 ++- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index 0e57210cd..f9c3b0f80 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -920,16 +920,19 @@ def plot_cosebis_scale_cut_heatmap( (min_scale, max_scale) for cross-hatching """ left_edges, right_edges = edges - nbins = len(left_edges) + starts = sorted({int(r["bin_indices"][0]) for r in cosebis_results.values()}) + stops = sorted({int(r["bin_indices"][1]) for r in cosebis_results.values()}) + nx, ny = len(starts), len(stops) # Place only the cuts actually scanned, using their binning metadata. # A sparse scan must not be assigned to the first N triangular cells. - snr_matrix = np.full((nbins, nbins), np.nan) - pte_matrix = np.full((nbins, nbins), np.nan) + snr_matrix = np.full((nx, ny), np.nan) + pte_matrix = np.full((nx, ny), np.nan) for result in cosebis_results.values(): start, stop = result["bin_indices"] - snr_matrix[start, stop - 1] = np.sqrt(result["chi2_E"]) - pte_matrix[start, stop - 1] = result["pte_B"] + i, j = starts.index(start), stops.index(stop) + snr_matrix[i, j] = np.sqrt(result["chi2_E"]) + pte_matrix[i, j] = result["pte_B"] # Create 1x2 subplot layout fig, axs = plt.subplots(1, 2, figsize=(15, 7)) @@ -951,11 +954,11 @@ def plot_cosebis_scale_cut_heatmap( cmap=mako_cmap, vmin=0, vmax=snr_max, - extent=[0, nbins, 0, nbins], + extent=[0, nx, 0, ny], ) axs[0].set_title(r"E-mode SNR: $\sqrt{\chi^2_E}$") - axs[0].set_xlabel("Lower scale cut") - axs[0].set_ylabel("Upper scale cut") + axs[0].set_xlabel("Lower scale cut [arcmin]") + axs[0].set_ylabel("Upper scale cut [arcmin]") # Right plot: B-mode PTE with contours pte_plot_data = pte_matrix.T @@ -966,18 +969,10 @@ def plot_cosebis_scale_cut_heatmap( cmap=vlag_cmap, vmin=0, vmax=1, - extent=[0, nbins, 0, nbins], + extent=[0, nx, 0, ny], ) - # Add contours to PTE plot - cs = axs[1].contour( - pte_plot_data, - levels=[0.05, 0.95], - colors="black", - linewidths=1, - extent=[0, nbins, 0, nbins], - ) - axs[1].clabel(cs, inline=True, fontsize=8) + # Sparse discrete cuts do not support interpolated significance contours. axs[1].set_title("B-mode PTE") axs[1].set_xlabel("Lower scale cut") @@ -989,10 +984,10 @@ def plot_cosebis_scale_cut_heatmap( ) start_bin, stop_bin = cosebis_results[fiducial_key]["bin_indices"] - if stop_bin > start_bin and start_bin < nbins and stop_bin > 0: - # Add cross-hatching at the fiducial scale cut matrix element - rect_x = start_bin - rect_y = stop_bin - 1 + if start_bin in starts and stop_bin in stops: + # Mark the cached selected cut. + rect_x = starts.index(start_bin) + rect_y = stops.index(stop_bin) rect_width = 1 rect_height = 1 @@ -1012,15 +1007,10 @@ def plot_cosebis_scale_cut_heatmap( ) # Set angular scale ticks - tick_indices = np.arange(0, nbins) - x_tick_labels = [ - f"{left_edges[i]:.1f}" for i in tick_indices if i < len(left_edges) - ] - y_tick_labels = [ - f"{right_edges[i]:.1f}" for i in tick_indices if i < len(right_edges) - ] - x_tick_positions = tick_indices + 0.5 - y_tick_positions = tick_indices + 0.5 + x_tick_labels = [f"{left_edges[i]:.2f}" for i in starts] + y_tick_labels = [f"{right_edges[i - 1]:.2f}" for i in stops] + x_tick_positions = np.arange(nx) + 0.5 + y_tick_positions = np.arange(ny) + 0.5 for ax in axs: ax.set_xticks(x_tick_positions[: len(x_tick_labels)]) @@ -1270,7 +1260,10 @@ def plot_cosebis_modes(results, version, output_path, fiducial_scale_cut=None): plt.legend() plt.xlabel("n (mode)") plt.ylabel("E_n, B_n") - plt.ylim(-0.5e-10, 3e-10) + errors = np.sqrt(np.diag(results["cov"])) + values = np.concatenate([results["En"], results["Bn"]]) + limit = 1.1 * np.nanmax(np.abs(values) + errors) + plt.ylim(-limit, limit) # Add scale cut information to title - use actual scale cut from results scale_info = "" diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index 9c547c6ed..f510b9331 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -296,9 +296,12 @@ def test_cosebis_heatmap_uses_stored_inward_bins(tmp_path, monkeypatch): ) ax = plt.gcf().axes[1] matrix = ax.images[0].get_array() - assert matrix[1, 1] == pytest.approx(0.7) + assert matrix.shape == (1, 1) + assert matrix[0, 0] == pytest.approx(0.7) assert np.ma.count(matrix) == 1 - assert ax.patches[0].get_xy() == (1, 1) + assert ax.patches[0].get_xy() == (0, 0) + assert ax.get_xticklabels()[0].get_text() == "2.00" + assert ax.get_yticklabels()[0].get_text() == "4.00" def test_notebook_cosebis_cache_and_heatmap_use_integration_edges( From 0cdc6bfd6e5bb0f67f10151559b41301c202f60c Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:18:30 -0400 Subject: [PATCH 10/53] fix(plots): derive symmetric pure E/B bounds including errors --- src/sp_validation/b_modes.py | 12 +++++++++++- src/sp_validation/tests/test_plot_products.py | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index f9c3b0f80..5853b0c2f 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -851,7 +851,17 @@ def plot_pure_eb_correlations( ax.set(xscale="log", xlabel=r"$\theta$ [arcmin]") ax.axhline(0, alpha=0.3, color="k", linestyle="--", linewidth=0.5) ax.legend(loc="upper left") - ax.set_ylim(-0.5, 2) + # Include every displayed mode and its error bars, symmetric about zero. + bounds = [] + for xi_type, var_attr in (("xip", "var_xip"), ("xim", "var_xim")): + bounds.extend(theta * (np.abs(results[xi_type]) + + np.sqrt(results[var_attr])) / scale_factor) + for mode in ("E", "B", "amb"): + key = f"{xi_type}_{mode}" + bounds.extend(theta * (np.abs(results[key]) + + results[f"std_{key}"]) / scale_factor) + limit = 1.1 * np.nanmax(bounds) + axs[0].set_ylim(-limit, limit) # Save the axis limits after data plotting but before adding gray regions original_xlims = [ax.get_xlim() for ax in axs] diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index f510b9331..774e9e163 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -242,6 +242,10 @@ def test_pure_eb_plot_cache_is_complete(tmp_path): outputs = [tmp_path / f"eb{i}.png" for i in range(4)] b_modes.plot_integration_vs_reporting(loaded, outputs[0], "synthetic") b_modes.plot_pure_eb_correlations(loaded, outputs[1], "synthetic") + lo, hi = plt.gcf().axes[0].get_ylim() + assert lo == -hi + assert hi > np.max(theta * (loaded["xip"] + + np.sqrt(loaded["var_xip"])) / 1e-4) b_modes.plot_pte_2d_heatmaps(loaded, "synthetic", outputs[2]) b_modes.plot_eb_covariance_matrix( loaded["cov"], "analytic", outputs[3], "synthetic" From 8f6419d2bbf701a2e5a05ec056a48a9f21a17691 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:19:07 -0400 Subject: [PATCH 11/53] fix(plots): label footprint galaxy counts and pixel NSIDE --- src/sp_validation/cosmo_val/diagnostic_plots.py | 5 ++++- src/sp_validation/tests/test_plot_products.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py index 59ba45ef0..fc16feb41 100644 --- a/src/sp_validation/cosmo_val/diagnostic_plots.py +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -13,7 +13,10 @@ def plot_footprint(count_map, outputs): """Render every requested region from one cached HealSparse count map.""" plotter = FootprintPlotter() for region, path in outputs.items(): - plotter.plot_region(count_map, plotter._regions[region], outpath=str(path)) + plotter.plot_region( + count_map, plotter._regions[region], outpath=str(path), + colorbar_label=f"Galaxies per pixel (NSIDE={count_map.nside_sparse})", + ) plt.close() diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index 774e9e163..a406fa3ad 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -143,7 +143,8 @@ def test_footprint_every_region_from_one_map(tmp_path, monkeypatch): loaded = load_footprint(cache) calls = [] - def draw(self, data, region, outpath): + def draw(self, data, region, outpath, colorbar_label): + assert colorbar_label == "Galaxies per pixel (NSIDE=16)" np.testing.assert_array_equal(data.valid_pixels, count_map.valid_pixels) calls.append(region) Path(outpath).write_bytes(b"rendered" * 100) From dfcdeda39bea0ae7c9d787ce0274e07360e64627 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:24:58 -0400 Subject: [PATCH 12/53] fix(plots): constrain xi panel layout to separate axis labels --- .../cosmo_val/diagnostic_plots.py | 2 +- .../tests/test_plotfix_layout.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/sp_validation/tests/test_plotfix_layout.py diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py index fc16feb41..c00f140b7 100644 --- a/src/sp_validation/cosmo_val/diagnostic_plots.py +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -183,7 +183,7 @@ def plot_ratio_xi_sys_xi( def plot_2pcf(correlations, styles, outputs): """Draw sealed reporting xi±, both plain and theta-weighted.""" for times_theta, output in enumerate(outputs): - fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + fig, axes = plt.subplots(1, 2, figsize=(14, 6), layout="constrained") for ver, gg in correlations.items(): factor = gg.meanr if times_theta else 1 for ax, xi, var, sign in zip( diff --git a/src/sp_validation/tests/test_plotfix_layout.py b/src/sp_validation/tests/test_plotfix_layout.py new file mode 100644 index 000000000..39da5ec80 --- /dev/null +++ b/src/sp_validation/tests/test_plotfix_layout.py @@ -0,0 +1,20 @@ +"""Presentation regressions with synthetic arrays only.""" +from types import SimpleNamespace + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +from sp_validation.cosmo_val import diagnostic_plots as plots + + +def test_xi_panels_use_constrained_layout(tmp_path, monkeypatch): + figures = [] + monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) + gg = SimpleNamespace(meanr=np.array([1., 10.]), xip=np.array([1., -1.]), + xim=np.array([-1., 1.]), varxip=np.ones(2), + varxim=np.ones(2)) + plots.plot_2pcf({"toy": gg}, {"toy": {"marker": "o", "colour": "C0"}}, + [tmp_path / "xi.png", tmp_path / "theta_xi.png"]) + assert all(fig.get_constrained_layout() for fig in figures if hasattr(fig, "axes")) From 6c1410eb25530ca2db46266012864bfafac30852 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:31:25 -0400 Subject: [PATCH 13/53] fix(plots): retain signed PSF contamination and uncertainty bands --- .../cosmo_val/diagnostic_plots.py | 4 +-- src/sp_validation/tests/test_plotfix_psf.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/sp_validation/tests/test_plotfix_psf.py diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py index c00f140b7..e9a3185e2 100644 --- a/src/sp_validation/cosmo_val/diagnostic_plots.py +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -109,7 +109,7 @@ def plot_rho_tau_fits(products, styles, contours, samples, summary): close=True, ) for path, draw_samples in ((samples, True), (summary, False)): - fig, axes = plt.subplots(1, 2, figsize=(14, 6)) + fig, axes = plt.subplots(1, 2, figsize=(14, 6), layout="constrained") for ver, data in products.items(): theta, colour = data["theta"], styles[ver]["colour"] for ax, component, sign in zip(axes, ("plus", "minus"), ("+", "-")): @@ -128,7 +128,7 @@ def plot_rho_tau_fits(products, styles, contours, samples, summary): ax.fill_between(theta, lo, hi, color=colour, alpha=0.3) ax.set( xscale="log", - yscale="log", + yscale="linear", xlabel=r"$\theta$ [arcmin]", ylabel=rf"$\xi^{{\rm PSF,sys}}_{sign}(\theta)$", ) diff --git a/src/sp_validation/tests/test_plotfix_psf.py b/src/sp_validation/tests/test_plotfix_psf.py new file mode 100644 index 000000000..172c39807 --- /dev/null +++ b/src/sp_validation/tests/test_plotfix_psf.py @@ -0,0 +1,26 @@ +"""Signed PSF contamination must remain visible.""" +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +from sp_validation.cosmo_val import diagnostic_plots as plots + + +def test_signed_psf_summary(tmp_path, monkeypatch): + from shear_psf_leakage import plots as external + monkeypatch.setattr(external, "plot_contours", lambda *a, **kw: None) + figures = [] + monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) + data = {"theta": np.array([1., 10.]), "flat_samples": np.ones((10, 3))} + for component in ("plus", "minus"): + data[f"samples_{component}"] = np.array([[1., -1.]]) + data[f"mean_{component}"] = np.array([1., -1.]) + data[f"quantiles_{component}"] = np.array([[0., -2.], [2., 0.]]) + plots.plot_rho_tau_fits({"toy": data}, {"toy": {"colour": "C0"}}, + tmp_path / "contours.png", tmp_path / "samples.png", + tmp_path / "summary.png") + for fig in figures: + if hasattr(fig, "axes"): + assert all(ax.get_yscale() == "linear" for ax in fig.axes) + assert all(ax.get_ylim()[0] < 0 for ax in fig.axes) From ce93dc2863190e69b36dedd02a4d11c7921f7e34 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 07:33:53 -0400 Subject: [PATCH 14/53] fix(plots): use signed symlog pseudo-Cl panel --- src/sp_validation/cosmo_val/pseudo_cl.py | 10 ++++++++-- src/sp_validation/tests/test_plotfix_cell.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/sp_validation/tests/test_plotfix_cell.py diff --git a/src/sp_validation/cosmo_val/pseudo_cl.py b/src/sp_validation/cosmo_val/pseudo_cl.py index 361369ac4..bfd3b7202 100644 --- a/src/sp_validation/cosmo_val/pseudo_cl.py +++ b/src/sp_validation/cosmo_val/pseudo_cl.py @@ -51,7 +51,7 @@ def plot_pseudo_cl_spectrum(datasets, spectrum, output_path): ``datasets`` maps a version to ``{"ell", "cl", "cov", "style"}``, where ``style`` carries the ``marker`` and ``colour`` the version is drawn with. """ - fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(8, 8)) + fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(8, 8), layout="constrained") minor_ticks = [i * 10 for i in range(1, 10)] + [i * 100 for i in range(1, 21)] for panel, scaled in ((ax[0], True), (ax[1], False)): @@ -77,7 +77,13 @@ def plot_pseudo_cl_spectrum(datasets, spectrum, output_path): panel.xaxis.set_ticks(minor_ticks, minor=True) ax[1].set_xlabel(r"$\ell$") - ax[1].set_yscale("log") + # A signed scale preserves negative spectra and both ends of error bars. + amplitudes = np.concatenate([np.abs(d["cl"]) for d in datasets.values()]) + positive = amplitudes[np.isfinite(amplitudes) & (amplitudes > 0)] + threshold = np.median(positive) if positive.size else 1.0 + ax[1].set_yscale("symlog", linthresh=threshold) + ax[1].yaxis.get_major_locator().set_params(numticks=7) + ax[1].axhline(0, color="grey", linewidth=0.5) plt.suptitle(f"Pseudo-Cl {spectrum} (Gaussian covariance)") plt.legend() plt.savefig(output_path) diff --git a/src/sp_validation/tests/test_plotfix_cell.py b/src/sp_validation/tests/test_plotfix_cell.py new file mode 100644 index 000000000..abba51b6d --- /dev/null +++ b/src/sp_validation/tests/test_plotfix_cell.py @@ -0,0 +1,19 @@ +"""Pseudo-Cl plots must preserve signs, including error bars.""" +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +from sp_validation.cosmo_val.pseudo_cl import plot_pseudo_cl_spectrum + + +def test_signed_cell(tmp_path, monkeypatch): + figures = [] + monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) + plot_pseudo_cl_spectrum({"toy": {"ell": np.array([100., 400.]), + "cl": np.array([-1., 1.]), "cov": np.eye(2) * 4}}, + "EB", tmp_path / "eb.png") + fig = next(fig for fig in reversed(figures) if hasattr(fig, "axes")) + assert fig.axes[1].get_yscale() == "symlog" + lo, hi = fig.axes[1].get_ylim() + assert lo < -3 and hi > 3 From c8bf2880fd569fd045a5572ee3b7bfb7fe02e8e9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 08:24:57 -0400 Subject: [PATCH 15/53] fix(plots): omit symlog labels crowded against zero --- src/sp_validation/cosmo_val/pseudo_cl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sp_validation/cosmo_val/pseudo_cl.py b/src/sp_validation/cosmo_val/pseudo_cl.py index bfd3b7202..6ddc3507d 100644 --- a/src/sp_validation/cosmo_val/pseudo_cl.py +++ b/src/sp_validation/cosmo_val/pseudo_cl.py @@ -83,6 +83,8 @@ def plot_pseudo_cl_spectrum(datasets, spectrum, output_path): threshold = np.median(positive) if positive.size else 1.0 ax[1].set_yscale("symlog", linthresh=threshold) ax[1].yaxis.get_major_locator().set_params(numticks=7) + ticks = ax[1].get_yticks() + ax[1].set_yticks([t for t in ticks if t == 0 or abs(t) >= 100 * threshold]) ax[1].axhline(0, color="grey", linewidth=0.5) plt.suptitle(f"Pseudo-Cl {spectrum} (Gaussian covariance)") plt.legend() From af0fc164e384a9d68b8cfcbd19b6eb4de2efd729 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 25 Oct 2026 08:35:44 -0400 Subject: [PATCH 16/53] fix(plots): separate rho and tau panel labels --- src/sp_validation/cosmo_val/diagnostic_plots.py | 1 + src/sp_validation/tests/test_plotfix_layout.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py index e9a3185e2..30d19bd97 100644 --- a/src/sp_validation/cosmo_val/diagnostic_plots.py +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -216,6 +216,7 @@ def plot_psf_stats(tables, styles, output, kind, covariances=()): 3, figsize=(15, 10 if kind == "rho" else 6), squeeze=False, + layout="constrained", ) for i, stat in enumerate(indices): ax = axes.flat[i] diff --git a/src/sp_validation/tests/test_plotfix_layout.py b/src/sp_validation/tests/test_plotfix_layout.py index 39da5ec80..e2c875dcc 100644 --- a/src/sp_validation/tests/test_plotfix_layout.py +++ b/src/sp_validation/tests/test_plotfix_layout.py @@ -18,3 +18,15 @@ def test_xi_panels_use_constrained_layout(tmp_path, monkeypatch): plots.plot_2pcf({"toy": gg}, {"toy": {"marker": "o", "colour": "C0"}}, [tmp_path / "xi.png", tmp_path / "theta_xi.png"]) assert all(fig.get_constrained_layout() for fig in figures if hasattr(fig, "axes")) + + +def test_tau_panels_use_constrained_layout(tmp_path, monkeypatch): + figures = [] + monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) + table = {"theta": np.array([1., 10.])} + for i in (0, 2, 5): + table[f"tau_{i}_p"] = np.zeros(2) + table[f"vartau_{i}_p"] = np.ones(2) + plots.plot_psf_stats({"toy": table}, {"toy": {"colour": "C0"}}, + tmp_path / "tau.png", "tau") + assert all(fig.get_constrained_layout() for fig in figures if hasattr(fig, "axes")) From b7d540586eb57cc195db8700a389ee4a3b765a9f Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Mon, 26 Oct 2026 02:37:40 -0400 Subject: [PATCH 17/53] style: format plot renderers and presentation regression tests --- src/sp_validation/b_modes.py | 12 ++++++--- .../cosmo_val/diagnostic_plots.py | 4 ++- src/sp_validation/tests/test_plot_products.py | 3 +-- src/sp_validation/tests/test_plotfix_cell.py | 16 +++++++++--- .../tests/test_plotfix_layout.py | 26 +++++++++++++------ src/sp_validation/tests/test_plotfix_psf.py | 21 ++++++++++----- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/sp_validation/b_modes.py b/src/sp_validation/b_modes.py index 5853b0c2f..48349043d 100644 --- a/src/sp_validation/b_modes.py +++ b/src/sp_validation/b_modes.py @@ -854,12 +854,16 @@ def plot_pure_eb_correlations( # Include every displayed mode and its error bars, symmetric about zero. bounds = [] for xi_type, var_attr in (("xip", "var_xip"), ("xim", "var_xim")): - bounds.extend(theta * (np.abs(results[xi_type]) - + np.sqrt(results[var_attr])) / scale_factor) + bounds.extend( + theta + * (np.abs(results[xi_type]) + np.sqrt(results[var_attr])) + / scale_factor + ) for mode in ("E", "B", "amb"): key = f"{xi_type}_{mode}" - bounds.extend(theta * (np.abs(results[key]) - + results[f"std_{key}"]) / scale_factor) + bounds.extend( + theta * (np.abs(results[key]) + results[f"std_{key}"]) / scale_factor + ) limit = 1.1 * np.nanmax(bounds) axs[0].set_ylim(-limit, limit) diff --git a/src/sp_validation/cosmo_val/diagnostic_plots.py b/src/sp_validation/cosmo_val/diagnostic_plots.py index 30d19bd97..6634c03a8 100644 --- a/src/sp_validation/cosmo_val/diagnostic_plots.py +++ b/src/sp_validation/cosmo_val/diagnostic_plots.py @@ -14,7 +14,9 @@ def plot_footprint(count_map, outputs): plotter = FootprintPlotter() for region, path in outputs.items(): plotter.plot_region( - count_map, plotter._regions[region], outpath=str(path), + count_map, + plotter._regions[region], + outpath=str(path), colorbar_label=f"Galaxies per pixel (NSIDE={count_map.nside_sparse})", ) plt.close() diff --git a/src/sp_validation/tests/test_plot_products.py b/src/sp_validation/tests/test_plot_products.py index a406fa3ad..2b72f2e03 100644 --- a/src/sp_validation/tests/test_plot_products.py +++ b/src/sp_validation/tests/test_plot_products.py @@ -245,8 +245,7 @@ def test_pure_eb_plot_cache_is_complete(tmp_path): b_modes.plot_pure_eb_correlations(loaded, outputs[1], "synthetic") lo, hi = plt.gcf().axes[0].get_ylim() assert lo == -hi - assert hi > np.max(theta * (loaded["xip"] - + np.sqrt(loaded["var_xip"])) / 1e-4) + assert hi > np.max(theta * (loaded["xip"] + np.sqrt(loaded["var_xip"])) / 1e-4) b_modes.plot_pte_2d_heatmaps(loaded, "synthetic", outputs[2]) b_modes.plot_eb_covariance_matrix( loaded["cov"], "analytic", outputs[3], "synthetic" diff --git a/src/sp_validation/tests/test_plotfix_cell.py b/src/sp_validation/tests/test_plotfix_cell.py index abba51b6d..e652d1597 100644 --- a/src/sp_validation/tests/test_plotfix_cell.py +++ b/src/sp_validation/tests/test_plotfix_cell.py @@ -1,5 +1,7 @@ """Pseudo-Cl plots must preserve signs, including error bars.""" + import matplotlib + matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np @@ -10,9 +12,17 @@ def test_signed_cell(tmp_path, monkeypatch): figures = [] monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) - plot_pseudo_cl_spectrum({"toy": {"ell": np.array([100., 400.]), - "cl": np.array([-1., 1.]), "cov": np.eye(2) * 4}}, - "EB", tmp_path / "eb.png") + plot_pseudo_cl_spectrum( + { + "toy": { + "ell": np.array([100.0, 400.0]), + "cl": np.array([-1.0, 1.0]), + "cov": np.eye(2) * 4, + } + }, + "EB", + tmp_path / "eb.png", + ) fig = next(fig for fig in reversed(figures) if hasattr(fig, "axes")) assert fig.axes[1].get_yscale() == "symlog" lo, hi = fig.axes[1].get_ylim() diff --git a/src/sp_validation/tests/test_plotfix_layout.py b/src/sp_validation/tests/test_plotfix_layout.py index e2c875dcc..2610b5e35 100644 --- a/src/sp_validation/tests/test_plotfix_layout.py +++ b/src/sp_validation/tests/test_plotfix_layout.py @@ -1,7 +1,9 @@ """Presentation regressions with synthetic arrays only.""" + from types import SimpleNamespace import matplotlib + matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np @@ -12,21 +14,29 @@ def test_xi_panels_use_constrained_layout(tmp_path, monkeypatch): figures = [] monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) - gg = SimpleNamespace(meanr=np.array([1., 10.]), xip=np.array([1., -1.]), - xim=np.array([-1., 1.]), varxip=np.ones(2), - varxim=np.ones(2)) - plots.plot_2pcf({"toy": gg}, {"toy": {"marker": "o", "colour": "C0"}}, - [tmp_path / "xi.png", tmp_path / "theta_xi.png"]) + gg = SimpleNamespace( + meanr=np.array([1.0, 10.0]), + xip=np.array([1.0, -1.0]), + xim=np.array([-1.0, 1.0]), + varxip=np.ones(2), + varxim=np.ones(2), + ) + plots.plot_2pcf( + {"toy": gg}, + {"toy": {"marker": "o", "colour": "C0"}}, + [tmp_path / "xi.png", tmp_path / "theta_xi.png"], + ) assert all(fig.get_constrained_layout() for fig in figures if hasattr(fig, "axes")) def test_tau_panels_use_constrained_layout(tmp_path, monkeypatch): figures = [] monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) - table = {"theta": np.array([1., 10.])} + table = {"theta": np.array([1.0, 10.0])} for i in (0, 2, 5): table[f"tau_{i}_p"] = np.zeros(2) table[f"vartau_{i}_p"] = np.ones(2) - plots.plot_psf_stats({"toy": table}, {"toy": {"colour": "C0"}}, - tmp_path / "tau.png", "tau") + plots.plot_psf_stats( + {"toy": table}, {"toy": {"colour": "C0"}}, tmp_path / "tau.png", "tau" + ) assert all(fig.get_constrained_layout() for fig in figures if hasattr(fig, "axes")) diff --git a/src/sp_validation/tests/test_plotfix_psf.py b/src/sp_validation/tests/test_plotfix_psf.py index 172c39807..5b6c33594 100644 --- a/src/sp_validation/tests/test_plotfix_psf.py +++ b/src/sp_validation/tests/test_plotfix_psf.py @@ -1,5 +1,7 @@ """Signed PSF contamination must remain visible.""" + import matplotlib + matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np @@ -9,17 +11,22 @@ def test_signed_psf_summary(tmp_path, monkeypatch): from shear_psf_leakage import plots as external + monkeypatch.setattr(external, "plot_contours", lambda *a, **kw: None) figures = [] monkeypatch.setattr(plt, "close", lambda fig: figures.append(fig)) - data = {"theta": np.array([1., 10.]), "flat_samples": np.ones((10, 3))} + data = {"theta": np.array([1.0, 10.0]), "flat_samples": np.ones((10, 3))} for component in ("plus", "minus"): - data[f"samples_{component}"] = np.array([[1., -1.]]) - data[f"mean_{component}"] = np.array([1., -1.]) - data[f"quantiles_{component}"] = np.array([[0., -2.], [2., 0.]]) - plots.plot_rho_tau_fits({"toy": data}, {"toy": {"colour": "C0"}}, - tmp_path / "contours.png", tmp_path / "samples.png", - tmp_path / "summary.png") + data[f"samples_{component}"] = np.array([[1.0, -1.0]]) + data[f"mean_{component}"] = np.array([1.0, -1.0]) + data[f"quantiles_{component}"] = np.array([[0.0, -2.0], [2.0, 0.0]]) + plots.plot_rho_tau_fits( + {"toy": data}, + {"toy": {"colour": "C0"}}, + tmp_path / "contours.png", + tmp_path / "samples.png", + tmp_path / "summary.png", + ) for fig in figures: if hasattr(fig, "axes"): assert all(ax.get_yscale() == "linear" for ax in fig.axes) From a98533e472badbed8380d3116d38ea1a4eb760ec Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Mon, 26 Oct 2026 17:49:09 -0400 Subject: [PATCH 18/53] feat(dashboard): web figure set for the cosmo_val dashboard section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plot-only renderers over cached cosmo_val products, in the Paper II figure language on the production page's parchment palette: the xi± hero, the pure E/B, COSEBIs and pseudo-C_ell B-mode data vectors, scale-cut PTE maps, covariance correlation matrices, the PSF leakage set (rho, tau fit, posterior, xi_sys ratio, alpha estimates), zenithal equal-area sky maps and version comparisons. field_maps.py is the one catalogue-level producer: per-pixel map sums and the weight-cell grid, cached once. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011rD7z3XSA9PmjQyPpNcG49 --- src/sp_validation/dashboard/__init__.py | 1 + src/sp_validation/dashboard/covariance.py | 68 +++ src/sp_validation/dashboard/data_vectors.py | 246 +++++++++++ src/sp_validation/dashboard/field_maps.py | 229 ++++++++++ src/sp_validation/dashboard/products.py | 248 +++++++++++ src/sp_validation/dashboard/psf.py | 250 +++++++++++ src/sp_validation/dashboard/scale_cuts.py | 147 +++++++ src/sp_validation/dashboard/sky.py | 436 ++++++++++++++++++++ src/sp_validation/dashboard/style.py | 244 +++++++++++ src/sp_validation/dashboard/versions.py | 146 +++++++ 10 files changed, 2015 insertions(+) create mode 100644 src/sp_validation/dashboard/__init__.py create mode 100644 src/sp_validation/dashboard/covariance.py create mode 100644 src/sp_validation/dashboard/data_vectors.py create mode 100644 src/sp_validation/dashboard/field_maps.py create mode 100644 src/sp_validation/dashboard/products.py create mode 100644 src/sp_validation/dashboard/psf.py create mode 100644 src/sp_validation/dashboard/scale_cuts.py create mode 100644 src/sp_validation/dashboard/sky.py create mode 100644 src/sp_validation/dashboard/style.py create mode 100644 src/sp_validation/dashboard/versions.py diff --git a/src/sp_validation/dashboard/__init__.py b/src/sp_validation/dashboard/__init__.py new file mode 100644 index 000000000..424c36e3e --- /dev/null +++ b/src/sp_validation/dashboard/__init__.py @@ -0,0 +1 @@ +"""Web figure set for the cosmo_val section of the production dashboard.""" diff --git a/src/sp_validation/dashboard/covariance.py b/src/sp_validation/dashboard/covariance.py new file mode 100644 index 000000000..6b8586366 --- /dev/null +++ b/src/sp_validation/dashboard/covariance.py @@ -0,0 +1,68 @@ +"""Correlation matrices of the covariances behind each statistic. + +One square figure per statistic, drawn as the correlation coefficient with +Paper II's diverging ``icefire`` map, block labels at the block centres and +hairline block separators. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from sp_validation.b_modes import correlation_from_covariance + +from . import style as S + + +def correlation(cov, blocks, out, label): + """Draw ``cov`` as a correlation matrix; ``blocks`` is ``[(name, size)]``.""" + corr = correlation_from_covariance(np.asarray(cov)) + n = corr.shape[0] + sizes = [size for _, size in blocks] + if sum(sizes) != n: + raise ValueError(f"blocks cover {sum(sizes)} rows of a {n}-row matrix") + starts = np.concatenate(([0], np.cumsum(sizes))) + with S.figure_style(): + fig, ax = plt.subplots(figsize=(3.6, 3.25)) + image = ax.imshow(corr, cmap=S.correlation_cmap(), vmin=-1, vmax=1, + interpolation="nearest", origin="upper") + for edge in starts[1:-1]: + ax.axhline(edge - 0.5, color=S.RAG, lw=0.5, alpha=0.8) + ax.axvline(edge - 0.5, color=S.RAG, lw=0.5, alpha=0.8) + centres = (starts[:-1] + starts[1:]) / 2 - 0.5 + names = [name for name, _ in blocks] + ax.set_xticks(centres) + ax.set_xticklabels(names, fontsize=8.5) + ax.set_yticks(centres) + ax.set_yticklabels(names, fontsize=8.5) + ax.tick_params(which="both", length=0, top=False, right=False) + ax.set_title(label, loc="left", fontsize=9.5) + cbar = fig.colorbar(image, ax=ax, shrink=0.92, aspect=22, pad=0.03) + cbar.set_ticks([-1, -0.5, 0, 0.5, 1]) + cbar.outline.set_linewidth(0.6) + cbar.set_label("correlation coefficient") + return S.save(fig, out, "half") + + +def xi_blocks(n): + return [(r"$\xi_+$", n), (r"$\xi_-$", n)] + + +def pure_eb_blocks(n): + return [ + (rf"$\xi_{s}^{{{m}}}$", n) + for m in ("E", "B", r"\rm amb") + for s in "+-" + ] + + +def cosebis_blocks(n): + return [(r"$E_n$", n), (r"$B_n$", n)] + + +def cl_matrix(cov, keys=("EE", "EB", "BB")): + """Assemble the NaMaster blocks for ``keys`` into one matrix.""" + return np.block([[cov[f"{a}_{b}"] for b in keys] for a in keys]) + + +def cl_blocks(n, keys=("EE", "EB", "BB")): + return [(rf"$C_\ell^{{{k}}}$", n) for k in keys] diff --git a/src/sp_validation/dashboard/data_vectors.py b/src/sp_validation/dashboard/data_vectors.py new file mode 100644 index 000000000..6b964a76a --- /dev/null +++ b/src/sp_validation/dashboard/data_vectors.py @@ -0,0 +1,246 @@ +"""The correlation-function hero figure and the three B-mode data vectors. + +Each function draws one figure from loaded products (see +:mod:`sp_validation.dashboard.products`) and writes it to ``out``. The B-mode +figures follow Paper II's single-data-vector figures (pure E/B Fig. 2, +COSEBIs Fig. 3, pseudo-C_ell Fig. 4): same estimator-family hues, marker +fills, normalisation and grey shading of excluded scales. For the dashboard's +row of three they share one layout -- two stacked panels on a common x axis +-- and carry their null-test PTEs in the panel headers. +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.lines import Line2D +from scipy import stats + +from sp_validation.b_modes import bins_from_scale_cut + +from . import style as S + +XI_SCALE = 1e-4 +THIRD_HEIGHT = 3.55 + + +def _theta_ticks(ax): + ax.set_xticks([1, 10, 100]) + ax.set_xticklabels(["$1$", "$10$", "$100$"]) + + +def _pte_header(ax, text): + ax.set_title(text, loc="right", fontsize=8.5, color=S.INK_MUTED, pad=3) + + +def xi_pm(xi, out, layout="wide", blind=None): + """Blinded reporting-grid ξ± (θξ × 10⁴) with analytic error bars. + + ``layout="wide"`` puts ξ+ and ξ− side by side for the page column; + ``"narrow"`` stacks them for phones. + """ + with S.figure_style(): + if layout == "wide": + fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), sharey=True) + size = "full" + else: + fig, axes = plt.subplots(2, 1, figsize=(3.6, 4.6), sharex=True) + size = "half" + theta = xi["theta"] + for ax, key, label in zip(axes, ("xip", "xim"), (r"$\xi_+$", r"$\xi_-$")): + S.zero_line(ax) + ax.errorbar( + theta, + theta * xi[key] / XI_SCALE, + yerr=theta * xi[f"sigma_{key}"] / XI_SCALE, + marker="o", + color=S.COLOR_TOTAL, + markerfacecolor=S.COLOR_TOTAL, + **{**S.DATA_VECTOR_MARK, "markersize": 3.8}, + ) + S.theta_axis(ax) + _theta_ticks(ax) + S.label_box(ax, label, loc="upper left", fontsize=11) + for ax in axes if layout == "wide" else axes[-1:]: + ax.set_xlabel(r"$\theta$ [arcmin]") + for ax in axes[:1] if layout == "wide" else axes: + ax.set_ylabel(r"$\theta\,\xi \times 10^{4}$") + lo = min(np.min(theta * (xi[k] - xi[f"sigma_{k}"])) for k in ("xip", "xim")) + hi = max(np.max(theta * (xi[k] + xi[f"sigma_{k}"])) for k in ("xip", "xim")) + pad = 0.08 * (hi - lo) + axes[0].set_ylim((lo - pad) / XI_SCALE, (hi + 2.2 * pad) / XI_SCALE) + if blind: + fig.suptitle(" ", fontsize=7) + S.blind_stamp(fig, blind) + return S.save(fig, out, size) + + +def _jitter(theta, i, n, spread=0.07): + return theta * np.exp(spread * (i - (n - 1) / 2)) + + +def pure_eb(pe, out, scale_cut, ptes, blind=None): + """Pure E / B / ambiguous decomposition of ξ± (Paper II Fig. 2), stacked.""" + series = [ + ("", "o", S.COLOR_TOTAL, 1.0, True, "total"), + ("_E", "s", S.COLOR_E, 0.5, True, "$E$"), + ("_B", "X", S.COLOR_B, 1.0, True, "$B$"), + ("_amb", "v", S.COLOR_AMB, 0.55, True, "amb."), + ] + with S.figure_style(): + fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True) + theta = pe["theta"] + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], + scale_cut) + shade = (pe["left_edges"][start], pe["right_edges"][stop - 1]) + values = [] + for ax, xi, sign in zip(axes, ("xip", "xim"), "+-"): + S.zero_line(ax) + for i, (suffix, marker, color, alpha, filled, _) in enumerate(series): + y = theta * pe[f"{xi}{suffix}"] / XI_SCALE + dy = theta * pe[f"sigma_{xi}{suffix}"] / XI_SCALE + values.append((y - dy, y + dy)) + ax.errorbar( + _jitter(theta, i, len(series)), + y, + yerr=dy, + marker=marker, + color=color, + alpha=alpha, + markerfacecolor=color if filled else "none", + **S.DATA_VECTOR_MARK, + ) + S.theta_axis(ax) + _theta_ticks(ax) + S.shade_outside(ax, *shade) + ax.set_ylabel(r"$\theta\,\xi \times 10^{4}$") + S.label_box(ax, rf"$\xi_{sign}$", loc="upper left") + _pte_header( + ax, rf"$\xi_{sign}^B$ PTE ${S.fmt_pte(ptes.get(xi + '_B'))}$" + ) + axes[-1].set_xlabel(r"$\theta$ [arcmin]") + # Shared y range from the in-cut points, so the excluded large-scale + # ambiguous modes don't set the scale. + inside = np.zeros(len(theta), bool) + inside[start:stop] = True + lo = min(v[0][inside].min() for v in values) + hi = max(v[1][inside].max() for v in values) + pad = 0.12 * (hi - lo) + for ax in axes: + ax.set_ylim(lo - pad, hi + 2.5 * pad) + handles = [ + Line2D([], [], ls="none", marker=m, color=c, alpha=a, + markersize=4, label=lab) + for _, m, c, a, _, lab in series + ] + fig.legend(handles=handles, loc="outside upper center", ncol=4, + handletextpad=0.1, columnspacing=0.9, borderaxespad=0.1) + if "combined" in ptes: + fig.text(0.99, 0.003, rf"joint $\xi_\pm^B$ PTE " + rf"${S.fmt_pte(ptes['combined'])}$", ha="right", va="bottom", + fontsize=8, color=S.INK_MUTED) + if blind: + S.blind_stamp(fig, blind, where="bottom") + return S.save(fig, out, "third") + + +def cosebis(co, out, full_cut=(1.0, 250.0)): + """B-mode COSEBIs in units of their uncertainty (Paper II Fig. 3). + + Fiducial cuts (filled, COSEBIs purple) over all scales (open, light tint), + each in its own panel. + """ + fid = co["fiducial"] + rows = [ + (fid, "o", True, S.COLOR_COSEBIS), + (full_cut, "D", False, S.COLOR_COSEBIS_ALL), + ] + n = np.arange(1, co["nmodes"] + 1) + with S.figure_style(): + fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True, + sharey=True) + extent = 0 + for ax, (cut, marker, filled, color) in zip(axes, rows): + r = co["cuts"][cut] + y = r["Bn"] / r["sigma_B"] + extent = max(extent, np.max(np.abs(y)) + 1) + S.zero_line(ax) + ax.errorbar( + n, y, yerr=np.ones_like(y), marker=marker, color=color, + markerfacecolor=color if filled else "none", + markeredgecolor=color, **S.DATA_VECTOR_MARK, + ) + ax.set_ylabel(r"$B_n / \sigma_n$") + S.label_box(ax, rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", + loc="upper left", fontsize=8.5) + _pte_header(ax, rf"$B_n$ PTE ${S.fmt_pte(r['pte_B'])}$") + lim = np.ceil(extent + 0.6) + axes[0].set_ylim(-lim, lim + 1.2) + axes[0].yaxis.set_major_locator(plt.MultipleLocator(2)) + axes[-1].set_xlim(0.4, n[-1] + 0.6) + axes[-1].set_xticks(n[::2] if len(n) > 12 else n) + axes[-1].set_xlabel(r"COSEBI mode $n$") + _spacer_legend(fig) + return S.save(fig, out, "third") + + +def _spacer_legend(fig): + """Reserve the legend band the pure E/B figure uses, keeping rows aligned.""" + fig.legend(handles=[Line2D([], [], ls="none", label=" ")], + loc="outside upper center", handlelength=0) + + +def _cl_cut_mask(cl, ell_cut): + return (cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1]) + + +def cl_pte(cl, key, ell_cut=None): + """χ² PTE of one spectrum against zero, over bins inside ``ell_cut``.""" + mask = np.ones(len(cl["ell"]), bool) if ell_cut is None else _cl_cut_mask( + cl, ell_cut) + d = cl[key][mask] + cov = cl["cov"][f"{key}_{key}"][np.ix_(mask, mask)] + return float(stats.chi2.sf(d @ np.linalg.solve(cov, d), mask.sum())) + + +def pseudo_cl(cl, out, ell_cut): + """C_ℓ^BB and C_ℓ^EB in units of their uncertainty (Paper II Fig. 4).""" + rows = [ + ("BB", "o", True, S.COLOR_CL_BB, r"$C_\ell^{BB}$"), + ("EB", "D", False, S.COLOR_CL_EB, r"$C_\ell^{EB}$"), + ] + ell = cl["ell"] + with S.figure_style(): + fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True, + sharey=True) + extent = 0 + for ax, (key, marker, filled, color, label) in zip(axes, rows): + y = cl[key] / cl[f"sigma_{key}"] + extent = max(extent, np.max(np.abs(y)) + 1) + S.zero_line(ax) + ax.errorbar( + ell, y, yerr=np.ones_like(y), marker=marker, color=color, + markerfacecolor=color if filled else "none", + markeredgecolor=color, **S.DATA_VECTOR_MARK, + ) + ax.set_xscale("function", functions=(np.sqrt, np.square)) + ax.set_ylabel(r"$C_\ell / \sigma$") + S.label_box(ax, label, loc="upper left") + _pte_header( + ax, + rf"PTE ${S.fmt_pte(cl_pte(cl, key, ell_cut))}$" + rf" (all $\ell$: ${S.fmt_pte(cl_pte(cl, key))}$)", + ) + mask = _cl_cut_mask(cl, ell_cut) + lo = cl["ell_lo"][mask][0] + hi = cl["ell_hi"][mask][-1] + for ax in axes: + ax.set_xlim(ell.min() * 0.85, ell.max() * 1.04) + S.shade_outside(ax, lo, hi) + ax.set_xticks([100, 400, 900, 1600]) + ax.set_xticklabels(["$100$", "$400$", "$900$", "$1600$"]) + ax.set_xticks([i * 100 for i in range(1, 21)], minor=True) + lim = np.ceil(extent + 0.6) + axes[0].set_ylim(-lim, lim + 1.2) + axes[0].yaxis.set_major_locator(plt.MultipleLocator(2)) + axes[-1].set_xlabel(r"$\ell$") + _spacer_legend(fig) + return S.save(fig, out, "third") diff --git a/src/sp_validation/dashboard/field_maps.py b/src/sp_validation/dashboard/field_maps.py new file mode 100644 index 000000000..4039e6704 --- /dev/null +++ b/src/sp_validation/dashboard/field_maps.py @@ -0,0 +1,229 @@ +"""Catalogue-level products behind the dashboard sky maps and weight figure. + +Two cached products, each computed once from the calibrated catalogue and +read by the renderers in :mod:`sp_validation.dashboard`: + +``field_maps_.npz`` + Per-pixel sums on a nested HEALPix grid: galaxy counts and weight sums, + weighted ellipticity and ellipticity-squared sums, and the PSF model + (ellipticity, FWHM) at the galaxy positions; plus star counts and the + star-minus-PSF-model residual sums. Means, dispersions and effective + densities are ratios of these sums, so a renderer can coarsen the grid + by summing children before dividing. Only occupied pixels are stored. + +``weight_cells_.npz`` + The galaxy weight on the SNR x size-ratio grid it is built on + (:func:`sp_validation.calibration.get_w_des`): per-cell counts and the + weight range within each cell, the grid edges, and the distinct weight + values with their multiplicities. + +Usage (inside the container):: + + python -m sp_validation.dashboard.field_maps \\ + --galaxies shape_catalog_cut_ngmix.fits --stars stars.hdf5 \\ + --version SP_v2.0 --out-dir cosmo_val/ +""" + +import argparse +import json +from pathlib import Path + +import fitsio +import healpy as hp +import numpy as np + +from sp_validation.io import read_catalogue + +GALAXY_COLUMNS = [ + "RA", + "Dec", + "w_des", + "e1", + "e2", + "e1_PSF", + "e2_PSF", + "fwhm_PSF", + "snr", + "NGMIX_T_NOSHEAR", + "NGMIX_T_PSF_RECONV_NOSHEAR", +] +STAR_COLUMNS = [ + "RA", + "DEC", + "HSM_G1_PSF", + "HSM_G2_PSF", + "HSM_T_PSF", + "HSM_G1_STAR", + "HSM_G2_STAR", + "HSM_T_STAR", + "HSM_FLAG_PSF", + "HSM_FLAG_STAR", +] +GALAXY_SUMS = [ + "n_gal", + "sum_w", + "sum_w2", + "sum_w_e1", + "sum_w_e2", + "sum_w2_ee", + "sum_w_e1_psf", + "sum_w_e2_psf", + "sum_w_fwhm_psf", +] +STAR_SUMS = [ + "n_star", + "sum_de1", + "sum_de2", + "sum_dT_over_T", + "sum_T_psf", +] + + +def weight_grid_edges(n_bins, snr_range, size_ratio_range): + """Log-spaced SNR and size-ratio edges of the weight grid.""" + snr_edges = np.logspace(*np.log10(snr_range), n_bins + 1) + size_edges = np.logspace(*np.log10(size_ratio_range), n_bins + 1) + return snr_edges, size_edges + + +def weight_cell_index(values, edges): + """Cell index of each value, matching ``calibration.cut_to_bins``. + + Bins are right-closed, ``(edge[i], edge[i+1]]``; a value outside the + edges belongs to the last cell, as ``cut_to_bins`` assigns it. + """ + n_bins = len(edges) - 1 + index = np.searchsorted(edges, values, side="left") - 1 + outside = (index < 0) | (index >= n_bins) | ~np.isfinite(values) + index[outside] = n_bins - 1 + return index + + +def _accumulate(total, pixels, weights, npix): + total += np.bincount(pixels, weights=weights, minlength=npix) + + +def galaxy_products(path, nside, n_bins, snr_range, size_ratio_range, chunk): + """Stream the galaxy catalogue once into map sums and weight-cell tallies.""" + npix = hp.nside2npix(nside) + sums = {key: np.zeros(npix) for key in GALAXY_SUMS} + snr_edges, size_edges = weight_grid_edges(n_bins, snr_range, size_ratio_range) + cell_count = np.zeros((n_bins, n_bins), dtype=np.int64) + cell_w_min = np.full((n_bins, n_bins), np.inf) + cell_w_max = np.full((n_bins, n_bins), -np.inf) + out_of_range = {"snr_low": 0, "snr_high": 0, "size_low": 0, "size_high": 0} + weights = [] + + with fitsio.FITS(path) as fits: + table = fits[1] + n_rows = table.get_nrows() + for start in range(0, n_rows, chunk): + rows = table[GALAXY_COLUMNS][start : start + chunk] + pix = hp.ang2pix(nside, rows["RA"], rows["Dec"], lonlat=True, nest=True) + w = rows["w_des"].astype(np.float64) + e1, e2 = rows["e1"], rows["e2"] + for key, value in ( + ("n_gal", None), + ("sum_w", w), + ("sum_w2", w**2), + ("sum_w_e1", w * e1), + ("sum_w_e2", w * e2), + ("sum_w2_ee", w**2 * (e1**2 + e2**2)), + ("sum_w_e1_psf", w * rows["e1_PSF"]), + ("sum_w_e2_psf", w * rows["e2_PSF"]), + ("sum_w_fwhm_psf", w * rows["fwhm_PSF"]), + ): + _accumulate(sums[key], pix, value, npix) + + snr = rows["snr"] + size_ratio = rows["NGMIX_T_NOSHEAR"] / rows["NGMIX_T_PSF_RECONV_NOSHEAR"] + out_of_range["snr_low"] += int(np.sum(snr <= snr_range[0])) + out_of_range["snr_high"] += int(np.sum(snr > snr_range[1])) + out_of_range["size_low"] += int(np.sum(size_ratio <= size_ratio_range[0])) + out_of_range["size_high"] += int(np.sum(size_ratio > size_ratio_range[1])) + i = weight_cell_index(snr, snr_edges) + j = weight_cell_index(size_ratio, size_edges) + flat = i * n_bins + j + cell_count += np.bincount(flat, minlength=n_bins**2).reshape(n_bins, -1) + np.minimum.at(cell_w_min.ravel(), flat, w) + np.maximum.at(cell_w_max.ravel(), flat, w) + weights.append(w) + + values, multiplicity = np.unique(np.concatenate(weights), return_counts=True) + cells = { + "snr_edges": snr_edges, + "size_ratio_edges": size_edges, + "count": cell_count, + "w_min": np.where(cell_count > 0, cell_w_min, np.nan), + "w_max": np.where(cell_count > 0, cell_w_max, np.nan), + "w_values": values, + "w_multiplicity": multiplicity, + "n_rows": n_rows, + "out_of_range": json.dumps(out_of_range), + } + return sums, cells + + +def star_sums(path, nside): + """Star counts and star-minus-PSF-model residual sums per pixel.""" + npix = hp.nside2npix(nside) + stars = read_catalogue(path, columns=STAR_COLUMNS) + keep = (stars["HSM_FLAG_PSF"] == 0) & (stars["HSM_FLAG_STAR"] == 0) + stars = stars[keep] + pix = hp.ang2pix(nside, stars["RA"], stars["DEC"], lonlat=True, nest=True) + t_star, t_psf = stars["HSM_T_STAR"], stars["HSM_T_PSF"] + sums = {key: np.zeros(npix) for key in STAR_SUMS} + for key, value in ( + ("n_star", None), + ("sum_de1", stars["HSM_G1_STAR"] - stars["HSM_G1_PSF"]), + ("sum_de2", stars["HSM_G2_STAR"] - stars["HSM_G2_PSF"]), + ("sum_dT_over_T", (t_star - t_psf) / t_star), + ("sum_T_psf", t_psf), + ): + _accumulate(sums[key], pix, value, npix) + return sums, int(keep.sum()) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--galaxies", required=True, help="calibrated galaxy FITS") + parser.add_argument("--stars", required=True, help="star/PSF catalogue") + parser.add_argument("--version", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--nside", type=int, default=512) + parser.add_argument("--weight-bins", type=int, default=20) + parser.add_argument("--snr-range", type=float, nargs=2, default=(10.0, 500.0)) + parser.add_argument( + "--size-ratio-range", type=float, nargs=2, default=(0.707, 3.0) + ) + parser.add_argument("--chunk", type=int, default=4_000_000) + args = parser.parse_args(argv) + + out_dir = Path(args.out_dir) + gal, cells = galaxy_products( + args.galaxies, + args.nside, + args.weight_bins, + args.snr_range, + args.size_ratio_range, + args.chunk, + ) + stars, n_stars = star_sums(args.stars, args.nside) + occupied = np.flatnonzero((gal["n_gal"] > 0) | (stars["n_star"] > 0)) + np.savez_compressed( + out_dir / f"field_maps_{args.version}.npz", + nside=args.nside, + nest=True, + pixels=occupied, + n_stars_used=n_stars, + **{key: value[occupied] for key, value in {**gal, **stars}.items()}, + ) + np.savez_compressed( + out_dir / f"weight_cells_{args.version}.npz", + version=args.version, + **cells, + ) + + +if __name__ == "__main__": + main() diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py new file mode 100644 index 000000000..248fab08b --- /dev/null +++ b/src/sp_validation/dashboard/products.py @@ -0,0 +1,248 @@ +"""Loaders from cached cosmo_val products to the plain arrays the figures draw. + +Every loader takes explicit paths and returns a dict; none measures, fits or +transforms anything. Cosmological products are read through the blind-stamp +checks of :mod:`sp_validation.sacc_io`, so a figure can only draw vectors +saved under the blind it was asked for. + +:class:`RunProducts` resolves those paths inside a cosmo_val run directory for +one catalogue version; the figure functions never see the directory layout. +""" + +import glob +import json +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + +from sp_validation import sacc_io +from sp_validation.b_modes import load_cosebis_results, load_pure_eb_results + + +def _one(pattern): + matches = sorted(glob.glob(str(pattern))) + if len(matches) != 1: + raise FileNotFoundError(f"expected one match for {pattern}, found {matches}") + return Path(matches[0]) + + +def _maybe(pattern): + try: + return _one(pattern) + except FileNotFoundError: + return None + + +@dataclass +class RunProducts: + """Paths of one version's cached products in a cosmo_val run directory.""" + + run_dir: Path + version: str + extra_dir: Path | None = None + + @property + def cv(self): + return Path(self.run_dir) / "cosmo_val" + + @property + def assembled(self): + return self.cv / f"{self.version}.sacc" + + @property + def pure_eb(self): + return _one(self.cv / f"{self.version}_eb_*_data.npz") + + @property + def cosebis(self): + return _one(self.cv / f"{self.version}_cosebis_*_data.npz") + + @property + def pseudo_cl_cov(self): + return _one(self.cv / f"pseudo_cl_cov_{self.version}_*.fits") + + @property + def psf_fit(self): + return _one(self.cv / f"psf_fit_{self.version}_*.npz") + + @property + def tau_cov(self): + return _one(self.cv / "rho_tau_stats" / f"cov_tau_{self.version}_*.npy") + + @property + def leakage(self): + return self.cv / "leakage_coefficients.json" + + @property + def summary(self): + return self.cv / "bmode_summary.json" + + @property + def xi_covariance(self): + """OneCovariance reporting-grid ξ± covariance (.dat), if present.""" + return _maybe( + Path(self.run_dir) + / "cosmo_inference/data/covariance" + / f"covariance_{self.version}_ng_*_masked" + / f"covariance_{self.version}_ng_*_masked.dat" + ) + + @property + def field_maps(self): + return _maybe(Path(self.extra_dir or self.cv) / f"field_maps_{self.version}.npz") + + @property + def weight_cells(self): + return _maybe( + Path(self.extra_dir or self.cv) / f"weight_cells_{self.version}.npz" + ) + + +def blind_of(sacc_path): + """The blind a SACC was saved under.""" + return sacc_io.stamp(sacc_io.load(sacc_path)) + + +def load_xi(sacc_path, blind): + """Reporting-grid ξ± with the covariance carried by the assembled SACC.""" + s = sacc_io.load(sacc_path, blind=blind) + view = sacc_io.xi_correlation(s, grid="reporting") + n = len(view.xip) + cov = np.asarray(view.cov) + return { + "theta": view.meanr, + "xip": view.xip, + "xim": view.xim, + "sigma_xip": np.sqrt(np.diag(cov)[:n]), + "sigma_xim": np.sqrt(np.diag(cov)[n:]), + "cov": cov, + "blind": blind, + } + + +def load_pure_eb(npz_path, blind): + r = load_pure_eb_results(npz_path, blind=blind) + return { + "theta": r["theta"], + "left_edges": r["left_edges"], + "right_edges": r["right_edges"], + "xip": r["xip"], + "xim": r["xim"], + "sigma_xip": np.sqrt(r["var_xip"]), + "sigma_xim": np.sqrt(r["var_xim"]), + **{ + f"{xi}_{m}": r[f"{xi}_{m}"] + for xi in ("xip", "xim") + for m in ("E", "B", "amb") + }, + **{ + f"sigma_{xi}_{m}": r[f"std_{xi}_{m}"] + for xi in ("xip", "xim") + for m in ("E", "B", "amb") + }, + "cov": r["cov"], + "pte_matrices": r["pte_matrices"], + } + + +def load_cosebis(npz_path, blind): + """Every cached COSEBIs scale cut, keyed by ``(theta_min, theta_max)``.""" + cuts, _ = load_cosebis_results(npz_path, blind=blind) + data = sacc_io.load_npz(npz_path, blind=blind) + out = {} + for cut, r in cuts.items(): + n = len(r["Bn"]) + cov = r["cov"] + out[tuple(float(c) for c in cut)] = { + "En": r["En"], + "Bn": r["Bn"], + "sigma_E": np.sqrt(np.diag(cov)[:n]), + "sigma_B": np.sqrt(np.diag(cov)[n:]), + "cov": cov, + "pte_B": float(r["pte_B"]), + } + return { + "cuts": out, + "fiducial": tuple(float(c) for c in data["scale_cut"]), + "nmodes": len(data["Bn"]), + } + + +def powspace_edges(ell_eff, lmin=None, lmax=2048, power=0.5): + """Inclusive multipole edges of the powspace band powers. + + Reconstructs the NaMaster binning of + :func:`sp_validation.pseudo_cl.make_namaster_bin` and checks that each + band's mean multipole reproduces the stored effective ell. + """ + from sp_validation.pseudo_cl import LMIN + + lmin = LMIN if lmin is None else lmin + n = len(ell_eff) + ells = np.arange(lmin, lmax + 1) + bins_ell = np.power(np.linspace(lmin**power, lmax**power, n + 1), 1 / power) + bpws = np.digitize(ells.astype(float), bins_ell) - 1 + bpws[0], bpws[-1] = 0, n - 1 + lo = np.array([ells[bpws == i].min() for i in range(n)]) + hi = np.array([ells[bpws == i].max() for i in range(n)]) + means = np.array([ells[bpws == i].mean() for i in range(n)]) + if not np.allclose(means, ell_eff, atol=0.51): + raise ValueError("powspace edges do not reproduce the stored ell_eff") + return lo, hi + + +def load_pseudo_cl(sacc_path, cov_path, blind): + """Band powers, their multipole edges, and the NaMaster covariance blocks.""" + from astropy.io import fits + + s = sacc_io.load(sacc_path, blind=blind) + ell, cl_ee, cl_bb, cl_eb, _ = sacc_io.get_pseudo_cl(s, (0, 0)) + ell = np.asarray(ell) + lo, hi = powspace_edges(ell) + with fits.open(cov_path) as hdus: + cov = { + h.name.removeprefix("COVAR_"): np.asarray(h.data) + for h in hdus + if h.name.startswith("COVAR_") + } + return { + "ell": ell, + "ell_lo": lo, + "ell_hi": hi, + "EE": cl_ee, + "BB": cl_bb, + "EB": cl_eb, + "cov": cov, + "sigma_BB": np.sqrt(np.diag(cov["BB_BB"])), + "sigma_EB": np.sqrt(np.diag(cov["EB_EB"])), + "sigma_EE": np.sqrt(np.diag(cov["EE_EE"])), + } + + +def load_rho_tau(sacc_path, tau_cov_path, psf_fit_path): + """ρ/τ statistics with their covariance blocks, and the PSF-fit samples.""" + s = sacc_io.load(sacc_path) + cov = s.covariance.dense + rho = {} + for k in range(6): + theta, p, m = sacc_io.get_rho(s, k) + idx = s.indices(sacc_io.RHO_PLUS.format(k=k)) + rho[k] = {"p": p, "m": m, "sigma_p": np.sqrt(np.abs(np.diag(cov)[idx]))} + tau = {} + for k in (0, 2, 5): + theta_tau, p, m = sacc_io.get_tau(s, (0,), k) + tau[k] = {"p": p, "m": m} + tau_cov = np.load(tau_cov_path) + n = len(theta_tau) + for i, k in enumerate((0, 2, 5)): + var = np.diag(tau_cov)[i * n : (i + 1) * n] + tau[k]["var_p"] = var + fit = dict(np.load(psf_fit_path)) + return {"theta": theta, "theta_tau": theta_tau, "rho": rho, "tau": tau, + "tau_cov": tau_cov, "fit": fit} + + +def load_json(path): + with open(path) as f: + return json.load(f) diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py new file mode 100644 index 000000000..a766e7f7d --- /dev/null +++ b/src/sp_validation/dashboard/psf.py @@ -0,0 +1,250 @@ +"""PSF leakage: ρ statistics, the τ fit and its posterior, the propagated +systematic, and every estimate of the leakage coefficient α side by side. + +The τ model is the one the ρ/τ fit uses (shear_psf_leakage, with η): + + τ0 = α ρ0 + β ρ2 + η ρ5, τ2 = α ρ2 + β ρ1 + η ρ4, τ5 = α ρ5 + β ρ4 + η ρ3 + +evaluated here on the cached posterior samples; nothing is refitted. +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.lines import Line2D +from scipy.ndimage import gaussian_filter + +from . import style as S + +# PSF figures carry no E/B mode, so they use the page accents rather than the +# mode hues: one accent for the fitted model, three validated categorical +# slots for the ρ statistics within a panel. +MODEL = "#3D5BA0" # page cobalt +SERIES = ["#2a78d6", "#eb6834", "#1baf7a"] + +TAU_MODEL = { + 0: ((0, 0), (1, 2), (2, 5)), + 2: ((0, 2), (1, 1), (2, 4)), + 5: ((0, 5), (1, 4), (2, 3)), +} +PARAMS = (r"\alpha", r"\beta", r"\eta") + + +def tau_model_samples(rt, k, samples): + """τ_{k,+}(θ) for each posterior sample, shape ``(n_samples, n_theta)``.""" + model = 0 + for param, rho_k in TAU_MODEL[k]: + model = model + samples[:, [param]] * rt["rho"][rho_k]["p"][None, :] + return model + + +def _power_of_ten(values): + top = np.nanmax(np.abs(values)) + return int(np.floor(np.log10(top))) if top > 0 else 0 + + +def tau_fit(rt, out): + """τ_{0,2,5}+ with the posterior band of the fitted model.""" + theta = rt["theta_tau"] + samples = rt["fit"]["flat_samples"] + with S.figure_style(): + fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.5)) + for ax, k in zip(axes, (0, 2, 5)): + tau = rt["tau"][k]["p"] + sigma = np.sqrt(np.clip(rt["tau"][k]["var_p"], 0, None)) + model = tau_model_samples(rt, k, samples) + lo, med, hi = np.percentile(model, [16, 50, 84], axis=0) + p = _power_of_ten(np.concatenate([tau + sigma, tau - sigma])) + scale = 10.0**p + S.zero_line(ax) + ax.fill_between(theta, lo / scale, hi / scale, color=MODEL, alpha=0.3, + lw=0) + ax.plot(theta, med / scale, color=MODEL, lw=1.1) + ax.errorbar(theta, tau / scale, yerr=sigma / scale, marker="o", + color=S.INK, **S.DATA_VECTOR_MARK) + S.theta_axis(ax) + ax.set_xticks([1, 10, 100]) + ax.set_xticklabels(["$1$", "$10$", "$100$"]) + ax.set_xlabel(r"$\theta$ [arcmin]") + ax.set_ylabel(rf"$\tau_{{{k},+}} \times 10^{{{-p}}}$") + S.label_box(ax, rf"$\tau_{{{k}}}$", loc="upper right") + handles = [ + Line2D([], [], ls="none", marker="o", color=S.INK, markersize=3.5, + label=r"measured $\tau_{k,+}$"), + Line2D([], [], color=MODEL, lw=4, alpha=0.4, + label=r"$\alpha, \beta, \eta$ posterior (68%)"), + ] + fig.legend(handles=handles, loc="outside upper center", ncol=2) + return S.save(fig, out, "full") + + +def _contour_levels(h, fractions=(0.68, 0.95)): + flat = np.sort(h.ravel())[::-1] + cum = np.cumsum(flat) / flat.sum() + return sorted(flat[np.searchsorted(cum, f)] for f in fractions) + + +def posterior(rt, out, markers=(0, 1, 1)): + """Corner plot of the (α, β, η) posterior from the cached samples.""" + samples = rt["fit"]["flat_samples"] + n = samples.shape[1] + with S.figure_style(): + fig, axes = plt.subplots(n, n, figsize=(3.6, 3.6)) + lims = [np.percentile(samples[:, i], [0.3, 99.7]) for i in range(n)] + for i in range(n): + for j in range(n): + ax = axes[i, j] + ax.tick_params(labelsize=7, top=False, right=False) + if j > i: + ax.set_visible(False) + continue + if i == j: + x = samples[:, i] + ax.hist(x, bins=40, range=lims[i], histtype="stepfilled", + color=MODEL, alpha=0.25) + ax.hist(x, bins=40, range=lims[i], histtype="step", + color=MODEL, lw=0.9) + lo, med, hi = np.percentile(x, [16, 50, 84]) + ax.set_title( + rf"${PARAMS[i]} = {med:.3g}^{{+{hi - med:.2g}}}" + rf"_{{-{med - lo:.2g}}}$", fontsize=7.5) + ax.set_yticks([]) + if markers[i] is not None: + ax.axvline(markers[i], color=S.INK_MUTED, lw=0.7, ls=":") + else: + h, xe, ye = np.histogram2d(samples[:, j], samples[:, i], + bins=40, range=[lims[j], lims[i]]) + h = gaussian_filter(h, 1.0) + levels = _contour_levels(h) + xc, yc = (xe[1:] + xe[:-1]) / 2, (ye[1:] + ye[:-1]) / 2 + ax.contourf(xc, yc, h.T, levels=[levels[0], h.max()], + colors=[MODEL], alpha=0.18) + ax.contourf(xc, yc, h.T, levels=[levels[1], h.max()], + colors=[MODEL], alpha=0.38) + ax.contour(xc, yc, h.T, levels=levels, colors=[MODEL], + linewidths=0.7) + for v, axis in ((markers[j], ax.axvline), (markers[i], ax.axhline)): + if v is not None: + axis(v, color=S.INK_MUTED, lw=0.7, ls=":") + ax.set_ylim(lims[i]) + ax.set_xlim(lims[j]) + ax.locator_params(nbins=3) + if i == n - 1: + ax.set_xlabel(rf"${PARAMS[j]}$") + else: + ax.set_xticklabels([]) + if j == 0 and i > 0: + ax.set_ylabel(rf"${PARAMS[i]}$") + elif j > 0 and i != j: + ax.set_yticklabels([]) + fig.get_layout_engine().set(hspace=0.02, wspace=0.02) + return S.save(fig, out, "half") + + +def rho_stats(rt, out): + """|ρ_{k,+}| on a log axis: filled markers positive, open negative.""" + theta = rt["theta"] + groups = [ + ((0, 1, 2), r"$\rho_{0,1,2}$: ellipticity"), + ((3, 4, 5), r"$\rho_{3,4,5}$: size-weighted"), + ] + with S.figure_style(): + fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.7), sharey=True) + for ax, (ks, label) in zip(axes, groups): + for c, k in zip(SERIES, ks): + rho = rt["rho"][k]["p"] + sig = rt["rho"][k]["sigma_p"] + pos = rho > 0 + ax.errorbar(theta, np.abs(rho), yerr=sig, color=c, ls="-", + lw=0.7, marker="none", elinewidth=0.6, capsize=0, + alpha=0.9) + ax.plot(theta[pos], rho[pos], "o", color=c, ms=3.4, + mec=c, label=rf"$\rho_{k}$") + ax.plot(theta[~pos], -rho[~pos], "o", color=c, ms=3.4, + mfc=S.RAG, mec=c, mew=0.9) + ax.set_yscale("log") + S.theta_axis(ax) + ax.set_xticks([1, 10, 100]) + ax.set_xticklabels(["$1$", "$10$", "$100$"]) + ax.set_xlabel(r"$\theta$ [arcmin]") + ax.legend(loc="upper right", ncol=3, handletextpad=0.2, + columnspacing=0.8) + ax.set_title(label, loc="left", fontsize=9) + axes[0].set_ylabel(r"$|\rho_{k,+}(\theta)|$") + axes[0].set_ylim(top=axes[0].get_ylim()[1] * 8) + axes[1].set_title(r"open markers: $\rho < 0$", loc="right", fontsize=8.5, + color=S.INK_MUTED) + return S.save(fig, out, "full") + + +def xi_sys_ratio(rt, xi, out, reference=0.1): + """Propagated PSF systematic relative to the blinded ξ±.""" + fit = rt["fit"] + theta = fit["theta"] + with S.figure_style(): + fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.5)) + for ax, comp, key, sign in zip(axes, ("plus", "minus"), ("xip", "xim"), + "+-"): + ratio = fit[f"samples_{comp}"] / xi[key][None, :] + lo, med, hi = np.percentile(ratio, [16, 50, 84], axis=0) + for r in (-reference, reference): + ax.axhline(r, color=S.INK_MUTED, lw=0.7, ls=(0, (2, 2))) + S.zero_line(ax) + ax.fill_between(theta, lo, hi, color=MODEL, alpha=0.3, lw=0) + ax.plot(theta, med, color=MODEL, lw=1.1) + S.theta_axis(ax) + ax.set_xticks([1, 10, 100]) + ax.set_xticklabels(["$1$", "$10$", "$100$"]) + ax.set_xlabel(r"$\theta$ [arcmin]") + ax.set_ylabel(rf"$\xi_{sign}^{{\rm sys}} / \xi_{sign}$") + top = max(np.nanmax(hi), reference) * 1.25 + bot = min(np.nanmin(lo), -reference) * 1.25 + ax.set_ylim(max(bot, -1.0), min(top, 1.0)) + S.label_box(ax, rf"$\xi_{sign}$", loc="upper left") + return S.save(fig, out, "full") + + +ALPHA_ROWS = [ + ("aii_mean", r"$\langle a_{ii} \rangle$", "object-wise"), + ("a11", r"$a_{11}$", "object-wise"), + ("a22", r"$a_{22}$", "object-wise"), + ("alpha_mean", r"$\bar\alpha$", "scale-dependent"), + ("alpha_0", r"$\alpha(0)$", "scale-dependent"), + ("alpha_1", r"$\alpha(\theta_{\min})$", "scale-dependent"), +] + + +def alpha_estimates(leakage, rt, out): + """Forest plot of every PSF-leakage coefficient estimate (± 1σ).""" + rows = [] + for key, label, group in ALPHA_ROWS: + if key in leakage: + value, err = leakage[key] + rows.append((label, group, value, err, err)) + a = rt["fit"]["flat_samples"][:, 0] + lo, med, hi = np.percentile(a, [16, 50, 84]) + rows.append((r"$\alpha$", r"$\rho/\tau$ fit", med, med - lo, hi - med)) + with S.figure_style(): + fig, ax = plt.subplots(figsize=(3.6, 2.9)) + y = np.arange(len(rows))[::-1].astype(float) + groups = [r[1] for r in rows] + # a small gap between estimator groups + for i in range(1, len(rows)): + if groups[i] != groups[i - 1]: + y[i:] -= 0.6 + ax.axvline(0, color=S.INK, lw=0.7, ls=(0, (4, 3)), alpha=0.6) + for yi, (label, group, v, em, ep) in zip(y, rows): + ax.errorbar([v], [yi], xerr=[[em], [ep]], fmt="o", color=S.INK, + ms=3.6, elinewidth=0.9, capsize=1.8) + ax.set_yticks(y) + ax.set_yticklabels([r[0] for r in rows]) + ax.tick_params(axis="y", length=0, right=False) + ax.tick_params(axis="x", top=False) + prev = None + for yi, g in zip(y, groups): + if g != prev: + ax.text(1.0, yi + 0.42, g, transform=ax.get_yaxis_transform(), + ha="right", va="bottom", fontsize=7.5, color=S.INK_MUTED) + prev = g + ax.set_ylim(y.min() - 0.7, y.max() + 0.9) + ax.set_xlabel("PSF leakage coefficient") + return S.save(fig, out, "half") diff --git a/src/sp_validation/dashboard/scale_cuts.py b/src/sp_validation/dashboard/scale_cuts.py new file mode 100644 index 000000000..bf6deb851 --- /dev/null +++ b/src/sp_validation/dashboard/scale_cuts.py @@ -0,0 +1,147 @@ +"""B-mode PTEs as a function of the scale cut (Paper II Figs. 8-9 style). + +Each heatmap cell is the PTE of the B-mode null test over the bins between a +lower cut (x) and an upper cut (y); the fiducial cut is outlined. The colour +map is anchored at the PTE threshold: solid blue below it, solid red above +one minus it, a smooth gradient between. +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Rectangle +from scipy import stats + +from sp_validation.b_modes import bins_from_scale_cut + +from . import style as S + + +def cl_pte_matrix(cl, key="BB"): + """PTE of ``key`` against zero for every contiguous band range [i, j].""" + data = cl[key] + cov = cl["cov"][f"{key}_{key}"] + n = len(data) + pte = np.full((n, n), np.nan) + for i in range(n): + for j in range(i, n): + d = data[i : j + 1] + c = cov[i : j + 1, i : j + 1] + pte[i, j] = stats.chi2.sf(d @ np.linalg.solve(c, d), j - i + 1) + return pte + + +def _tick_subset(n, k=6): + return np.unique(np.round(np.linspace(0, n - 1, k)).astype(int)) + + +def _heatmap(ax, pte, lo_labels, hi_labels, fid, label, cmap, norm): + """Triangular PTE heatmap on bin indices; ``fid`` is ``(i, j)`` inclusive.""" + n = pte.shape[0] + grid = np.arange(n + 1) - 0.5 + mesh = ax.pcolormesh(grid, grid, pte.T, cmap=cmap, norm=norm, + edgecolors="face", linewidth=0, rasterized=True) + ax.set_facecolor("#e4dccb") + ax.set_aspect("equal") + ax.set_xlim(-0.5, n - 0.5) + ax.set_ylim(-0.5, n - 0.5) + ticks = _tick_subset(n) + ax.set_xticks(ticks) + ax.set_xticklabels([f"${lo_labels[t]}$" for t in ticks], rotation=45, + ha="right", rotation_mode="anchor") + ax.set_yticks(ticks) + ax.set_yticklabels([f"${hi_labels[t]}$" for t in ticks]) + ax.tick_params(which="both", top=False, right=False, length=2.5) + if fid is not None: + i, j = fid + ax.add_patch(Rectangle((i - 0.5, j - 0.5), 1, 1, fill=False, + edgecolor=S.INK, lw=1.6, zorder=5)) + S.label_box(ax, label, loc="lower right", fontsize=9) + return mesh + + +def _fmt_angle(v): + return f"{v:.0f}" if v >= 10 else f"{v:.1f}".rstrip("0").rstrip(".") + + +def config_space(pe, out, scale_cut, layout="wide"): + """ξ+^B, ξ−^B and joint ξ±^B PTEs over all reporting-bin scale cuts.""" + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], + scale_cut) + fid = (start, stop - 1) + lo = [_fmt_angle(v) for v in pe["left_edges"]] + hi = [_fmt_angle(v) for v in pe["right_edges"]] + panels = [ + ("xip_B", r"$\xi_+^B$"), + ("xim_B", r"$\xi_-^B$"), + ("combined", r"$\xi_\pm^B$ joint"), + ] + cmap, norm = S.pte_cmap(), S.pte_norm() + with S.figure_style(): + if layout == "wide": + fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.75), sharey=True) + else: + fig, axes = plt.subplots(3, 1, figsize=(3.6, 9.6), sharex=True) + for ax, (key, label) in zip(axes, panels): + mesh = _heatmap(ax, pe["pte_matrices"][key], lo, hi, fid, label, + cmap, norm) + for ax in axes if layout == "wide" else axes[-1:]: + ax.set_xlabel(r"$\theta_{\min}$ [arcmin]") + for ax in axes[:1] if layout == "wide" else axes: + ax.set_ylabel(r"$\theta_{\max}$ [arcmin]") + S.pte_colorbar(fig, mesh, axes, shrink=0.9 if layout == "wide" else 0.5, + aspect=25, pad=0.02, + location="right" if layout == "wide" else "bottom") + return S.save(fig, out, "full" if layout == "wide" else "half") + + +def harmonic_and_cosebis(cl, co, out, ell_cut, layout="wide"): + """C_ℓ^BB PTEs over band ranges, beside COSEBIs B_n PTEs per cached cut.""" + pte = cl_pte_matrix(cl, "BB") + inside = np.flatnonzero((cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1])) + fid = (inside[0], inside[-1]) + lo = [f"{v:.0f}" for v in cl["ell_lo"]] + hi = [f"{v:.0f}" for v in cl["ell_hi"]] + cmap, norm = S.pte_cmap(), S.pte_norm() + with S.figure_style(): + if layout == "wide": + fig, (ax_cl, ax_co) = plt.subplots( + 1, 2, figsize=(7.2, 3.0), width_ratios=(1, 1.15)) + else: + fig, (ax_cl, ax_co) = plt.subplots(2, 1, figsize=(3.6, 6.6), + height_ratios=(1.25, 1)) + mesh = _heatmap(ax_cl, pte, lo, hi, fid, r"$C_\ell^{BB}$", cmap, norm) + ax_cl.set_xlabel(r"$\ell_{\min}$") + ax_cl.set_ylabel(r"$\ell_{\max}$") + _cosebis_cuts(ax_co, co, cmap, norm) + S.pte_colorbar(fig, mesh, [ax_cl, ax_co], aspect=25, pad=0.02, + shrink=0.9 if layout == "wide" else 0.6, + location="right" if layout == "wide" else "bottom") + return S.save(fig, out, "full" if layout == "wide" else "half") + + +def _cosebis_cuts(ax, co, cmap, norm, threshold=S.PTE_THRESHOLD): + """One row per cached COSEBIs cut: the B_n PTE on a [0, 1] axis.""" + cuts = sorted(co["cuts"], key=lambda c: (c[1] - c[0], c[0]), reverse=True) + y = np.arange(len(cuts))[::-1] + ax.axvspan(0, threshold, color=cmap(0), alpha=0.18, lw=0) + ax.axvspan(1 - threshold, 1, color=cmap(cmap.N - 1), alpha=0.18, lw=0) + for yi, cut in zip(y, cuts): + p = co["cuts"][cut]["pte_B"] + is_fid = cut == co["fiducial"] + ax.hlines(yi, 0, 1, color=S.RULE, lw=0.6, zorder=1) + ax.scatter([p], [yi], s=46 if is_fid else 30, color=cmap(norm(p)), + edgecolor=S.INK, linewidth=1.4 if is_fid else 0.6, zorder=3, + marker="s" if is_fid else "o") + ax.text(min(p + 0.035, 0.97), yi + 0.18, f"${S.fmt_pte(p)}$", + fontsize=7.5, color=S.INK_MUTED, va="bottom", + ha="left" if p < 0.85 else "right") + ax.set_yticks(y) + ax.set_yticklabels([rf"${c[0]:g}'\!-\!{c[1]:g}'$" for c in cuts]) + ax.set_xlim(0, 1) + ax.set_ylim(-0.7, len(cuts) - 0.3) + ticks = (threshold, 0.5, 1 - threshold) + ax.set_xticks(ticks) + ax.set_xticklabels([f"${t:g}$" for t in ticks]) + ax.tick_params(which="both", top=False, right=False, left=False) + ax.set_xlabel(r"$B_n$ PTE ($n \leq %d$)" % co["nmodes"]) + S.label_box(ax, "COSEBIs", loc="lower right", fontsize=9) diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py new file mode 100644 index 000000000..ad6982fda --- /dev/null +++ b/src/sp_validation/dashboard/sky.py @@ -0,0 +1,436 @@ +"""Footprint and field maps in a projection that suits the survey. + +The footprint is split into its disconnected parts (e.g. the main patch and +a long thin strip). Each part gets its own Lambert azimuthal equal-area +(zenithal equal-area) projection centred on it, rotated so its long axis is +horizontal unless it is the main part, which keeps north up; RA increases to +the left as on the sky. All parts are drawn at the same angular scale, the +main part on top and any others below it. + +Map products are ``field_maps_.npz`` (see +:mod:`sp_validation.dashboard.field_maps`) and the cosmo_val footprint +``footprint_.npz``. Quantities are ratios of pixel sums, coarsened +by summing nested children before dividing. +""" + +from dataclasses import dataclass + +import healpy as hp +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm + +from . import style as S + +DEG = np.pi / 180 + +SEQUENTIAL = LinearSegmentedColormap.from_list( + "parchment_teal", ["#f3ead6", "#9cc3b5", "#3F8278", "#1d4a44", "#0f2421"] +) +DIVERGING = LinearSegmentedColormap.from_list( + "cobalt_cinnabar", ["#22386e", "#3D5BA0", "#a9b6d6", "#efe6d2", "#e4ab9f", + "#BC4538", "#6e2119"] +) + + +# --------------------------------------------------------------------------- +# Projection +# --------------------------------------------------------------------------- +@dataclass +class Zenithal: + """Lambert azimuthal equal-area projection about ``(ra0, dec0)``. + + Output coordinates are in degrees on the projection plane, x increasing + to the east-on-sky-left convention (x grows toward decreasing RA), then + rotated by ``rotation`` degrees. + """ + + ra0: float + dec0: float + rotation: float = 0.0 + + def forward(self, ra, dec): + a = (np.asarray(ra) - self.ra0) * DEG + d = np.asarray(dec) * DEG + d0 = self.dec0 * DEG + cosc = np.sin(d0) * np.sin(d) + np.cos(d0) * np.cos(d) * np.cos(a) + k = np.sqrt(2 / np.clip(1 + cosc, 1e-12, None)) + x = -k * np.cos(d) * np.sin(a) + y = k * (np.cos(d0) * np.sin(d) - np.sin(d0) * np.cos(d) * np.cos(a)) + return self._rotate(x / DEG, y / DEG, self.rotation) + + def inverse(self, x, y): + x, y = self._rotate(np.asarray(x), np.asarray(y), -self.rotation) + x, y = -x * DEG, y * DEG + rho = np.hypot(x, y) + valid = rho < 2 + c = 2 * np.arcsin(np.clip(rho / 2, 0, 1)) + d0 = self.dec0 * DEG + with np.errstate(invalid="ignore", divide="ignore"): + sin_d = np.cos(c) * np.sin(d0) + np.where( + rho > 0, y * np.sin(c) * np.cos(d0) / rho, 0) + dec = np.arcsin(np.clip(sin_d, -1, 1)) + ra = self.ra0 * DEG + np.arctan2( + x * np.sin(c), rho * np.cos(d0) * np.cos(c) - y * np.sin(d0) * np.sin(c) + ) + return np.mod(ra / DEG, 360), dec / DEG, valid + + @staticmethod + def _rotate(x, y, angle): + t = angle * DEG + return x * np.cos(t) - y * np.sin(t), x * np.sin(t) + y * np.cos(t) + + +# --------------------------------------------------------------------------- +# Footprint parts +# --------------------------------------------------------------------------- +def footprint_parts(pixels, nside, nest=True, coarse=64, min_fraction=0.01): + """Split occupied pixels into connected parts, largest first. + + Connectivity is judged on a ``coarse`` grid so masking holes do not split + a patch. Returns a list of boolean masks over ``pixels``; parts smaller + than ``min_fraction`` of the total are attached to the nearest part. + """ + ratio = (nside // coarse) ** 2 + parent = (pixels // ratio) if nest else hp.ring2nest(nside, pixels) // ratio + occupied = np.unique(parent) + occ_set = set(occupied.tolist()) + label = {} + current = 0 + for p in occupied: + if p in label: + continue + stack = [p] + label[p] = current + while stack: + q = stack.pop() + for nb in hp.get_all_neighbours(coarse, q, nest=True): + if nb >= 0 and nb in occ_set and nb not in label: + label[nb] = current + stack.append(nb) + current += 1 + labels = np.array([label[p] for p in parent]) + sizes = np.bincount(labels) + order = np.argsort(sizes)[::-1] + keep = [lab for lab in order if sizes[lab] >= min_fraction * sizes.sum()] + if len(keep) < len(order): + # Fold small islands into the nearest kept part. + vec = np.array(hp.pix2vec(nside, pixels, nest=nest)).T + centres = np.array([vec[labels == lab].mean(0) for lab in keep]) + small = ~np.isin(labels, keep) + nearest = np.argmax(vec[small] @ centres.T, axis=1) + labels[small] = np.array(keep)[nearest] + return [labels == lab for lab in keep] + + +@dataclass +class Part: + projection: Zenithal + xlim: tuple + ylim: tuple + + @property + def width(self): + return self.xlim[1] - self.xlim[0] + + @property + def height(self): + return self.ylim[1] - self.ylim[0] + + +def _centre(ra, dec): + v = np.array(hp.ang2vec(ra, dec, lonlat=True)) + m = v.mean(0) + ra0, dec0 = hp.vec2ang(m / np.linalg.norm(m), lonlat=True) + return float(ra0[0]), float(dec0[0]) + + +def make_parts(pixels, nside, nest=True, margin=1.5): + """Projection and extent for each disconnected part of the footprint.""" + parts = [] + for i, mask in enumerate(footprint_parts(pixels, nside, nest)): + ra, dec = hp.pix2ang(nside, pixels[mask], nest=nest, lonlat=True) + ra0, dec0 = _centre(ra, dec) + proj = Zenithal(ra0, dec0) + x, y = proj.forward(ra, dec) + if i > 0: + # Long axis horizontal: rotate by the principal-axis angle. + cov = np.cov(np.vstack([x, y])) + evals, evecs = np.linalg.eigh(cov) + major = evecs[:, np.argmax(evals)] + angle = np.degrees(np.arctan2(major[1], major[0])) + angle = (angle + 90) % 180 - 90 # keep RA increasing to the left + proj = Zenithal(ra0, dec0, -angle) + x, y = proj.forward(ra, dec) + parts.append(Part(proj, (x.min() - margin, x.max() + margin), + (y.min() - margin, y.max() + margin))) + return parts + + +# --------------------------------------------------------------------------- +# Maps +# --------------------------------------------------------------------------- +def coarsen(pixels, sums, nside, nside_out): + """Sum nested-pixel sums onto ``nside_out``; returns (pixels, sums).""" + ratio = (nside // nside_out) ** 2 + parent = pixels // ratio + out_pix, inverse = np.unique(parent, return_inverse=True) + return out_pix, { + key: np.bincount(inverse, weights=value, minlength=len(out_pix)) + for key, value in sums.items() + } + + +def raster(part, pixels, values, nside, resolution, nest=True): + """Sample a partial HEALPix map on the part's projection-plane grid.""" + nx = int(np.ceil(part.width / resolution)) + ny = int(np.ceil(part.height / resolution)) + x = np.linspace(part.xlim[0], part.xlim[1], nx) + y = np.linspace(part.ylim[0], part.ylim[1], ny) + xx, yy = np.meshgrid(x, y) + ra, dec, valid = part.projection.inverse(xx, yy) + pix = hp.ang2pix(nside, ra, dec, lonlat=True, nest=nest) + order = np.argsort(pixels) + pos = np.searchsorted(pixels, pix, sorter=order) + pos = np.clip(pos, 0, len(pixels) - 1) + found = valid & (pixels[order][pos] == pix) + img = np.full(pix.shape, np.nan) + img[found] = values[order][pos[found]] + return img + + +def graticule(ax, part, step_ra=None, step_dec=10, color=S.RULE, label=True): + """Draw RA/Dec lines across the part and label them along the frame.""" + ra_c, dec_c = part.projection.ra0, part.projection.dec0 + span = max(part.width, part.height) + step_ra = step_ra or (30 if abs(dec_c) > 65 else 15 if span > 25 else 10) + decs = np.arange(-90 + step_dec, 90, step_dec) + ras = np.arange(0, 360, step_ra) + t = np.linspace(0, 360, 721) + s = np.linspace(-89.5, 89.5, 360) + kw = dict(color=color, lw=0.5, zorder=1) + x0, x1 = part.xlim + y0, y1 = part.ylim + for d in decs: + x, y = part.projection.forward(t, np.full_like(t, d)) + far = _far_side(part, t, np.full_like(t, d)) + x, y = np.where(far, np.nan, x), np.where(far, np.nan, y) + ax.plot(x, y, **kw) + if label: + _edge_label(ax, x, y, (x0, x1, y0, y1), rf"${d:g}^\circ$", side="left") + for r in ras: + x, y = part.projection.forward(np.full_like(s, r), s) + far = _far_side(part, np.full_like(s, r), s) + x, y = np.where(far, np.nan, x), np.where(far, np.nan, y) + ax.plot(x, y, **kw) + if label: + _edge_label(ax, x, y, (x0, x1, y0, y1), rf"${r:g}^\circ$", side="bottom") + + +def _far_side(part, ra, dec): + v = np.array(hp.ang2vec(ra, dec, lonlat=True)) + c = hp.ang2vec(part.projection.ra0, part.projection.dec0, lonlat=True) + return v @ c < np.cos(80 * DEG) + + +def _edge_label(ax, x, y, box, text, side): + x0, x1, y0, y1 = box + ok = np.isfinite(x) & (x >= x0) & (x <= x1) & (y >= y0) & (y <= y1) + if not ok.any(): + return + if side == "left": + i = np.flatnonzero(ok)[np.argmin(x[ok])] + if x[i] - x0 > 0.08 * (x1 - x0): + return + ax.text(x0, y[i], text, ha="right", va="center", fontsize=7, + color=S.INK_MUTED, clip_on=False) + else: + i = np.flatnonzero(ok)[np.argmin(y[ok])] + if y[i] - y0 < 0.08 * (y1 - y0): + ax.text(x[i], y0, text, ha="center", va="top", fontsize=7, + color=S.INK_MUTED, clip_on=False) + return + i = np.flatnonzero(ok)[np.argmax(y[ok])] + placed = ax.__dict__.setdefault("_top_labels", []) + if y1 - y[i] < 0.08 * (y1 - y0) and all( + abs(x[i] - p) > 0.07 * (x1 - x0) for p in placed + ): + placed.append(x[i]) + ax.text(x[i], y1, text, ha="center", va="bottom", fontsize=7, + color=S.INK_MUTED, clip_on=False) + + +def _scale_bar(ax, part, length=10): + x1 = part.xlim[1] - 0.04 * part.width + y0 = part.ylim[0] + 0.05 * part.height + ax.plot([x1 - length, x1], [y0, y0], color=S.INK, lw=1.2, solid_capstyle="butt") + ax.text(x1 - length / 2, y0 + 0.012 * part.height + 0.3, rf"${length}^\circ$", + ha="center", va="bottom", fontsize=7, color=S.INK) + + +def draw_map(ax, part, pixels, values, nside, cmap, norm, resolution=None, + labels=True, scale_bar=False): + resolution = resolution or max(hp.nside2resol(nside, arcmin=True) / 60 / 2, 0.03) + img = raster(part, pixels, values, nside, resolution) + graticule(ax, part, label=labels) + im = ax.imshow(img, origin="lower", extent=(*part.xlim, *part.ylim), + cmap=cmap, norm=norm, interpolation="nearest", zorder=2, + rasterized=True) + ax.set_xlim(part.xlim) + ax.set_ylim(part.ylim) + ax.set_aspect("equal") + ax.set_axis_off() + if scale_bar: + _scale_bar(ax, part) + return im + + +def map_grid(fig, spec, parts): + """Axes for every part inside ``spec``: main part above, others below.""" + if len(parts) == 1: + return [fig.add_subplot(spec)] + heights = [parts[0].height] + [p.height * parts[0].width / max(p.width, + parts[0].width) for p in parts[1:]] + sub = spec.subgridspec(len(parts), 1, height_ratios=heights, hspace=0.05) + return [fig.add_subplot(sub[i]) for i in range(len(parts))] + + +# --------------------------------------------------------------------------- +# Quantities +# --------------------------------------------------------------------------- +def _ratio(num, den): + with np.errstate(invalid="ignore", divide="ignore"): + return np.where(den > 0, num / den, np.nan) + + +def coverage(footprint, nside_out): + """Fraction of each ``nside_out`` pixel covered by the occupied footprint.""" + pix = footprint["pixels"] + nside = int(footprint["nside_sparse"]) + ratio = (nside // nside_out) ** 2 + out, counts = np.unique(pix // ratio, return_counts=True) + return out, counts / ratio + + +def quantity(maps, name, nside_out, footprint=None): + """Pixels and values of one named map quantity at ``nside_out``.""" + nside = int(maps["nside"]) + keys = [k for k in maps if k.startswith(("n_", "sum_"))] + pix, sums = coarsen(maps["pixels"], {k: maps[k] for k in keys}, nside, nside_out) + area = hp.nside2pixarea(nside_out, degrees=True) * 3600 + if name == "n_eff": + frac = np.ones(len(pix)) + if footprint is not None: + cpix, cfrac = coverage(footprint, nside_out) + lookup = dict(zip(cpix.tolist(), cfrac.tolist())) + frac = np.array([lookup.get(p, np.nan) for p in pix.tolist()]) + values = _ratio(sums["sum_w"] ** 2, sums["sum_w2"]) / (area * frac) + keep = (sums["n_gal"] > 0) & (frac > 0.5) + elif name == "sigma_e": + values = np.sqrt(_ratio(sums["sum_w2_ee"], 2 * sums["sum_w2"])) + keep = sums["n_gal"] > 0 + elif name == "mean_w": + values = _ratio(sums["sum_w"], sums["n_gal"]) + keep = sums["n_gal"] > 0 + elif name in ("e1", "e2"): + values = _ratio(sums[f"sum_w_{name}"], sums["sum_w"]) + keep = sums["n_gal"] > 0 + elif name == "fwhm_psf": + values = _ratio(sums["sum_w_fwhm_psf"], sums["sum_w"]) + keep = sums["n_gal"] > 0 + elif name in ("de1", "de2", "dT_over_T"): + values = _ratio(sums[f"sum_{name}"], sums["n_star"]) + keep = sums["n_star"] >= 5 + else: + raise KeyError(name) + return pix[keep], values[keep] + + +def _robust_limits(values, symmetric=False, q=(1, 99)): + lo, hi = np.nanpercentile(values, q) + if symmetric: + m = max(abs(lo), abs(hi)) + return -m, m + return lo, hi + + +def _colorbar(fig, im, ax, label, symmetric): + cbar = fig.colorbar(im, ax=ax, orientation="horizontal", fraction=0.05, + pad=0.02, aspect=28, extend="both") + cbar.outline.set_linewidth(0.5) + cbar.ax.tick_params(labelsize=7.5, length=2, top=False) + cbar.locator = mpl.ticker.MaxNLocator(5, symmetric=symmetric) + cbar.update_ticks() + cbar.set_label(label, fontsize=8.5) + return cbar + + +def footprint_map(maps, footprint, out, nside=512, stats=None): + """Effective galaxy density over the footprint.""" + parts = make_parts(maps["pixels"][maps["n_gal"] > 0], int(maps["nside"])) + pix, val = quantity(maps, "n_eff", nside, footprint) + with S.figure_style(): + fig = plt.figure(figsize=(7.2, 7.2 * _aspect(parts) + 0.6)) + gs = fig.add_gridspec(1, 1) + axes = map_grid(fig, gs[0], parts) + norm = mpl.colors.Normalize(*_robust_limits(val, q=(0.5, 99.5))) + for i, (ax, part) in enumerate(zip(axes, parts)): + im = draw_map(ax, part, pix, val, nside, SEQUENTIAL, norm, + scale_bar=i == 0) + _colorbar(fig, im, axes, r"$n_{\rm eff}$ [arcmin$^{-2}$]", False) + if stats: + axes[0].text(0.0, 1.0, stats, transform=axes[0].transAxes, ha="left", + va="top", fontsize=8.5, color=S.INK_MUTED, linespacing=1.5) + return S.save(fig, out, "full") + + +def _aspect(parts): + main = parts[0] + h = main.height + sum(p.height * main.width / max(p.width, main.width) + for p in parts[1:]) + return h / main.width + + +FIELD_SETS = { + "galaxies": [ + ("sigma_e", 256, r"$\sigma_e$ per component", False), + ("mean_w", 256, r"$\langle w \rangle$", False), + ("e1", 128, r"$\langle e_1 \rangle$", True), + ("e2", 128, r"$\langle e_2 \rangle$", True), + ], + "psf": [ + ("fwhm_psf", 256, r"PSF FWHM at galaxies [arcsec]", False), + ("dT_over_T", 128, r"stars: $\langle (T_\star - T_{\rm PSF})/T_\star \rangle$", True), + ("de1", 128, r"stars: $\langle e_{1,\star} - e_{1,{\rm PSF}} \rangle$", True), + ("de2", 128, r"stars: $\langle e_{2,\star} - e_{2,{\rm PSF}} \rangle$", True), + ], +} + + +def field_maps(maps, out, which, footprint=None): + """2 x 2 small multiples of map quantities over the main footprint part. + + Diverging quantities use a symmetric range about zero; all ranges are + robust (1st-99th percentile) so single pixels do not set the scale. + """ + gal_pix = maps["pixels"][maps["n_gal"] > 0] + parts = make_parts(gal_pix, int(maps["nside"])) + with S.figure_style(): + fig = plt.figure(figsize=(7.2, 7.2 * _aspect(parts) + 1.0)) + gs = fig.add_gridspec(2, 2, hspace=0.08, wspace=0.04) + for cell, (name, nside, label, symmetric) in zip(gs, FIELD_SETS[which]): + pix, val = quantity(maps, name, nside, footprint) + # Restrict star-based maps to the galaxy footprint. + inside = np.isin(pix, np.unique(gal_pix // (int(maps["nside"]) // nside) ** 2)) + pix, val = pix[inside], val[inside] + lo, hi = _robust_limits(val, symmetric) + norm = (TwoSlopeNorm(0, lo, hi) if symmetric + else mpl.colors.Normalize(lo, hi)) + axes = map_grid(fig, cell, parts) + for ax, part in zip(axes, parts): + im = draw_map(ax, part, pix, val, nside, + DIVERGING if symmetric else SEQUENTIAL, norm, + labels=False) + _colorbar(fig, im, axes, label, symmetric) + return S.save(fig, out, "full") diff --git a/src/sp_validation/dashboard/style.py b/src/sp_validation/dashboard/style.py new file mode 100644 index 000000000..0a622ea75 --- /dev/null +++ b/src/sp_validation/dashboard/style.py @@ -0,0 +1,244 @@ +"""Shared look of the dashboard figures. + +Typography and colour semantics follow the UNIONS Paper II (B-modes) figures: +Computer Modern via mathtext (no TeX dependency), one hue per estimator +family in the B-mode data vectors, grey shading over excluded scales, a boxed +in-panel annotation naming the statistic, and the threshold-anchored PTE +colormap. Surfaces and ink come from the production page's parchment +palette so the PNGs sit on its cards without a white slab. + +Sizes are in inches at print-like font sizes; :func:`save` fixes the tight +saved width, and the resolution is chosen per size class so a figure stays +crisp at the width the page shows it. +""" + +from contextlib import contextmanager + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.colors import BoundaryNorm, ListedColormap + +# Page palette (production/status.html) +RAG = "#FBF6E9" # card surface; figure background +INK = "#1B1611" +INK_MUTED = "#7A6F5C" +RULE = "#C5B89E" +CINNABAR = "#BC4538" + +# Paper II mode / family hues (Reproductions UNIONS/pure_eb plotting_utils) +COLOR_TOTAL = INK +COLOR_E = "#2a78d6" +COLOR_B = "crimson" +COLOR_AMB = "#9a9a9a" +COLOR_XI_B_MINUS = "#e8792b" +COLOR_COSEBIS = "#7b3fa0" +COLOR_COSEBIS_ALL = "#b48cce" +COLOR_CL_BB = "#008080" +COLOR_CL_EB = "#4d4d4d" + +# Catalogue versions, only in version-comparison figures (Paper II Figs 7-9) +VERSION_MARKERS = ["o", "s", "D", "^"] + +# Neutral series inside one figure where no mode semantics apply +NEUTRAL = INK +NEUTRAL_LIGHT = "#a89f8c" + +DATA_VECTOR_MARK = { + "ls": "none", + "markersize": 3.4, + "markeredgewidth": 0.8, + "elinewidth": 0.75, + "capsize": 1.6, + "capthick": 0.75, +} +EXCLUDED_SHADE = {"color": INK, "alpha": 0.07, "lw": 0, "zorder": 0} + +# Width classes (inches) and their resolutions: about 1600 px for the full +# column, about 2x the displayed size for the row of three and phones. +WIDTH = {"full": 7.2, "half": 3.6, "third": 3.4} +DPI = {"full": 220, "half": 220, "third": 220} + +RC = { + "figure.facecolor": RAG, + "axes.facecolor": RAG, + "savefig.facecolor": RAG, + "text.color": INK, + "axes.edgecolor": INK, + "axes.labelcolor": INK, + "xtick.color": INK, + "ytick.color": INK, + "font.family": "serif", + "font.serif": ["cmr10"], + "mathtext.fontset": "cm", + "axes.formatter.use_mathtext": True, + "axes.unicode_minus": True, + "font.size": 10, + "axes.titlesize": 10, + "axes.labelsize": 10, + "xtick.labelsize": 9, + "ytick.labelsize": 9, + "legend.fontsize": 8.5, + "legend.frameon": False, + "axes.linewidth": 0.8, + "xtick.major.width": 0.7, + "ytick.major.width": 0.7, + "xtick.minor.width": 0.5, + "ytick.minor.width": 0.5, + "xtick.major.size": 3.5, + "ytick.major.size": 3.5, + "xtick.minor.size": 2, + "ytick.minor.size": 2, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "lines.linewidth": 1.2, + "figure.constrained_layout.use": True, + "savefig.pad_inches": 0.04, +} + + +@contextmanager +def figure_style(): + """Apply the dashboard rcParams for the duration of a render.""" + with mpl.rc_context(RC): + yield + + +def save(fig, path, size="full", width=None): + """Save ``fig`` with a tight bounding box exactly the class width wide. + + Scales the canvas (keeping its aspect) until the cropped width equals the + target, so font sizes in the style are the sizes on the page at that width + (the Paper II ``savefig_at_width`` recipe). + """ + width = width or WIDTH[size] + pad = mpl.rcParams["savefig.pad_inches"] + for _ in range(10): + fig.canvas.draw() + saved = fig.get_tightbbox(fig.canvas.get_renderer()).width + 2 * pad + if abs(saved / width - 1.0) < 1e-3: + break + fig.set_size_inches(fig.get_size_inches() * (width / saved)) + fig.savefig(path, dpi=DPI.get(size, 220), bbox_inches="tight") + plt.close(fig) + return path + + +def label_box(ax, text, loc="upper left", fontsize=None): + """Boxed in-panel annotation naming the statistic (Paper II convention).""" + x, ha = (0.04, "left") if "left" in loc else (0.96, "right") + y, va = (0.95, "top") if "upper" in loc else (0.05, "bottom") + ax.text( + x, + y, + text, + transform=ax.transAxes, + ha=ha, + va=va, + fontsize=fontsize or mpl.rcParams["font.size"], + zorder=10, + bbox=dict( + boxstyle="round,pad=0.28", + facecolor=RAG, + edgecolor=INK, + linewidth=0.7, + ), + ) + + +def note(ax, text, loc="lower right", color=INK_MUTED, fontsize=8): + """Small unboxed annotation, e.g. a PTE, in muted ink.""" + x, ha = (0.04, "left") if "left" in loc else (0.96, "right") + y, va = (0.95, "top") if "upper" in loc else (0.05, "bottom") + ax.text( + x, y, text, transform=ax.transAxes, ha=ha, va=va, color=color, + fontsize=fontsize, zorder=10, + ) + + +def blind_stamp(fig, blind, where="top"): + """Mark a figure that shows blinded data vectors, outside the axes.""" + y, va = (1.0, "top") if where == "top" else (0.0, "bottom") + x, ha = (1.0, "right") if where == "top" else (0.0, "left") + fig.text( + x, y, f"BLINDED · {blind}", ha=ha, va=va, fontsize=6.5, + family="monospace", color=CINNABAR, + ) + + +def zero_line(ax): + ax.axhline(0, color=INK, ls=(0, (4, 3)), lw=0.7, alpha=0.6, zorder=1) + + +def shade_outside(ax, lo, hi): + """Grey-shade the parts of the x range outside ``[lo, hi]``.""" + x0, x1 = ax.get_xlim() + if lo is not None and lo > x0: + ax.axvspan(x0, lo, **EXCLUDED_SHADE) + if hi is not None and hi < x1: + ax.axvspan(hi, x1, **EXCLUDED_SHADE) + ax.set_xlim(x0, x1) + + +def theta_axis(ax, lo=0.9, hi=280): + ax.set_xscale("log") + ax.set_xlim(lo, hi) + ax.xaxis.set_major_formatter( + mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$" if v >= 1 else "") + ) + + +def fmt_pte(value): + """PTE to two significant figures, in mathtext.""" + if value is None or not np.isfinite(value): + return "n/a" + if value < 1e-3: + return rf"{value:.1e}".replace("e-0", r"\times10^{-") + "}" + return f"{value:#.2g}".rstrip(".") + + +# Threshold-anchored PTE colormap (Paper II): solid poles below the threshold +# and above one minus it, a vlag gradient between. +PTE_THRESHOLD = 0.05 +_PTE_LEVELS = 512 + + +def pte_cmap(threshold=PTE_THRESHOLD): + import seaborn as sns + + vlag = sns.color_palette("vlag", as_cmap=True) + colors = ( + [vlag(0.0)] + + [vlag(v) for v in np.linspace(0.15, 0.85, _PTE_LEVELS)] + + [vlag(1.0)] + ) + cmap = ListedColormap(colors, name="pte_threshold") + cmap.set_bad(color="#e4dccb") + return cmap + + +def pte_norm(threshold=PTE_THRESHOLD): + bounds = np.concatenate( + ([0.0], np.linspace(threshold, 1 - threshold, _PTE_LEVELS + 1), [1.0]) + ) + return BoundaryNorm(bounds, ncolors=_PTE_LEVELS + 2, clip=True) + + +def pte_colorbar(fig, mappable, ax, threshold=PTE_THRESHOLD, **kwargs): + cbar = fig.colorbar(mappable, ax=ax, spacing="proportional", **kwargs) + ticks = [threshold, 0.5, 1 - threshold] + cbar.set_ticks(ticks) + cbar.set_ticklabels([f"${t:g}$" for t in ticks]) + cbar.minorticks_off() + cbar.outline.set_linewidth(0.6) + cbar.solids.set_edgecolor("face") + cbar.set_label("PTE") + return cbar + + +def correlation_cmap(): + import seaborn as sns + + return sns.color_palette("icefire", as_cmap=True) diff --git a/src/sp_validation/dashboard/versions.py b/src/sp_validation/dashboard/versions.py new file mode 100644 index 000000000..94e9a22b4 --- /dev/null +++ b/src/sp_validation/dashboard/versions.py @@ -0,0 +1,146 @@ +"""Catalogue-version comparisons (Paper II Figs. 7-9), drawn only when a run +carries two or more versions. + +Each figure overlays the versions with jittered markers in the version +palette and, per bin, a box spanning the versions' values with a bar at the +reference (first) version. ``datasets`` is a list of ``(label, products)`` +with the reference first; products are the dicts of +:mod:`sp_validation.dashboard.products`. +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import Rectangle + +from sp_validation.b_modes import bins_from_scale_cut + +from . import style as S +from .data_vectors import XI_SCALE, _cl_cut_mask + + +def version_colors(n): + """Paper II version palette: ``seaborn.husl_palette(n, l=0.4)``.""" + import seaborn as sns + + return sns.husl_palette(n, l=0.4) + + +def _boxes(ax, x_left, x_right, values, ref): + """Box spanning ``values`` (versions x bins) per bin, bar at ``ref``.""" + lo, hi = values.min(0), values.max(0) + for xl, xr, a, b, r in zip(x_left, x_right, lo, hi, ref): + ax.add_patch(Rectangle((xl, a), xr - xl, b - a, fill=False, + edgecolor=S.INK, lw=0.8, alpha=0.75, zorder=1)) + ax.hlines(r, xl, xr, color=S.INK, lw=1.3, zorder=1) + + +def _series(ax, x, y, yerr, i, color, label=None): + ax.errorbar(x, y, yerr=yerr, marker=S.VERSION_MARKERS[i % 4], color=color, + markerfacecolor="none", markeredgecolor=color, label=label, + zorder=2, **S.DATA_VECTOR_MARK) + + +def _log_offsets(n, spread=0.05): + return np.exp(spread * (np.arange(n) - (n - 1) / 2)) + + +def pure_eb(datasets, out, scale_cut): + """Total θξ± (top) and ξ±^B/σ (bottom) across versions.""" + n = len(datasets) + colors = version_colors(n) + off = _log_offsets(n) + pad = np.exp(0.05 * (n - 1) / 2 + 0.04) + ref = datasets[0][1] + theta = ref["theta"] + start, stop = bins_from_scale_cut(ref["left_edges"], ref["right_edges"], scale_cut) + shade = (ref["left_edges"][start], ref["right_edges"][stop - 1]) + with S.figure_style(): + fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.0), sharex=True, + height_ratios=(3, 2)) + for col, xi in enumerate(("xip", "xim")): + top = np.array([theta * d[xi] / XI_SCALE for _, d in datasets]) + bot = np.array([d[f"{xi}_B"] / d[f"sigma_{xi}_B"] for _, d in datasets]) + _boxes(axes[0, col], theta / pad, theta * pad, top, top[0]) + _boxes(axes[1, col], theta / pad, theta * pad, bot, bot[0]) + for i, (label, d) in enumerate(datasets): + _series(axes[0, col], theta * off[i], top[i], + theta * d[f"sigma_{xi}"] / XI_SCALE, i, colors[i], + label if col == 0 else None) + _series(axes[1, col], theta * off[i], bot[i], np.ones_like(theta), + i, colors[i]) + sign = "+" if xi == "xip" else "-" + S.label_box(axes[0, col], rf"$\xi_{sign}$") + S.zero_line(axes[1, col]) + for ax in axes[:, col]: + S.theta_axis(ax) + S.shade_outside(ax, *shade) + axes[1, col].set_xticks([1, 10, 100]) + axes[1, col].set_xticklabels(["$1$", "$10$", "$100$"]) + axes[1, col].set_xlabel(r"$\theta$ [arcmin]") + axes[0, 0].set_ylabel(r"$\theta\,\xi \times 10^{4}$") + axes[1, 0].set_ylabel(r"$\xi^B / \sigma$") + for row in range(2): + axes[row, 1].sharey(axes[row, 0]) + axes[row, 1].tick_params(labelleft=False) + fig.legend(loc="outside upper center", ncol=min(n, 4)) + return S.save(fig, out, "full") + + +def cosebis(datasets, out, full_cut=(1.0, 250.0)): + """B_n/σ_n across versions, all scales (top) and fiducial cut (bottom).""" + n = len(datasets) + colors = version_colors(n) + offsets = 0.13 * (np.arange(n) - (n - 1) / 2) + half = 0.13 * (n - 1) / 2 + 0.12 + fid = datasets[0][1]["fiducial"] + nmodes = datasets[0][1]["nmodes"] + modes = np.arange(1, nmodes + 1) + with S.figure_style(): + fig, axes = plt.subplots(2, 1, figsize=(7.2, 3.6), sharex=True, sharey=True) + for ax, cut in zip(axes, (full_cut, fid)): + vals = np.array([d["cuts"][cut]["Bn"] / d["cuts"][cut]["sigma_B"] + for _, d in datasets]) + _boxes(ax, modes - half, modes + half, vals, vals[0]) + for i, (label, _) in enumerate(datasets): + _series(ax, modes + offsets[i], vals[i], np.ones(nmodes), i, + colors[i], label if cut == full_cut else None) + S.zero_line(ax) + ax.set_ylabel(r"$B_n / \sigma_n$") + ax.set_title(rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", loc="right", + fontsize=8.5, color=S.INK_MUTED, pad=3) + axes[-1].set_xlim(0.4, nmodes + 0.6) + axes[-1].set_xticks(modes) + axes[-1].set_xlabel(r"COSEBI mode $n$") + fig.legend(loc="outside upper center", ncol=min(n, 4)) + return S.save(fig, out, "full") + + +def pseudo_cl(datasets, out, ell_cut): + """C_ℓ^BB/σ and C_ℓ^EB/σ across versions.""" + n = len(datasets) + colors = version_colors(n) + ref = datasets[0][1] + ell = ref["ell"] + width = ref["ell_hi"] - ref["ell_lo"] + 1 + offsets = 0.16 * (np.arange(n) - (n - 1) / 2) + half = (0.16 * (n - 1) / 2 + 0.1) * width + mask = _cl_cut_mask(ref, ell_cut) + with S.figure_style(): + fig, axes = plt.subplots(2, 1, figsize=(7.2, 3.6), sharex=True, sharey=True) + for ax, key in zip(axes, ("BB", "EB")): + vals = np.array([d[key] / d[f"sigma_{key}"] for _, d in datasets]) + _boxes(ax, ell - half, ell + half, vals, vals[0]) + for i, (label, _) in enumerate(datasets): + _series(ax, ell + offsets[i] * width, vals[i], np.ones_like(ell), i, + colors[i], label if key == "BB" else None) + S.zero_line(ax) + ax.set_xscale("function", functions=(np.sqrt, np.square)) + ax.set_xlim(ell.min() * 0.85, ell.max() * 1.04) + S.shade_outside(ax, ref["ell_lo"][mask][0], ref["ell_hi"][mask][-1]) + ax.set_ylabel(rf"$C_\ell^{{{key}}} / \sigma$") + S.label_box(ax, rf"$C_\ell^{{{key}}}$") + axes[-1].set_xticks([100, 400, 900, 1600]) + axes[-1].set_xticklabels(["$100$", "$400$", "$900$", "$1600$"]) + axes[-1].set_xlabel(r"$\ell$") + fig.legend(loc="outside upper center", ncol=min(n, 4)) + return S.save(fig, out, "full") From 7b36ca3735fbebe02adee64446992770b65f7f42 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Mon, 26 Oct 2026 18:06:12 -0400 Subject: [PATCH 19/53] feat(dashboard): render CLI, figures manifest, weight and map figures, tests render.py writes every figure plus figures.json (sections, captions, size classes, headline PTEs) from cached products; version comparisons appear only for runs with two or more versions. Weight figure shows the 20x20 SNR x size-ratio cell grid behind the 400 distinct weights; field maps gain phone layouts and drop sparsely filled edge pixels. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011rD7z3XSA9PmjQyPpNcG49 --- src/sp_validation/dashboard/data_vectors.py | 8 +- src/sp_validation/dashboard/products.py | 21 +- src/sp_validation/dashboard/psf.py | 2 +- src/sp_validation/dashboard/render.py | 247 ++++++++++++++++++ src/sp_validation/dashboard/sky.py | 41 +-- src/sp_validation/dashboard/style.py | 4 +- src/sp_validation/dashboard/weights.py | 102 ++++++++ .../tests/test_dashboard_figures.py | 58 ++++ 8 files changed, 450 insertions(+), 33 deletions(-) create mode 100644 src/sp_validation/dashboard/render.py create mode 100644 src/sp_validation/dashboard/weights.py create mode 100644 src/sp_validation/tests/test_dashboard_figures.py diff --git a/src/sp_validation/dashboard/data_vectors.py b/src/sp_validation/dashboard/data_vectors.py index 6b964a76a..cafbf926f 100644 --- a/src/sp_validation/dashboard/data_vectors.py +++ b/src/sp_validation/dashboard/data_vectors.py @@ -19,7 +19,7 @@ from . import style as S XI_SCALE = 1e-4 -THIRD_HEIGHT = 3.55 +THIRD_HEIGHT = 3.3 def _theta_ticks(ax): @@ -86,7 +86,7 @@ def pure_eb(pe, out, scale_cut, ptes, blind=None): ("_amb", "v", S.COLOR_AMB, 0.55, True, "amb."), ] with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True) + fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True) theta = pe["theta"] start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], scale_cut) @@ -155,7 +155,7 @@ def cosebis(co, out, full_cut=(1.0, 250.0)): ] n = np.arange(1, co["nmodes"] + 1) with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True, + fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True) extent = 0 for ax, (cut, marker, filled, color) in zip(axes, rows): @@ -209,7 +209,7 @@ def pseudo_cl(cl, out, ell_cut): ] ell = cl["ell"] with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.4, THIRD_HEIGHT), sharex=True, + fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True) extent = 0 for ax, (key, marker, filled, color, label) in zip(axes, rows): diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index 248fab08b..a4247e119 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -11,6 +11,7 @@ import glob import json +import re from dataclasses import dataclass from pathlib import Path @@ -52,23 +53,29 @@ def assembled(self): @property def pure_eb(self): - return _one(self.cv / f"{self.version}_eb_*_data.npz") + return _one(self.cv / f"{self.version}_eb_minsep=*_data.npz") @property def cosebis(self): - return _one(self.cv / f"{self.version}_cosebis_*_data.npz") + return _one(self.cv / f"{self.version}_cosebis_minsep=*_data.npz") @property def pseudo_cl_cov(self): - return _one(self.cv / f"pseudo_cl_cov_{self.version}_*.fits") + prefix = f"pseudo_cl_cov_{self.version}_" + binning = re.compile(r"(powspace|logspace|linear)_nbins=\d+\.fits") + matches = [p for p in self.cv.glob(prefix + "*.fits") + if binning.fullmatch(p.name[len(prefix):])] + if len(matches) != 1: + raise FileNotFoundError(f"expected one {prefix}.fits, found {matches}") + return matches[0] @property def psf_fit(self): - return _one(self.cv / f"psf_fit_{self.version}_*.npz") + return _one(self.cv / f"psf_fit_{self.version}_minsep=*.npz") @property def tau_cov(self): - return _one(self.cv / "rho_tau_stats" / f"cov_tau_{self.version}_*.npy") + return _one(self.cv / "rho_tau_stats" / f"cov_tau_{self.version}_tomo_bin_all_*.npy") @property def leakage(self): @@ -84,8 +91,8 @@ def xi_covariance(self): return _maybe( Path(self.run_dir) / "cosmo_inference/data/covariance" - / f"covariance_{self.version}_ng_*_masked" - / f"covariance_{self.version}_ng_*_masked.dat" + / f"covariance_{self.version}_ng_minsep=*_masked" + / f"covariance_{self.version}_ng_minsep=*_masked.dat" ) @property diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py index a766e7f7d..a834f990c 100644 --- a/src/sp_validation/dashboard/psf.py +++ b/src/sp_validation/dashboard/psf.py @@ -242,7 +242,7 @@ def alpha_estimates(leakage, rt, out): prev = None for yi, g in zip(y, groups): if g != prev: - ax.text(1.0, yi + 0.42, g, transform=ax.get_yaxis_transform(), + ax.text(0.98, yi + 0.42, g, transform=ax.get_yaxis_transform(), ha="right", va="bottom", fontsize=7.5, color=S.INK_MUTED) prev = g ax.set_ylim(y.min() - 0.7, y.max() + 0.9) diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py new file mode 100644 index 000000000..656a33fc1 --- /dev/null +++ b/src/sp_validation/dashboard/render.py @@ -0,0 +1,247 @@ +"""Render the dashboard figure set for a cosmo_val run. + +Reads only cached products (see :mod:`sp_validation.dashboard.products`) and +writes the PNGs plus ``figures.json``, the manifest the page generator lays +out: one entry per figure with its section, caption and size class, and the +headline B-mode PTEs. A run with two or more versions also gets the version +comparisons; the first version is the reference for every other figure. + +Usage (inside the container):: + + python -m sp_validation.dashboard.render --run-dir RUN --version SP_v2.0 \\ + --maps-dir PRODUCTS --out RUN/dashboard-figures +""" + +import argparse +import json +from pathlib import Path + +import numpy as np +import yaml + +from . import covariance as CV +from . import data_vectors as DV +from . import products as P +from . import psf as PSF +from . import scale_cuts as SC +from . import sky +from . import versions as V +from . import weights as W + +SECTIONS = [ + ("xi", "Shear correlation functions"), + ("bmodes", "B-mode null tests"), + ("scale_cuts", "PTEs across scale cuts"), + ("covariance", "Covariances"), + ("psf", "PSF leakage"), + ("weights", "Weights"), + ("sky", "Footprint and field maps"), + ("versions", "Catalogue versions"), +] + + +def run_config(run_dir): + """Scale cuts from the run's recorded configuration, when present.""" + prov = Path(run_dir) / "provenance.json" + cuts = {"theta": (12.0, 83.0), "ell": (300, 1600), "cosebis_full": (1.0, 250.0)} + if prov.exists(): + cfg = yaml.safe_load(json.loads(prov.read_text())["config"]) + cuts["theta"] = tuple(map(float, cfg["cosmo_val"]["fiducial_scale_cut"])) + cuts["ell"] = (int(cfg["cl"]["fiducial_ell_min"]), + int(cfg["cl"]["fiducial_ell_max"])) + cuts["cosebis_full"] = (float(cfg["cosebis"]["theta_min"]), + float(cfg["cosebis"]["theta_max"])) + return cuts + + +class Figures: + """Collects figure entries while rendering.""" + + def __init__(self, out): + self.out = Path(out) + self.out.mkdir(parents=True, exist_ok=True) + self.entries = [] + + def path(self, name): + return self.out / f"{name}.png" + + def add(self, fid, section, caption, size, narrow=False, alt=None): + entry = {"id": fid, "section": section, "file": f"{fid}.png", + "caption": caption, "size": size, "alt": alt or caption} + if narrow: + entry["narrow"] = f"{fid}_narrow.png" + self.entries.append(entry) + + +def render(run_dir, versions, out, maps_dir=None): + cuts = run_config(run_dir) + ref = P.RunProducts(run_dir, versions[0], maps_dir) + blind = P.blind_of(ref.assembled) + figs = Figures(out) + xi = P.load_xi(ref.assembled, blind) + pe = P.load_pure_eb(ref.pure_eb, blind) + co = P.load_cosebis(ref.cosebis, blind) + cl = P.load_pseudo_cl(ref.assembled, ref.pseudo_cl_cov, blind) + summary = P.load_json(ref.summary).get(versions[0], {}) if ref.summary.exists() else {} + theta_cut, ell_cut = cuts["theta"], cuts["ell"] + tcut = rf"{theta_cut[0]:g}′–{theta_cut[1]:g}′" + + # 1. hero + DV.xi_pm(xi, figs.path("xi_pm"), blind=blind) + DV.xi_pm(xi, figs.path("xi_pm_narrow"), layout="narrow", blind=blind) + figs.add("xi_pm", "xi", + "Blinded ξ± on the 20-bin reporting grid; error bars from the analytic covariance.", + "full", narrow=True) + + # 2. B-mode data vectors + ptes = {k: summary.get(k) for k in ("xip_B", "xim_B", "combined")} + DV.pure_eb(pe, figs.path("bmode_pure_eb"), theta_cut, ptes, blind=blind) + figs.add("bmode_pure_eb", "bmodes", + f"Pure-mode decomposition of ξ±; B (crimson) is the null test. Shaded: outside {tcut}.", + "third") + DV.cosebis(co, figs.path("bmode_cosebis"), cuts["cosebis_full"]) + figs.add("bmode_cosebis", "bmodes", + "COSEBIs B-modes in units of their uncertainty, at the fiducial cut and over all scales.", + "third") + DV.pseudo_cl(cl, figs.path("bmode_cl"), ell_cut) + figs.add("bmode_cl", "bmodes", + f"Pseudo-Cℓ BB and EB in units of their uncertainty. Shaded: outside {ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}.", + "third") + fid_co = co["cuts"][co["fiducial"]]["pte_B"] + headline = [ + {"label": "ξ± pure B, joint", "symbol": "xi", "pte": ptes.get("combined"), + "range": tcut}, + {"label": "COSEBIs B", "symbol": "cosebis", "pte": fid_co, + "range": rf"{co['fiducial'][0]:g}′–{co['fiducial'][1]:g}′"}, + {"label": "pseudo-Cℓ BB", "symbol": "cl", "pte": DV.cl_pte(cl, "BB", ell_cut), + "range": f"{ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}"}, + ] + + # 3. scale cuts + SC.config_space(pe, figs.path("ptes_config"), theta_cut) + SC.config_space(pe, figs.path("ptes_config_narrow"), theta_cut, layout="narrow") + figs.add("ptes_config", "scale_cuts", + "Configuration-space B-mode PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", + "full", narrow=True) + SC.harmonic_and_cosebis(cl, co, figs.path("ptes_harmonic"), ell_cut) + SC.harmonic_and_cosebis(cl, co, figs.path("ptes_harmonic_narrow"), ell_cut, + layout="narrow") + figs.add("ptes_harmonic", "scale_cuts", + "Cℓ BB PTE for every band range, and the COSEBIs B PTE for each cached angular cut.", + "full", narrow=True) + + # 4. covariances + n = len(xi["theta"]) + CV.correlation(xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), + rf"$\xi_\pm$ reporting grid") + figs.add("cov_xi", "covariance", "ξ± reporting-grid covariance (analytic, masked).", "half") + CV.correlation(pe["cov"], CV.pure_eb_blocks(len(pe["theta"])), + figs.path("cov_pure_eb"), "pure E/B modes") + figs.add("cov_pure_eb", "covariance", "Pure E, B and ambiguous modes of ξ±.", "half") + fc = co["fiducial"] + CV.correlation(co["cuts"][fc]["cov"], CV.cosebis_blocks(co["nmodes"]), + figs.path("cov_cosebis"), + rf"COSEBIs, ${fc[0]:g}'\!-\!{fc[1]:g}'$") + figs.add("cov_cosebis", "covariance", "COSEBIs E and B modes at the fiducial cut.", "half") + CV.correlation(CV.cl_matrix(cl["cov"]), CV.cl_blocks(len(cl["ell"])), + figs.path("cov_cl"), r"pseudo-$C_\ell$") + figs.add("cov_cl", "covariance", "Pseudo-Cℓ EE, EB and BB (Gaussian, NaMaster).", "half") + + # 5. PSF leakage + rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit) + PSF.tau_fit(rt, figs.path("psf_tau")) + figs.add("psf_tau", "psf", + "Galaxy–PSF correlations τ₀,₂,₅ with the posterior band of the α, β, η leakage model.", + "full") + PSF.posterior(rt, figs.path("psf_posterior")) + figs.add("psf_posterior", "psf", "Posterior of the leakage parameters (68 and 95%).", "half") + leakage = P.load_json(ref.leakage).get(versions[0], {}).get("tomo_bin_all", {}) + PSF.alpha_estimates(leakage, rt, figs.path("psf_alpha")) + figs.add("psf_alpha", "psf", + "Every estimate of the leakage coefficient: object-wise, scale-dependent and ρ/τ fit.", + "half") + PSF.rho_stats(rt, figs.path("psf_rho")) + figs.add("psf_rho", "psf", "ρ statistics of the PSF model and its residuals.", "full") + PSF.xi_sys_ratio(rt, xi, figs.path("psf_xi_sys")) + figs.add("psf_xi_sys", "psf", + "Propagated PSF systematic relative to the blinded ξ±; dotted lines at ±10%.", + "full") + + # 6. weights + if ref.weight_cells is not None: + cells = dict(np.load(ref.weight_cells, allow_pickle=False)) + W.weights(cells, figs.path("weights")) + s = W.summary(cells) + figs.add("weights", "weights", + f"Each galaxy carries the weight R²/σₑ² of its SNR × size cell, so the " + f"{s['n_values']} distinct values are the {s['n_occupied']} occupied cells; " + f"histograms of w are combs, not clipping.", + "full") + + # 7. sky + if ref.field_maps is not None: + maps = dict(np.load(ref.field_maps, allow_pickle=False)) + footprint = dict(np.load(ref.cv / f"footprint_{versions[0]}.npz")) + sky.footprint_map(maps, footprint, figs.path("sky_footprint")) + figs.add("sky_footprint", "sky", + "Effective source density in a zenithal equal-area projection centred on the patch.", + "full") + for which in ("galaxies", "psf"): + sky.field_maps(maps, figs.path(f"sky_{which}"), which, footprint) + sky.field_maps(maps, figs.path(f"sky_{which}_narrow"), which, footprint, + layout="narrow") + figs.add("sky_galaxies", "sky", + "Galaxy fields: ellipticity dispersion, mean weight and weighted mean e₁, e₂.", + "full", narrow=True) + dt = sky.median_quantity(maps, "dT_over_T", 128) + figs.add("sky_psf", "sky", + "PSF fields: model FWHM at galaxies, and star-minus-model size and ellipticity " + f"residuals (median size residual {100 * dt:+.2f}%).", + "full", narrow=True) + + # 8. versions + if len(versions) > 1: + runs = [P.RunProducts(run_dir, v, maps_dir) for v in versions] + blinds = [P.blind_of(r.assembled) for r in runs] + V.pure_eb([(v, P.load_pure_eb(r.pure_eb, b)) + for v, r, b in zip(versions, runs, blinds)], + figs.path("versions_pure_eb"), theta_cut) + figs.add("versions_pure_eb", "versions", + "ξ± and pure-B ξ±/σ across versions; boxes span the versions, bars mark the reference.", + "full") + V.cosebis([(v, P.load_cosebis(r.cosebis, b)) + for v, r, b in zip(versions, runs, blinds)], + figs.path("versions_cosebis"), cuts["cosebis_full"]) + figs.add("versions_cosebis", "versions", "COSEBIs B/σ across versions.", "full") + V.pseudo_cl([(v, P.load_pseudo_cl(r.assembled, r.pseudo_cl_cov, b)) + for v, r, b in zip(versions, runs, blinds)], + figs.path("versions_cl"), ell_cut) + figs.add("versions_cl", "versions", "Cℓ BB/σ and EB/σ across versions.", "full") + + manifest = { + "schema_version": 1, + "versions": list(versions), + "blind": blind, + "sections": [{"id": s, "title": t} for s, t in SECTIONS], + "headline_ptes": headline, + "figures": figs.entries, + } + (figs.out / "figures.json").write_text(json.dumps(manifest, indent=2, + ensure_ascii=False) + "\n") + return manifest + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--run-dir", required=True) + parser.add_argument("--version", action="append", required=True, + help="catalogue version; repeat for a comparison run") + parser.add_argument("--maps-dir", help="directory of field_maps/weight_cells NPZs") + parser.add_argument("--out", required=True) + args = parser.parse_args(argv) + manifest = render(args.run_dir, args.version, args.out, args.maps_dir) + print(f"{len(manifest['figures'])} figures -> {args.out}") + + +if __name__ == "__main__": + main() diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py index ad6982fda..57d43a31d 100644 --- a/src/sp_validation/dashboard/sky.py +++ b/src/sp_validation/dashboard/sky.py @@ -250,15 +250,6 @@ def _edge_label(ax, x, y, box, text, side): if y[i] - y0 < 0.08 * (y1 - y0): ax.text(x[i], y0, text, ha="center", va="top", fontsize=7, color=S.INK_MUTED, clip_on=False) - return - i = np.flatnonzero(ok)[np.argmax(y[ok])] - placed = ax.__dict__.setdefault("_top_labels", []) - if y1 - y[i] < 0.08 * (y1 - y0) and all( - abs(x[i] - p) > 0.07 * (x1 - x0) for p in placed - ): - placed.append(x[i]) - ax.text(x[i], y1, text, ha="center", va="bottom", fontsize=7, - color=S.INK_MUTED, clip_on=False) def _scale_bar(ax, part, length=10): @@ -270,10 +261,10 @@ def _scale_bar(ax, part, length=10): def draw_map(ax, part, pixels, values, nside, cmap, norm, resolution=None, - labels=True, scale_bar=False): + labels=True, scale_bar=False, grid_color=S.RULE): resolution = resolution or max(hp.nside2resol(nside, arcmin=True) / 60 / 2, 0.03) img = raster(part, pixels, values, nside, resolution) - graticule(ax, part, label=labels) + graticule(ax, part, color=grid_color, label=labels) im = ax.imshow(img, origin="lower", extent=(*part.xlim, *part.ylim), cmap=cmap, norm=norm, interpolation="nearest", zorder=2, rasterized=True) @@ -313,10 +304,15 @@ def coverage(footprint, nside_out): return out, counts / ratio +def median_quantity(maps, name, nside_out, footprint=None): + """Median over pixels of one map quantity.""" + return float(np.median(quantity(maps, name, nside_out, footprint)[1])) + + def quantity(maps, name, nside_out, footprint=None): """Pixels and values of one named map quantity at ``nside_out``.""" nside = int(maps["nside"]) - keys = [k for k in maps if k.startswith(("n_", "sum_"))] + keys = [k for k in maps if k.startswith(("n_", "sum_")) and np.ndim(maps[k]) == 1] pix, sums = coarsen(maps["pixels"], {k: maps[k] for k in keys}, nside, nside_out) area = hp.nside2pixarea(nside_out, degrees=True) * 3600 if name == "n_eff": @@ -344,6 +340,9 @@ def quantity(maps, name, nside_out, footprint=None): keep = sums["n_star"] >= 5 else: raise KeyError(name) + # Drop sparsely filled pixels (footprint edges), whose means are noise. + n = sums["n_star" if name in ("de1", "de2", "dT_over_T") else "n_gal"] + keep &= n >= 0.3 * np.median(n[n > 0]) return pix[keep], values[keep] @@ -396,8 +395,8 @@ def _aspect(parts): "galaxies": [ ("sigma_e", 256, r"$\sigma_e$ per component", False), ("mean_w", 256, r"$\langle w \rangle$", False), - ("e1", 128, r"$\langle e_1 \rangle$", True), - ("e2", 128, r"$\langle e_2 \rangle$", True), + ("e1", 64, r"$\langle e_1 \rangle$", True), + ("e2", 64, r"$\langle e_2 \rangle$", True), ], "psf": [ ("fwhm_psf", 256, r"PSF FWHM at galaxies [arcsec]", False), @@ -408,7 +407,7 @@ def _aspect(parts): } -def field_maps(maps, out, which, footprint=None): +def field_maps(maps, out, which, footprint=None, layout="wide"): """2 x 2 small multiples of map quantities over the main footprint part. Diverging quantities use a symmetric range about zero; all ranges are @@ -417,8 +416,12 @@ def field_maps(maps, out, which, footprint=None): gal_pix = maps["pixels"][maps["n_gal"] > 0] parts = make_parts(gal_pix, int(maps["nside"])) with S.figure_style(): - fig = plt.figure(figsize=(7.2, 7.2 * _aspect(parts) + 1.0)) - gs = fig.add_gridspec(2, 2, hspace=0.08, wspace=0.04) + if layout == "wide": + fig = plt.figure(figsize=(7.2, 7.2 * _aspect(parts) + 1.0)) + gs = fig.add_gridspec(2, 2, hspace=0.08, wspace=0.04) + else: + fig = plt.figure(figsize=(3.6, 4 * (3.6 * _aspect(parts) + 0.6))) + gs = fig.add_gridspec(4, 1, hspace=0.06) for cell, (name, nside, label, symmetric) in zip(gs, FIELD_SETS[which]): pix, val = quantity(maps, name, nside, footprint) # Restrict star-based maps to the galaxy footprint. @@ -431,6 +434,6 @@ def field_maps(maps, out, which, footprint=None): for ax, part in zip(axes, parts): im = draw_map(ax, part, pix, val, nside, DIVERGING if symmetric else SEQUENTIAL, norm, - labels=False) + labels=False, grid_color="#ddd2bb") _colorbar(fig, im, axes, label, symmetric) - return S.save(fig, out, "full") + return S.save(fig, out, "full" if layout == "wide" else "half") diff --git a/src/sp_validation/dashboard/style.py b/src/sp_validation/dashboard/style.py index 0a622ea75..5b088a9c2 100644 --- a/src/sp_validation/dashboard/style.py +++ b/src/sp_validation/dashboard/style.py @@ -56,8 +56,8 @@ # Width classes (inches) and their resolutions: about 1600 px for the full # column, about 2x the displayed size for the row of three and phones. -WIDTH = {"full": 7.2, "half": 3.6, "third": 3.4} -DPI = {"full": 220, "half": 220, "third": 220} +WIDTH = {"full": 7.2, "half": 3.6, "third": 3.0} +DPI = {"full": 220, "half": 220, "third": 250} RC = { "figure.facecolor": RAG, diff --git a/src/sp_validation/dashboard/weights.py b/src/sp_validation/dashboard/weights.py new file mode 100644 index 000000000..cdc5b21e0 --- /dev/null +++ b/src/sp_validation/dashboard/weights.py @@ -0,0 +1,102 @@ +"""What the galaxy weights are. + +The shape weight is built per cell of a log-spaced SNR x size-ratio grid +(:func:`sp_validation.calibration.get_w_des`): every galaxy in a cell gets +that cell's R^2 / sigma_e^2. The weights therefore take at most one value per +occupied cell. The figure shows the weight over the grid, with each cell's +share of galaxies, and the resulting discrete distribution. +""" + +import json + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + +from . import style as S +from .sky import SEQUENTIAL + + +def summary(cells): + """Distinct values, grid size and whether every cell holds one weight.""" + count = cells["count"] + occupied = count > 0 + spread = np.nanmax(np.abs(cells["w_max"] - cells["w_min"])[occupied]) + return { + "n_values": int(len(cells["w_values"])), + "n_cells": int(count.size), + "n_occupied": int(occupied.sum()), + "max_within_cell_spread": float(spread), + "w_min": float(cells["w_values"].min()), + "w_max": float(cells["w_values"].max()), + "out_of_range": json.loads(str(cells["out_of_range"])), + } + + +def weights(cells, out): + """Weight on the SNR x size-ratio grid, and the distinct-weight spectrum.""" + snr, size = cells["snr_edges"], cells["size_ratio_edges"] + count = cells["count"].astype(float) + frac = count / count.sum() + w = np.where(count > 0, cells["w_min"], np.nan) + values, mult = cells["w_values"], cells["w_multiplicity"] + log_fmt = mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$") + with S.figure_style(): + fig, ax = plt.subplot_mosaic( + [["top", ".", "spec"], ["grid", "side", "spec"]], + figsize=(7.2, 3.5), width_ratios=(4, 0.7, 4.2), height_ratios=(0.8, 4), + ) + g = ax["grid"] + mesh = g.pcolormesh(snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, + linewidth=0.25) + g.set_xscale("log") + g.set_yscale("log") + g.set_xlim(snr[0], snr[-1]) + g.set_ylim(size[0], size[-1]) + g.set_xlabel("signal-to-noise") + g.set_ylabel(r"size ratio $T_{\rm gal} / T_{\rm PSF}$") + g.xaxis.set_major_formatter(log_fmt) + g.xaxis.set_minor_formatter(mpl.ticker.FuncFormatter( + lambda v, _: f"${v:g}$" if v in (30, 300) else "")) + g.yaxis.set_major_formatter(log_fmt) + g.yaxis.set_minor_formatter(mpl.ticker.FuncFormatter( + lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2) else "")) + g.tick_params(which="both", top=False, right=False) + cbar = fig.colorbar(mesh, ax=g, location="bottom", aspect=30, pad=0.02, + fraction=0.06) + cbar.set_label(r"cell weight $w = R^2 / \sigma_e^2$") + cbar.outline.set_linewidth(0.5) + + # Marginal shares of galaxies along each grid axis. + bar = dict(color=S.NEUTRAL_LIGHT, edgecolor=S.RAG, linewidth=0.3) + ax["top"].bar(snr[:-1], 100 * frac.sum(1), width=np.diff(snr), align="edge", + **bar) + ax["top"].set_xscale("log") + ax["top"].set_xlim(g.get_xlim()) + ax["side"].barh(size[:-1], 100 * frac.sum(0), height=np.diff(size), + align="edge", **bar) + ax["side"].set_yscale("log") + ax["side"].set_ylim(g.get_ylim()) + for name, axis in (("top", "x"), ("side", "y")): + m = ax[name] + m.tick_params(axis=axis, which="both", labelbottom=False, + labelleft=False, bottom=False, left=False, top=False, + right=False) + for side in ("top", "right"): + m.spines[side].set_visible(False) + m.tick_params(which="both", top=False, right=False, labelsize=7) + m.locator_params(axis="y" if name == "top" else "x", nbins=2) + ax["top"].set_ylabel("%", fontsize=8) + ax["side"].set_xlabel("%", fontsize=8) + + spec = ax["spec"] + share = mult / mult.sum() + spec.vlines(values, 0, share * 100, color=S.INK, lw=0.5, alpha=0.85) + spec.set_xlabel(r"weight $w$") + spec.set_ylabel("galaxies at this weight [%]") + spec.set_ylim(0, None) + spec.set_xlim(0, values.max() * 1.03) + spec.tick_params(which="both", top=False, right=False) + spec.set_title(f"{len(values)} distinct values, one per cell", loc="left", + fontsize=9) + return S.save(fig, out, "full") diff --git a/src/sp_validation/tests/test_dashboard_figures.py b/src/sp_validation/tests/test_dashboard_figures.py new file mode 100644 index 000000000..9e685f464 --- /dev/null +++ b/src/sp_validation/tests/test_dashboard_figures.py @@ -0,0 +1,58 @@ +"""Geometry and binning helpers behind the dashboard figures.""" + +import healpy as hp +import numpy as np +import pandas as pd +import pytest + +from sp_validation.dashboard.field_maps import weight_cell_index, weight_grid_edges +from sp_validation.dashboard.sky import Zenithal, coarsen, footprint_parts + + +def test_weight_cells_match_calibration_binning(): + """Cell indices reproduce cut_to_bins: right-closed, outside -> last cell.""" + snr_edges, _ = weight_grid_edges(20, (10, 500), (0.707, 3)) + rng = np.random.default_rng(1) + values = np.concatenate([rng.uniform(5, 700, 2000), snr_edges, [np.nan]]) + expected = pd.cut(values, snr_edges, labels=False) + expected = np.where(np.isnan(expected), 19, expected).astype(int) + np.testing.assert_array_equal(weight_cell_index(values.copy(), snr_edges), expected) + + +@pytest.mark.parametrize("rotation", [0.0, 37.0, -80.0]) +def test_zenithal_round_trip(rotation): + proj = Zenithal(229.0, 57.0, rotation) + rng = np.random.default_rng(2) + ra = rng.uniform(150, 290, 500) + dec = rng.uniform(25, 85, 500) + x, y = proj.forward(ra, dec) + ra2, dec2, valid = proj.inverse(x, y) + assert valid.all() + np.testing.assert_allclose(dec2, dec, atol=1e-8) + np.testing.assert_allclose(np.mod(ra2 - ra + 180, 360) - 180, 0, atol=1e-8) + + +def test_zenithal_ra_increases_to_the_left(): + x, _ = Zenithal(200.0, 40.0).forward(np.array([199.0, 201.0]), np.array([40.0, 40.0])) + assert x[1] < x[0] + + +def test_footprint_parts_separates_patch_and_strip(): + nside = 256 + patch = hp.query_disc(nside, hp.ang2vec(220, 55, lonlat=True), np.radians(10), + nest=True) + ra = np.mod(np.linspace(-30, 60, 4000), 360) + strip = np.unique(np.concatenate([ + hp.ang2pix(nside, ra, np.full_like(ra, d), lonlat=True, nest=True) + for d in np.arange(30, 34, 0.2) + ])) + parts = footprint_parts(np.concatenate([patch, strip]), nside) + assert len(parts) == 2 + assert parts[0].sum() == len(patch) + + +def test_coarsen_sums_children(): + pixels = np.arange(64) + out, sums = coarsen(pixels, {"n": np.ones(64)}, nside=4, nside_out=2) + np.testing.assert_array_equal(out, np.arange(16)) + np.testing.assert_array_equal(sums["n"], np.full(16, 4)) From 29839b99abdc16ffa43df73129c03bd8e3cbc395 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Mon, 26 Oct 2026 18:07:29 -0400 Subject: [PATCH 20/53] style(dashboard): ruff format and lint fixes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011rD7z3XSA9PmjQyPpNcG49 --- src/sp_validation/dashboard/covariance.py | 16 +- src/sp_validation/dashboard/data_vectors.py | 85 +++-- src/sp_validation/dashboard/field_maps.py | 4 +- src/sp_validation/dashboard/products.py | 29 +- src/sp_validation/dashboard/psf.py | 156 ++++++--- src/sp_validation/dashboard/render.py | 310 +++++++++++++----- src/sp_validation/dashboard/scale_cuts.py | 90 +++-- src/sp_validation/dashboard/sky.py | 152 +++++++-- src/sp_validation/dashboard/style.py | 21 +- src/sp_validation/dashboard/versions.py | 92 ++++-- src/sp_validation/dashboard/weights.py | 54 ++- .../tests/test_dashboard_figures.py | 21 +- 12 files changed, 749 insertions(+), 281 deletions(-) diff --git a/src/sp_validation/dashboard/covariance.py b/src/sp_validation/dashboard/covariance.py index 6b8586366..36a088b3c 100644 --- a/src/sp_validation/dashboard/covariance.py +++ b/src/sp_validation/dashboard/covariance.py @@ -23,8 +23,14 @@ def correlation(cov, blocks, out, label): starts = np.concatenate(([0], np.cumsum(sizes))) with S.figure_style(): fig, ax = plt.subplots(figsize=(3.6, 3.25)) - image = ax.imshow(corr, cmap=S.correlation_cmap(), vmin=-1, vmax=1, - interpolation="nearest", origin="upper") + image = ax.imshow( + corr, + cmap=S.correlation_cmap(), + vmin=-1, + vmax=1, + interpolation="nearest", + origin="upper", + ) for edge in starts[1:-1]: ax.axhline(edge - 0.5, color=S.RAG, lw=0.5, alpha=0.8) ax.axvline(edge - 0.5, color=S.RAG, lw=0.5, alpha=0.8) @@ -48,11 +54,7 @@ def xi_blocks(n): def pure_eb_blocks(n): - return [ - (rf"$\xi_{s}^{{{m}}}$", n) - for m in ("E", "B", r"\rm amb") - for s in "+-" - ] + return [(rf"$\xi_{s}^{{{m}}}$", n) for m in ("E", "B", r"\rm amb") for s in "+-"] def cosebis_blocks(n): diff --git a/src/sp_validation/dashboard/data_vectors.py b/src/sp_validation/dashboard/data_vectors.py index cafbf926f..15153bdc9 100644 --- a/src/sp_validation/dashboard/data_vectors.py +++ b/src/sp_validation/dashboard/data_vectors.py @@ -88,8 +88,9 @@ def pure_eb(pe, out, scale_cut, ptes, blind=None): with S.figure_style(): fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True) theta = pe["theta"] - start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], - scale_cut) + start, stop = bins_from_scale_cut( + pe["left_edges"], pe["right_edges"], scale_cut + ) shade = (pe["left_edges"][start], pe["right_edges"][stop - 1]) values = [] for ax, xi, sign in zip(axes, ("xip", "xim"), "+-"): @@ -113,9 +114,7 @@ def pure_eb(pe, out, scale_cut, ptes, blind=None): S.shade_outside(ax, *shade) ax.set_ylabel(r"$\theta\,\xi \times 10^{4}$") S.label_box(ax, rf"$\xi_{sign}$", loc="upper left") - _pte_header( - ax, rf"$\xi_{sign}^B$ PTE ${S.fmt_pte(ptes.get(xi + '_B'))}$" - ) + _pte_header(ax, rf"$\xi_{sign}^B$ PTE ${S.fmt_pte(ptes.get(xi + '_B'))}$") axes[-1].set_xlabel(r"$\theta$ [arcmin]") # Shared y range from the in-cut points, so the excluded large-scale # ambiguous modes don't set the scale. @@ -127,16 +126,30 @@ def pure_eb(pe, out, scale_cut, ptes, blind=None): for ax in axes: ax.set_ylim(lo - pad, hi + 2.5 * pad) handles = [ - Line2D([], [], ls="none", marker=m, color=c, alpha=a, - markersize=4, label=lab) + Line2D( + [], [], ls="none", marker=m, color=c, alpha=a, markersize=4, label=lab + ) for _, m, c, a, _, lab in series ] - fig.legend(handles=handles, loc="outside upper center", ncol=4, - handletextpad=0.1, columnspacing=0.9, borderaxespad=0.1) + fig.legend( + handles=handles, + loc="outside upper center", + ncol=4, + handletextpad=0.1, + columnspacing=0.9, + borderaxespad=0.1, + ) if "combined" in ptes: - fig.text(0.99, 0.003, rf"joint $\xi_\pm^B$ PTE " - rf"${S.fmt_pte(ptes['combined'])}$", ha="right", va="bottom", - fontsize=8, color=S.INK_MUTED) + fig.text( + 0.99, + 0.003, + rf"joint $\xi_\pm^B$ PTE " + rf"${S.fmt_pte(ptes['combined'])}$", + ha="right", + va="bottom", + fontsize=8, + color=S.INK_MUTED, + ) if blind: S.blind_stamp(fig, blind, where="bottom") return S.save(fig, out, "third") @@ -155,8 +168,9 @@ def cosebis(co, out, full_cut=(1.0, 250.0)): ] n = np.arange(1, co["nmodes"] + 1) with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, - sharey=True) + fig, axes = plt.subplots( + 2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True + ) extent = 0 for ax, (cut, marker, filled, color) in zip(axes, rows): r = co["cuts"][cut] @@ -164,13 +178,22 @@ def cosebis(co, out, full_cut=(1.0, 250.0)): extent = max(extent, np.max(np.abs(y)) + 1) S.zero_line(ax) ax.errorbar( - n, y, yerr=np.ones_like(y), marker=marker, color=color, + n, + y, + yerr=np.ones_like(y), + marker=marker, + color=color, markerfacecolor=color if filled else "none", - markeredgecolor=color, **S.DATA_VECTOR_MARK, + markeredgecolor=color, + **S.DATA_VECTOR_MARK, ) ax.set_ylabel(r"$B_n / \sigma_n$") - S.label_box(ax, rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", - loc="upper left", fontsize=8.5) + S.label_box( + ax, + rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", + loc="upper left", + fontsize=8.5, + ) _pte_header(ax, rf"$B_n$ PTE ${S.fmt_pte(r['pte_B'])}$") lim = np.ceil(extent + 0.6) axes[0].set_ylim(-lim, lim + 1.2) @@ -184,8 +207,11 @@ def cosebis(co, out, full_cut=(1.0, 250.0)): def _spacer_legend(fig): """Reserve the legend band the pure E/B figure uses, keeping rows aligned.""" - fig.legend(handles=[Line2D([], [], ls="none", label=" ")], - loc="outside upper center", handlelength=0) + fig.legend( + handles=[Line2D([], [], ls="none", label=" ")], + loc="outside upper center", + handlelength=0, + ) def _cl_cut_mask(cl, ell_cut): @@ -194,8 +220,9 @@ def _cl_cut_mask(cl, ell_cut): def cl_pte(cl, key, ell_cut=None): """χ² PTE of one spectrum against zero, over bins inside ``ell_cut``.""" - mask = np.ones(len(cl["ell"]), bool) if ell_cut is None else _cl_cut_mask( - cl, ell_cut) + mask = ( + np.ones(len(cl["ell"]), bool) if ell_cut is None else _cl_cut_mask(cl, ell_cut) + ) d = cl[key][mask] cov = cl["cov"][f"{key}_{key}"][np.ix_(mask, mask)] return float(stats.chi2.sf(d @ np.linalg.solve(cov, d), mask.sum())) @@ -209,17 +236,23 @@ def pseudo_cl(cl, out, ell_cut): ] ell = cl["ell"] with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, - sharey=True) + fig, axes = plt.subplots( + 2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True + ) extent = 0 for ax, (key, marker, filled, color, label) in zip(axes, rows): y = cl[key] / cl[f"sigma_{key}"] extent = max(extent, np.max(np.abs(y)) + 1) S.zero_line(ax) ax.errorbar( - ell, y, yerr=np.ones_like(y), marker=marker, color=color, + ell, + y, + yerr=np.ones_like(y), + marker=marker, + color=color, markerfacecolor=color if filled else "none", - markeredgecolor=color, **S.DATA_VECTOR_MARK, + markeredgecolor=color, + **S.DATA_VECTOR_MARK, ) ax.set_xscale("function", functions=(np.sqrt, np.square)) ax.set_ylabel(r"$C_\ell / \sigma$") diff --git a/src/sp_validation/dashboard/field_maps.py b/src/sp_validation/dashboard/field_maps.py index 4039e6704..736022329 100644 --- a/src/sp_validation/dashboard/field_maps.py +++ b/src/sp_validation/dashboard/field_maps.py @@ -193,9 +193,7 @@ def main(argv=None): parser.add_argument("--nside", type=int, default=512) parser.add_argument("--weight-bins", type=int, default=20) parser.add_argument("--snr-range", type=float, nargs=2, default=(10.0, 500.0)) - parser.add_argument( - "--size-ratio-range", type=float, nargs=2, default=(0.707, 3.0) - ) + parser.add_argument("--size-ratio-range", type=float, nargs=2, default=(0.707, 3.0)) parser.add_argument("--chunk", type=int, default=4_000_000) args = parser.parse_args(argv) diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index a4247e119..80e9d3dbf 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -63,10 +63,15 @@ def cosebis(self): def pseudo_cl_cov(self): prefix = f"pseudo_cl_cov_{self.version}_" binning = re.compile(r"(powspace|logspace|linear)_nbins=\d+\.fits") - matches = [p for p in self.cv.glob(prefix + "*.fits") - if binning.fullmatch(p.name[len(prefix):])] + matches = [ + p + for p in self.cv.glob(prefix + "*.fits") + if binning.fullmatch(p.name[len(prefix) :]) + ] if len(matches) != 1: - raise FileNotFoundError(f"expected one {prefix}.fits, found {matches}") + raise FileNotFoundError( + f"expected one {prefix}.fits, found {matches}" + ) return matches[0] @property @@ -75,7 +80,9 @@ def psf_fit(self): @property def tau_cov(self): - return _one(self.cv / "rho_tau_stats" / f"cov_tau_{self.version}_tomo_bin_all_*.npy") + return _one( + self.cv / "rho_tau_stats" / f"cov_tau_{self.version}_tomo_bin_all_*.npy" + ) @property def leakage(self): @@ -97,7 +104,9 @@ def xi_covariance(self): @property def field_maps(self): - return _maybe(Path(self.extra_dir or self.cv) / f"field_maps_{self.version}.npz") + return _maybe( + Path(self.extra_dir or self.cv) / f"field_maps_{self.version}.npz" + ) @property def weight_cells(self): @@ -246,8 +255,14 @@ def load_rho_tau(sacc_path, tau_cov_path, psf_fit_path): var = np.diag(tau_cov)[i * n : (i + 1) * n] tau[k]["var_p"] = var fit = dict(np.load(psf_fit_path)) - return {"theta": theta, "theta_tau": theta_tau, "rho": rho, "tau": tau, - "tau_cov": tau_cov, "fit": fit} + return { + "theta": theta, + "theta_tau": theta_tau, + "rho": rho, + "tau": tau, + "tau_cov": tau_cov, + "fit": fit, + } def load_json(path): diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py index a834f990c..b4cfb2a03 100644 --- a/src/sp_validation/dashboard/psf.py +++ b/src/sp_validation/dashboard/psf.py @@ -56,11 +56,16 @@ def tau_fit(rt, out): p = _power_of_ten(np.concatenate([tau + sigma, tau - sigma])) scale = 10.0**p S.zero_line(ax) - ax.fill_between(theta, lo / scale, hi / scale, color=MODEL, alpha=0.3, - lw=0) + ax.fill_between(theta, lo / scale, hi / scale, color=MODEL, alpha=0.3, lw=0) ax.plot(theta, med / scale, color=MODEL, lw=1.1) - ax.errorbar(theta, tau / scale, yerr=sigma / scale, marker="o", - color=S.INK, **S.DATA_VECTOR_MARK) + ax.errorbar( + theta, + tau / scale, + yerr=sigma / scale, + marker="o", + color=S.INK, + **S.DATA_VECTOR_MARK, + ) S.theta_axis(ax) ax.set_xticks([1, 10, 100]) ax.set_xticklabels(["$1$", "$10$", "$100$"]) @@ -68,10 +73,23 @@ def tau_fit(rt, out): ax.set_ylabel(rf"$\tau_{{{k},+}} \times 10^{{{-p}}}$") S.label_box(ax, rf"$\tau_{{{k}}}$", loc="upper right") handles = [ - Line2D([], [], ls="none", marker="o", color=S.INK, markersize=3.5, - label=r"measured $\tau_{k,+}$"), - Line2D([], [], color=MODEL, lw=4, alpha=0.4, - label=r"$\alpha, \beta, \eta$ posterior (68%)"), + Line2D( + [], + [], + ls="none", + marker="o", + color=S.INK, + markersize=3.5, + label=r"measured $\tau_{k,+}$", + ), + Line2D( + [], + [], + color=MODEL, + lw=4, + alpha=0.4, + label=r"$\alpha, \beta, \eta$ posterior (68%)", + ), ] fig.legend(handles=handles, loc="outside upper center", ncol=2) return S.save(fig, out, "full") @@ -99,29 +117,52 @@ def posterior(rt, out, markers=(0, 1, 1)): continue if i == j: x = samples[:, i] - ax.hist(x, bins=40, range=lims[i], histtype="stepfilled", - color=MODEL, alpha=0.25) - ax.hist(x, bins=40, range=lims[i], histtype="step", - color=MODEL, lw=0.9) + ax.hist( + x, + bins=40, + range=lims[i], + histtype="stepfilled", + color=MODEL, + alpha=0.25, + ) + ax.hist( + x, bins=40, range=lims[i], histtype="step", color=MODEL, lw=0.9 + ) lo, med, hi = np.percentile(x, [16, 50, 84]) ax.set_title( rf"${PARAMS[i]} = {med:.3g}^{{+{hi - med:.2g}}}" - rf"_{{-{med - lo:.2g}}}$", fontsize=7.5) + rf"_{{-{med - lo:.2g}}}$", + fontsize=7.5, + ) ax.set_yticks([]) if markers[i] is not None: ax.axvline(markers[i], color=S.INK_MUTED, lw=0.7, ls=":") else: - h, xe, ye = np.histogram2d(samples[:, j], samples[:, i], - bins=40, range=[lims[j], lims[i]]) + h, xe, ye = np.histogram2d( + samples[:, j], samples[:, i], bins=40, range=[lims[j], lims[i]] + ) h = gaussian_filter(h, 1.0) levels = _contour_levels(h) xc, yc = (xe[1:] + xe[:-1]) / 2, (ye[1:] + ye[:-1]) / 2 - ax.contourf(xc, yc, h.T, levels=[levels[0], h.max()], - colors=[MODEL], alpha=0.18) - ax.contourf(xc, yc, h.T, levels=[levels[1], h.max()], - colors=[MODEL], alpha=0.38) - ax.contour(xc, yc, h.T, levels=levels, colors=[MODEL], - linewidths=0.7) + ax.contourf( + xc, + yc, + h.T, + levels=[levels[0], h.max()], + colors=[MODEL], + alpha=0.18, + ) + ax.contourf( + xc, + yc, + h.T, + levels=[levels[1], h.max()], + colors=[MODEL], + alpha=0.38, + ) + ax.contour( + xc, yc, h.T, levels=levels, colors=[MODEL], linewidths=0.7 + ) for v, axis in ((markers[j], ax.axvline), (markers[i], ax.axhline)): if v is not None: axis(v, color=S.INK_MUTED, lw=0.7, ls=":") @@ -154,25 +195,49 @@ def rho_stats(rt, out): rho = rt["rho"][k]["p"] sig = rt["rho"][k]["sigma_p"] pos = rho > 0 - ax.errorbar(theta, np.abs(rho), yerr=sig, color=c, ls="-", - lw=0.7, marker="none", elinewidth=0.6, capsize=0, - alpha=0.9) - ax.plot(theta[pos], rho[pos], "o", color=c, ms=3.4, - mec=c, label=rf"$\rho_{k}$") - ax.plot(theta[~pos], -rho[~pos], "o", color=c, ms=3.4, - mfc=S.RAG, mec=c, mew=0.9) + ax.errorbar( + theta, + np.abs(rho), + yerr=sig, + color=c, + ls="-", + lw=0.7, + marker="none", + elinewidth=0.6, + capsize=0, + alpha=0.9, + ) + ax.plot( + theta[pos], + rho[pos], + "o", + color=c, + ms=3.4, + mec=c, + label=rf"$\rho_{k}$", + ) + ax.plot( + theta[~pos], + -rho[~pos], + "o", + color=c, + ms=3.4, + mfc=S.RAG, + mec=c, + mew=0.9, + ) ax.set_yscale("log") S.theta_axis(ax) ax.set_xticks([1, 10, 100]) ax.set_xticklabels(["$1$", "$10$", "$100$"]) ax.set_xlabel(r"$\theta$ [arcmin]") - ax.legend(loc="upper right", ncol=3, handletextpad=0.2, - columnspacing=0.8) + ax.legend(loc="upper right", ncol=3, handletextpad=0.2, columnspacing=0.8) ax.set_title(label, loc="left", fontsize=9) axes[0].set_ylabel(r"$|\rho_{k,+}(\theta)|$") axes[0].set_ylim(top=axes[0].get_ylim()[1] * 8) - axes[1].set_title(r"open markers: $\rho < 0$", loc="right", fontsize=8.5, - color=S.INK_MUTED) + axes[1].set_title( + r"open markers: $\rho < 0$", loc="right", fontsize=8.5, color=S.INK_MUTED + ) return S.save(fig, out, "full") @@ -182,8 +247,7 @@ def xi_sys_ratio(rt, xi, out, reference=0.1): theta = fit["theta"] with S.figure_style(): fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.5)) - for ax, comp, key, sign in zip(axes, ("plus", "minus"), ("xip", "xim"), - "+-"): + for ax, comp, key, sign in zip(axes, ("plus", "minus"), ("xip", "xim"), "+-"): ratio = fit[f"samples_{comp}"] / xi[key][None, :] lo, med, hi = np.percentile(ratio, [16, 50, 84], axis=0) for r in (-reference, reference): @@ -233,8 +297,16 @@ def alpha_estimates(leakage, rt, out): y[i:] -= 0.6 ax.axvline(0, color=S.INK, lw=0.7, ls=(0, (4, 3)), alpha=0.6) for yi, (label, group, v, em, ep) in zip(y, rows): - ax.errorbar([v], [yi], xerr=[[em], [ep]], fmt="o", color=S.INK, - ms=3.6, elinewidth=0.9, capsize=1.8) + ax.errorbar( + [v], + [yi], + xerr=[[em], [ep]], + fmt="o", + color=S.INK, + ms=3.6, + elinewidth=0.9, + capsize=1.8, + ) ax.set_yticks(y) ax.set_yticklabels([r[0] for r in rows]) ax.tick_params(axis="y", length=0, right=False) @@ -242,8 +314,16 @@ def alpha_estimates(leakage, rt, out): prev = None for yi, g in zip(y, groups): if g != prev: - ax.text(0.98, yi + 0.42, g, transform=ax.get_yaxis_transform(), - ha="right", va="bottom", fontsize=7.5, color=S.INK_MUTED) + ax.text( + 0.98, + yi + 0.42, + g, + transform=ax.get_yaxis_transform(), + ha="right", + va="bottom", + fontsize=7.5, + color=S.INK_MUTED, + ) prev = g ax.set_ylim(y.min() - 0.7, y.max() + 0.9) ax.set_xlabel("PSF leakage coefficient") diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index 656a33fc1..bcfd18698 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -47,10 +47,14 @@ def run_config(run_dir): if prov.exists(): cfg = yaml.safe_load(json.loads(prov.read_text())["config"]) cuts["theta"] = tuple(map(float, cfg["cosmo_val"]["fiducial_scale_cut"])) - cuts["ell"] = (int(cfg["cl"]["fiducial_ell_min"]), - int(cfg["cl"]["fiducial_ell_max"])) - cuts["cosebis_full"] = (float(cfg["cosebis"]["theta_min"]), - float(cfg["cosebis"]["theta_max"])) + cuts["ell"] = ( + int(cfg["cl"]["fiducial_ell_min"]), + int(cfg["cl"]["fiducial_ell_max"]), + ) + cuts["cosebis_full"] = ( + float(cfg["cosebis"]["theta_min"]), + float(cfg["cosebis"]["theta_max"]), + ) return cuts @@ -66,8 +70,14 @@ def path(self, name): return self.out / f"{name}.png" def add(self, fid, section, caption, size, narrow=False, alt=None): - entry = {"id": fid, "section": section, "file": f"{fid}.png", - "caption": caption, "size": size, "alt": alt or caption} + entry = { + "id": fid, + "section": section, + "file": f"{fid}.png", + "caption": caption, + "size": size, + "alt": alt or caption, + } if narrow: entry["narrow"] = f"{fid}_narrow.png" self.entries.append(entry) @@ -82,140 +92,255 @@ def render(run_dir, versions, out, maps_dir=None): pe = P.load_pure_eb(ref.pure_eb, blind) co = P.load_cosebis(ref.cosebis, blind) cl = P.load_pseudo_cl(ref.assembled, ref.pseudo_cl_cov, blind) - summary = P.load_json(ref.summary).get(versions[0], {}) if ref.summary.exists() else {} + summary = ( + P.load_json(ref.summary).get(versions[0], {}) if ref.summary.exists() else {} + ) theta_cut, ell_cut = cuts["theta"], cuts["ell"] tcut = rf"{theta_cut[0]:g}′–{theta_cut[1]:g}′" # 1. hero DV.xi_pm(xi, figs.path("xi_pm"), blind=blind) DV.xi_pm(xi, figs.path("xi_pm_narrow"), layout="narrow", blind=blind) - figs.add("xi_pm", "xi", - "Blinded ξ± on the 20-bin reporting grid; error bars from the analytic covariance.", - "full", narrow=True) + figs.add( + "xi_pm", + "xi", + "Blinded ξ± on the 20-bin reporting grid; error bars from the analytic covariance.", + "full", + narrow=True, + ) # 2. B-mode data vectors ptes = {k: summary.get(k) for k in ("xip_B", "xim_B", "combined")} DV.pure_eb(pe, figs.path("bmode_pure_eb"), theta_cut, ptes, blind=blind) - figs.add("bmode_pure_eb", "bmodes", - f"Pure-mode decomposition of ξ±; B (crimson) is the null test. Shaded: outside {tcut}.", - "third") + figs.add( + "bmode_pure_eb", + "bmodes", + f"Pure-mode decomposition of ξ±; B (crimson) is the null test. Shaded: outside {tcut}.", + "third", + ) DV.cosebis(co, figs.path("bmode_cosebis"), cuts["cosebis_full"]) - figs.add("bmode_cosebis", "bmodes", - "COSEBIs B-modes in units of their uncertainty, at the fiducial cut and over all scales.", - "third") + figs.add( + "bmode_cosebis", + "bmodes", + "COSEBIs B-modes in units of their uncertainty, at the fiducial cut and over all scales.", + "third", + ) DV.pseudo_cl(cl, figs.path("bmode_cl"), ell_cut) - figs.add("bmode_cl", "bmodes", - f"Pseudo-Cℓ BB and EB in units of their uncertainty. Shaded: outside {ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}.", - "third") + figs.add( + "bmode_cl", + "bmodes", + f"Pseudo-Cℓ BB and EB in units of their uncertainty. Shaded: outside {ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}.", + "third", + ) fid_co = co["cuts"][co["fiducial"]]["pte_B"] headline = [ - {"label": "ξ± pure B, joint", "symbol": "xi", "pte": ptes.get("combined"), - "range": tcut}, - {"label": "COSEBIs B", "symbol": "cosebis", "pte": fid_co, - "range": rf"{co['fiducial'][0]:g}′–{co['fiducial'][1]:g}′"}, - {"label": "pseudo-Cℓ BB", "symbol": "cl", "pte": DV.cl_pte(cl, "BB", ell_cut), - "range": f"{ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}"}, + { + "label": "ξ± pure B, joint", + "symbol": "xi", + "pte": ptes.get("combined"), + "range": tcut, + }, + { + "label": "COSEBIs B", + "symbol": "cosebis", + "pte": fid_co, + "range": rf"{co['fiducial'][0]:g}′–{co['fiducial'][1]:g}′", + }, + { + "label": "pseudo-Cℓ BB", + "symbol": "cl", + "pte": DV.cl_pte(cl, "BB", ell_cut), + "range": f"{ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}", + }, ] # 3. scale cuts SC.config_space(pe, figs.path("ptes_config"), theta_cut) SC.config_space(pe, figs.path("ptes_config_narrow"), theta_cut, layout="narrow") - figs.add("ptes_config", "scale_cuts", - "Configuration-space B-mode PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", - "full", narrow=True) + figs.add( + "ptes_config", + "scale_cuts", + "Configuration-space B-mode PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", + "full", + narrow=True, + ) SC.harmonic_and_cosebis(cl, co, figs.path("ptes_harmonic"), ell_cut) - SC.harmonic_and_cosebis(cl, co, figs.path("ptes_harmonic_narrow"), ell_cut, - layout="narrow") - figs.add("ptes_harmonic", "scale_cuts", - "Cℓ BB PTE for every band range, and the COSEBIs B PTE for each cached angular cut.", - "full", narrow=True) + SC.harmonic_and_cosebis( + cl, co, figs.path("ptes_harmonic_narrow"), ell_cut, layout="narrow" + ) + figs.add( + "ptes_harmonic", + "scale_cuts", + "Cℓ BB PTE for every band range, and the COSEBIs B PTE for each cached angular cut.", + "full", + narrow=True, + ) # 4. covariances n = len(xi["theta"]) - CV.correlation(xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), - rf"$\xi_\pm$ reporting grid") - figs.add("cov_xi", "covariance", "ξ± reporting-grid covariance (analytic, masked).", "half") - CV.correlation(pe["cov"], CV.pure_eb_blocks(len(pe["theta"])), - figs.path("cov_pure_eb"), "pure E/B modes") - figs.add("cov_pure_eb", "covariance", "Pure E, B and ambiguous modes of ξ±.", "half") + CV.correlation( + xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), r"$\xi_\pm$ reporting grid" + ) + figs.add( + "cov_xi", + "covariance", + "ξ± reporting-grid covariance (analytic, masked).", + "half", + ) + CV.correlation( + pe["cov"], + CV.pure_eb_blocks(len(pe["theta"])), + figs.path("cov_pure_eb"), + "pure E/B modes", + ) + figs.add( + "cov_pure_eb", "covariance", "Pure E, B and ambiguous modes of ξ±.", "half" + ) fc = co["fiducial"] - CV.correlation(co["cuts"][fc]["cov"], CV.cosebis_blocks(co["nmodes"]), - figs.path("cov_cosebis"), - rf"COSEBIs, ${fc[0]:g}'\!-\!{fc[1]:g}'$") - figs.add("cov_cosebis", "covariance", "COSEBIs E and B modes at the fiducial cut.", "half") - CV.correlation(CV.cl_matrix(cl["cov"]), CV.cl_blocks(len(cl["ell"])), - figs.path("cov_cl"), r"pseudo-$C_\ell$") - figs.add("cov_cl", "covariance", "Pseudo-Cℓ EE, EB and BB (Gaussian, NaMaster).", "half") + CV.correlation( + co["cuts"][fc]["cov"], + CV.cosebis_blocks(co["nmodes"]), + figs.path("cov_cosebis"), + rf"COSEBIs, ${fc[0]:g}'\!-\!{fc[1]:g}'$", + ) + figs.add( + "cov_cosebis", + "covariance", + "COSEBIs E and B modes at the fiducial cut.", + "half", + ) + CV.correlation( + CV.cl_matrix(cl["cov"]), + CV.cl_blocks(len(cl["ell"])), + figs.path("cov_cl"), + r"pseudo-$C_\ell$", + ) + figs.add( + "cov_cl", "covariance", "Pseudo-Cℓ EE, EB and BB (Gaussian, NaMaster).", "half" + ) # 5. PSF leakage rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit) PSF.tau_fit(rt, figs.path("psf_tau")) - figs.add("psf_tau", "psf", - "Galaxy–PSF correlations τ₀,₂,₅ with the posterior band of the α, β, η leakage model.", - "full") + figs.add( + "psf_tau", + "psf", + "Galaxy–PSF correlations τ₀,₂,₅ with the posterior band of the α, β, η leakage model.", + "full", + ) PSF.posterior(rt, figs.path("psf_posterior")) - figs.add("psf_posterior", "psf", "Posterior of the leakage parameters (68 and 95%).", "half") + figs.add( + "psf_posterior", + "psf", + "Posterior of the leakage parameters (68 and 95%).", + "half", + ) leakage = P.load_json(ref.leakage).get(versions[0], {}).get("tomo_bin_all", {}) PSF.alpha_estimates(leakage, rt, figs.path("psf_alpha")) - figs.add("psf_alpha", "psf", - "Every estimate of the leakage coefficient: object-wise, scale-dependent and ρ/τ fit.", - "half") + figs.add( + "psf_alpha", + "psf", + "Every estimate of the leakage coefficient: object-wise, scale-dependent and ρ/τ fit.", + "half", + ) PSF.rho_stats(rt, figs.path("psf_rho")) - figs.add("psf_rho", "psf", "ρ statistics of the PSF model and its residuals.", "full") + figs.add( + "psf_rho", "psf", "ρ statistics of the PSF model and its residuals.", "full" + ) PSF.xi_sys_ratio(rt, xi, figs.path("psf_xi_sys")) - figs.add("psf_xi_sys", "psf", - "Propagated PSF systematic relative to the blinded ξ±; dotted lines at ±10%.", - "full") + figs.add( + "psf_xi_sys", + "psf", + "Propagated PSF systematic relative to the blinded ξ±; dotted lines at ±10%.", + "full", + ) # 6. weights if ref.weight_cells is not None: cells = dict(np.load(ref.weight_cells, allow_pickle=False)) W.weights(cells, figs.path("weights")) s = W.summary(cells) - figs.add("weights", "weights", - f"Each galaxy carries the weight R²/σₑ² of its SNR × size cell, so the " - f"{s['n_values']} distinct values are the {s['n_occupied']} occupied cells; " - f"histograms of w are combs, not clipping.", - "full") + figs.add( + "weights", + "weights", + f"Each galaxy carries the weight R²/σₑ² of its SNR × size cell, so the " + f"{s['n_values']} distinct values are the {s['n_occupied']} occupied cells; " + f"histograms of w are combs, not clipping.", + "full", + ) # 7. sky if ref.field_maps is not None: maps = dict(np.load(ref.field_maps, allow_pickle=False)) footprint = dict(np.load(ref.cv / f"footprint_{versions[0]}.npz")) sky.footprint_map(maps, footprint, figs.path("sky_footprint")) - figs.add("sky_footprint", "sky", - "Effective source density in a zenithal equal-area projection centred on the patch.", - "full") + figs.add( + "sky_footprint", + "sky", + "Effective source density in a zenithal equal-area projection centred on the patch.", + "full", + ) for which in ("galaxies", "psf"): sky.field_maps(maps, figs.path(f"sky_{which}"), which, footprint) - sky.field_maps(maps, figs.path(f"sky_{which}_narrow"), which, footprint, - layout="narrow") - figs.add("sky_galaxies", "sky", - "Galaxy fields: ellipticity dispersion, mean weight and weighted mean e₁, e₂.", - "full", narrow=True) + sky.field_maps( + maps, + figs.path(f"sky_{which}_narrow"), + which, + footprint, + layout="narrow", + ) + figs.add( + "sky_galaxies", + "sky", + "Galaxy fields: ellipticity dispersion, mean weight and weighted mean e₁, e₂.", + "full", + narrow=True, + ) dt = sky.median_quantity(maps, "dT_over_T", 128) - figs.add("sky_psf", "sky", - "PSF fields: model FWHM at galaxies, and star-minus-model size and ellipticity " - f"residuals (median size residual {100 * dt:+.2f}%).", - "full", narrow=True) + figs.add( + "sky_psf", + "sky", + "PSF fields: model FWHM at galaxies, and star-minus-model size and ellipticity " + f"residuals (median size residual {100 * dt:+.2f}%).", + "full", + narrow=True, + ) # 8. versions if len(versions) > 1: runs = [P.RunProducts(run_dir, v, maps_dir) for v in versions] blinds = [P.blind_of(r.assembled) for r in runs] - V.pure_eb([(v, P.load_pure_eb(r.pure_eb, b)) - for v, r, b in zip(versions, runs, blinds)], - figs.path("versions_pure_eb"), theta_cut) - figs.add("versions_pure_eb", "versions", - "ξ± and pure-B ξ±/σ across versions; boxes span the versions, bars mark the reference.", - "full") - V.cosebis([(v, P.load_cosebis(r.cosebis, b)) - for v, r, b in zip(versions, runs, blinds)], - figs.path("versions_cosebis"), cuts["cosebis_full"]) + V.pure_eb( + [ + (v, P.load_pure_eb(r.pure_eb, b)) + for v, r, b in zip(versions, runs, blinds) + ], + figs.path("versions_pure_eb"), + theta_cut, + ) + figs.add( + "versions_pure_eb", + "versions", + "ξ± and pure-B ξ±/σ across versions; boxes span the versions, bars mark the reference.", + "full", + ) + V.cosebis( + [ + (v, P.load_cosebis(r.cosebis, b)) + for v, r, b in zip(versions, runs, blinds) + ], + figs.path("versions_cosebis"), + cuts["cosebis_full"], + ) figs.add("versions_cosebis", "versions", "COSEBIs B/σ across versions.", "full") - V.pseudo_cl([(v, P.load_pseudo_cl(r.assembled, r.pseudo_cl_cov, b)) - for v, r, b in zip(versions, runs, blinds)], - figs.path("versions_cl"), ell_cut) + V.pseudo_cl( + [ + (v, P.load_pseudo_cl(r.assembled, r.pseudo_cl_cov, b)) + for v, r, b in zip(versions, runs, blinds) + ], + figs.path("versions_cl"), + ell_cut, + ) figs.add("versions_cl", "versions", "Cℓ BB/σ and EB/σ across versions.", "full") manifest = { @@ -226,16 +351,21 @@ def render(run_dir, versions, out, maps_dir=None): "headline_ptes": headline, "figures": figs.entries, } - (figs.out / "figures.json").write_text(json.dumps(manifest, indent=2, - ensure_ascii=False) + "\n") + (figs.out / "figures.json").write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n" + ) return manifest def main(argv=None): parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) parser.add_argument("--run-dir", required=True) - parser.add_argument("--version", action="append", required=True, - help="catalogue version; repeat for a comparison run") + parser.add_argument( + "--version", + action="append", + required=True, + help="catalogue version; repeat for a comparison run", + ) parser.add_argument("--maps-dir", help="directory of field_maps/weight_cells NPZs") parser.add_argument("--out", required=True) args = parser.parse_args(argv) diff --git a/src/sp_validation/dashboard/scale_cuts.py b/src/sp_validation/dashboard/scale_cuts.py index bf6deb851..77f910828 100644 --- a/src/sp_validation/dashboard/scale_cuts.py +++ b/src/sp_validation/dashboard/scale_cuts.py @@ -38,23 +38,38 @@ def _heatmap(ax, pte, lo_labels, hi_labels, fid, label, cmap, norm): """Triangular PTE heatmap on bin indices; ``fid`` is ``(i, j)`` inclusive.""" n = pte.shape[0] grid = np.arange(n + 1) - 0.5 - mesh = ax.pcolormesh(grid, grid, pte.T, cmap=cmap, norm=norm, - edgecolors="face", linewidth=0, rasterized=True) + mesh = ax.pcolormesh( + grid, + grid, + pte.T, + cmap=cmap, + norm=norm, + edgecolors="face", + linewidth=0, + rasterized=True, + ) ax.set_facecolor("#e4dccb") ax.set_aspect("equal") ax.set_xlim(-0.5, n - 0.5) ax.set_ylim(-0.5, n - 0.5) ticks = _tick_subset(n) ax.set_xticks(ticks) - ax.set_xticklabels([f"${lo_labels[t]}$" for t in ticks], rotation=45, - ha="right", rotation_mode="anchor") + ax.set_xticklabels( + [f"${lo_labels[t]}$" for t in ticks], + rotation=45, + ha="right", + rotation_mode="anchor", + ) ax.set_yticks(ticks) ax.set_yticklabels([f"${hi_labels[t]}$" for t in ticks]) ax.tick_params(which="both", top=False, right=False, length=2.5) if fid is not None: i, j = fid - ax.add_patch(Rectangle((i - 0.5, j - 0.5), 1, 1, fill=False, - edgecolor=S.INK, lw=1.6, zorder=5)) + ax.add_patch( + Rectangle( + (i - 0.5, j - 0.5), 1, 1, fill=False, edgecolor=S.INK, lw=1.6, zorder=5 + ) + ) S.label_box(ax, label, loc="lower right", fontsize=9) return mesh @@ -65,8 +80,7 @@ def _fmt_angle(v): def config_space(pe, out, scale_cut, layout="wide"): """ξ+^B, ξ−^B and joint ξ±^B PTEs over all reporting-bin scale cuts.""" - start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], - scale_cut) + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], scale_cut) fid = (start, stop - 1) lo = [_fmt_angle(v) for v in pe["left_edges"]] hi = [_fmt_angle(v) for v in pe["right_edges"]] @@ -82,15 +96,20 @@ def config_space(pe, out, scale_cut, layout="wide"): else: fig, axes = plt.subplots(3, 1, figsize=(3.6, 9.6), sharex=True) for ax, (key, label) in zip(axes, panels): - mesh = _heatmap(ax, pe["pte_matrices"][key], lo, hi, fid, label, - cmap, norm) + mesh = _heatmap(ax, pe["pte_matrices"][key], lo, hi, fid, label, cmap, norm) for ax in axes if layout == "wide" else axes[-1:]: ax.set_xlabel(r"$\theta_{\min}$ [arcmin]") for ax in axes[:1] if layout == "wide" else axes: ax.set_ylabel(r"$\theta_{\max}$ [arcmin]") - S.pte_colorbar(fig, mesh, axes, shrink=0.9 if layout == "wide" else 0.5, - aspect=25, pad=0.02, - location="right" if layout == "wide" else "bottom") + S.pte_colorbar( + fig, + mesh, + axes, + shrink=0.9 if layout == "wide" else 0.5, + aspect=25, + pad=0.02, + location="right" if layout == "wide" else "bottom", + ) return S.save(fig, out, "full" if layout == "wide" else "half") @@ -105,17 +124,25 @@ def harmonic_and_cosebis(cl, co, out, ell_cut, layout="wide"): with S.figure_style(): if layout == "wide": fig, (ax_cl, ax_co) = plt.subplots( - 1, 2, figsize=(7.2, 3.0), width_ratios=(1, 1.15)) + 1, 2, figsize=(7.2, 3.0), width_ratios=(1, 1.15) + ) else: - fig, (ax_cl, ax_co) = plt.subplots(2, 1, figsize=(3.6, 6.6), - height_ratios=(1.25, 1)) + fig, (ax_cl, ax_co) = plt.subplots( + 2, 1, figsize=(3.6, 6.6), height_ratios=(1.25, 1) + ) mesh = _heatmap(ax_cl, pte, lo, hi, fid, r"$C_\ell^{BB}$", cmap, norm) ax_cl.set_xlabel(r"$\ell_{\min}$") ax_cl.set_ylabel(r"$\ell_{\max}$") _cosebis_cuts(ax_co, co, cmap, norm) - S.pte_colorbar(fig, mesh, [ax_cl, ax_co], aspect=25, pad=0.02, - shrink=0.9 if layout == "wide" else 0.6, - location="right" if layout == "wide" else "bottom") + S.pte_colorbar( + fig, + mesh, + [ax_cl, ax_co], + aspect=25, + pad=0.02, + shrink=0.9 if layout == "wide" else 0.6, + location="right" if layout == "wide" else "bottom", + ) return S.save(fig, out, "full" if layout == "wide" else "half") @@ -129,12 +156,25 @@ def _cosebis_cuts(ax, co, cmap, norm, threshold=S.PTE_THRESHOLD): p = co["cuts"][cut]["pte_B"] is_fid = cut == co["fiducial"] ax.hlines(yi, 0, 1, color=S.RULE, lw=0.6, zorder=1) - ax.scatter([p], [yi], s=46 if is_fid else 30, color=cmap(norm(p)), - edgecolor=S.INK, linewidth=1.4 if is_fid else 0.6, zorder=3, - marker="s" if is_fid else "o") - ax.text(min(p + 0.035, 0.97), yi + 0.18, f"${S.fmt_pte(p)}$", - fontsize=7.5, color=S.INK_MUTED, va="bottom", - ha="left" if p < 0.85 else "right") + ax.scatter( + [p], + [yi], + s=46 if is_fid else 30, + color=cmap(norm(p)), + edgecolor=S.INK, + linewidth=1.4 if is_fid else 0.6, + zorder=3, + marker="s" if is_fid else "o", + ) + ax.text( + min(p + 0.035, 0.97), + yi + 0.18, + f"${S.fmt_pte(p)}$", + fontsize=7.5, + color=S.INK_MUTED, + va="bottom", + ha="left" if p < 0.85 else "right", + ) ax.set_yticks(y) ax.set_yticklabels([rf"${c[0]:g}'\!-\!{c[1]:g}'$" for c in cuts]) ax.set_xlim(0, 1) diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py index 57d43a31d..37cdc23b0 100644 --- a/src/sp_validation/dashboard/sky.py +++ b/src/sp_validation/dashboard/sky.py @@ -29,8 +29,8 @@ "parchment_teal", ["#f3ead6", "#9cc3b5", "#3F8278", "#1d4a44", "#0f2421"] ) DIVERGING = LinearSegmentedColormap.from_list( - "cobalt_cinnabar", ["#22386e", "#3D5BA0", "#a9b6d6", "#efe6d2", "#e4ab9f", - "#BC4538", "#6e2119"] + "cobalt_cinnabar", + ["#22386e", "#3D5BA0", "#a9b6d6", "#efe6d2", "#e4ab9f", "#BC4538", "#6e2119"], ) @@ -69,7 +69,8 @@ def inverse(self, x, y): d0 = self.dec0 * DEG with np.errstate(invalid="ignore", divide="ignore"): sin_d = np.cos(c) * np.sin(d0) + np.where( - rho > 0, y * np.sin(c) * np.cos(d0) / rho, 0) + rho > 0, y * np.sin(c) * np.cos(d0) / rho, 0 + ) dec = np.arcsin(np.clip(sin_d, -1, 1)) ra = self.ra0 * DEG + np.arctan2( x * np.sin(c), rho * np.cos(d0) * np.cos(c) - y * np.sin(d0) * np.sin(c) @@ -163,8 +164,13 @@ def make_parts(pixels, nside, nest=True, margin=1.5): angle = (angle + 90) % 180 - 90 # keep RA increasing to the left proj = Zenithal(ra0, dec0, -angle) x, y = proj.forward(ra, dec) - parts.append(Part(proj, (x.min() - margin, x.max() + margin), - (y.min() - margin, y.max() + margin))) + parts.append( + Part( + proj, + (x.min() - margin, x.max() + margin), + (y.min() - margin, y.max() + margin), + ) + ) return parts @@ -202,7 +208,7 @@ def raster(part, pixels, values, nside, resolution, nest=True): def graticule(ax, part, step_ra=None, step_dec=10, color=S.RULE, label=True): """Draw RA/Dec lines across the part and label them along the frame.""" - ra_c, dec_c = part.projection.ra0, part.projection.dec0 + dec_c = part.projection.dec0 span = max(part.width, part.height) step_ra = step_ra or (30 if abs(dec_c) > 65 else 15 if span > 25 else 10) decs = np.arange(-90 + step_dec, 90, step_dec) @@ -243,31 +249,72 @@ def _edge_label(ax, x, y, box, text, side): i = np.flatnonzero(ok)[np.argmin(x[ok])] if x[i] - x0 > 0.08 * (x1 - x0): return - ax.text(x0, y[i], text, ha="right", va="center", fontsize=7, - color=S.INK_MUTED, clip_on=False) + ax.text( + x0, + y[i], + text, + ha="right", + va="center", + fontsize=7, + color=S.INK_MUTED, + clip_on=False, + ) else: i = np.flatnonzero(ok)[np.argmin(y[ok])] if y[i] - y0 < 0.08 * (y1 - y0): - ax.text(x[i], y0, text, ha="center", va="top", fontsize=7, - color=S.INK_MUTED, clip_on=False) + ax.text( + x[i], + y0, + text, + ha="center", + va="top", + fontsize=7, + color=S.INK_MUTED, + clip_on=False, + ) def _scale_bar(ax, part, length=10): x1 = part.xlim[1] - 0.04 * part.width y0 = part.ylim[0] + 0.05 * part.height ax.plot([x1 - length, x1], [y0, y0], color=S.INK, lw=1.2, solid_capstyle="butt") - ax.text(x1 - length / 2, y0 + 0.012 * part.height + 0.3, rf"${length}^\circ$", - ha="center", va="bottom", fontsize=7, color=S.INK) - - -def draw_map(ax, part, pixels, values, nside, cmap, norm, resolution=None, - labels=True, scale_bar=False, grid_color=S.RULE): + ax.text( + x1 - length / 2, + y0 + 0.012 * part.height + 0.3, + rf"${length}^\circ$", + ha="center", + va="bottom", + fontsize=7, + color=S.INK, + ) + + +def draw_map( + ax, + part, + pixels, + values, + nside, + cmap, + norm, + resolution=None, + labels=True, + scale_bar=False, + grid_color=S.RULE, +): resolution = resolution or max(hp.nside2resol(nside, arcmin=True) / 60 / 2, 0.03) img = raster(part, pixels, values, nside, resolution) graticule(ax, part, color=grid_color, label=labels) - im = ax.imshow(img, origin="lower", extent=(*part.xlim, *part.ylim), - cmap=cmap, norm=norm, interpolation="nearest", zorder=2, - rasterized=True) + im = ax.imshow( + img, + origin="lower", + extent=(*part.xlim, *part.ylim), + cmap=cmap, + norm=norm, + interpolation="nearest", + zorder=2, + rasterized=True, + ) ax.set_xlim(part.xlim) ax.set_ylim(part.ylim) ax.set_aspect("equal") @@ -281,8 +328,9 @@ def map_grid(fig, spec, parts): """Axes for every part inside ``spec``: main part above, others below.""" if len(parts) == 1: return [fig.add_subplot(spec)] - heights = [parts[0].height] + [p.height * parts[0].width / max(p.width, - parts[0].width) for p in parts[1:]] + heights = [parts[0].height] + [ + p.height * parts[0].width / max(p.width, parts[0].width) for p in parts[1:] + ] sub = spec.subgridspec(len(parts), 1, height_ratios=heights, hspace=0.05) return [fig.add_subplot(sub[i]) for i in range(len(parts))] @@ -355,8 +403,15 @@ def _robust_limits(values, symmetric=False, q=(1, 99)): def _colorbar(fig, im, ax, label, symmetric): - cbar = fig.colorbar(im, ax=ax, orientation="horizontal", fraction=0.05, - pad=0.02, aspect=28, extend="both") + cbar = fig.colorbar( + im, + ax=ax, + orientation="horizontal", + fraction=0.05, + pad=0.02, + aspect=28, + extend="both", + ) cbar.outline.set_linewidth(0.5) cbar.ax.tick_params(labelsize=7.5, length=2, top=False) cbar.locator = mpl.ticker.MaxNLocator(5, symmetric=symmetric) @@ -375,19 +430,28 @@ def footprint_map(maps, footprint, out, nside=512, stats=None): axes = map_grid(fig, gs[0], parts) norm = mpl.colors.Normalize(*_robust_limits(val, q=(0.5, 99.5))) for i, (ax, part) in enumerate(zip(axes, parts)): - im = draw_map(ax, part, pix, val, nside, SEQUENTIAL, norm, - scale_bar=i == 0) + im = draw_map(ax, part, pix, val, nside, SEQUENTIAL, norm, scale_bar=i == 0) _colorbar(fig, im, axes, r"$n_{\rm eff}$ [arcmin$^{-2}$]", False) if stats: - axes[0].text(0.0, 1.0, stats, transform=axes[0].transAxes, ha="left", - va="top", fontsize=8.5, color=S.INK_MUTED, linespacing=1.5) + axes[0].text( + 0.0, + 1.0, + stats, + transform=axes[0].transAxes, + ha="left", + va="top", + fontsize=8.5, + color=S.INK_MUTED, + linespacing=1.5, + ) return S.save(fig, out, "full") def _aspect(parts): main = parts[0] - h = main.height + sum(p.height * main.width / max(p.width, main.width) - for p in parts[1:]) + h = main.height + sum( + p.height * main.width / max(p.width, main.width) for p in parts[1:] + ) return h / main.width @@ -400,7 +464,12 @@ def _aspect(parts): ], "psf": [ ("fwhm_psf", 256, r"PSF FWHM at galaxies [arcsec]", False), - ("dT_over_T", 128, r"stars: $\langle (T_\star - T_{\rm PSF})/T_\star \rangle$", True), + ( + "dT_over_T", + 128, + r"stars: $\langle (T_\star - T_{\rm PSF})/T_\star \rangle$", + True, + ), ("de1", 128, r"stars: $\langle e_{1,\star} - e_{1,{\rm PSF}} \rangle$", True), ("de2", 128, r"stars: $\langle e_{2,\star} - e_{2,{\rm PSF}} \rangle$", True), ], @@ -425,15 +494,26 @@ def field_maps(maps, out, which, footprint=None, layout="wide"): for cell, (name, nside, label, symmetric) in zip(gs, FIELD_SETS[which]): pix, val = quantity(maps, name, nside, footprint) # Restrict star-based maps to the galaxy footprint. - inside = np.isin(pix, np.unique(gal_pix // (int(maps["nside"]) // nside) ** 2)) + inside = np.isin( + pix, np.unique(gal_pix // (int(maps["nside"]) // nside) ** 2) + ) pix, val = pix[inside], val[inside] lo, hi = _robust_limits(val, symmetric) - norm = (TwoSlopeNorm(0, lo, hi) if symmetric - else mpl.colors.Normalize(lo, hi)) + norm = ( + TwoSlopeNorm(0, lo, hi) if symmetric else mpl.colors.Normalize(lo, hi) + ) axes = map_grid(fig, cell, parts) for ax, part in zip(axes, parts): - im = draw_map(ax, part, pix, val, nside, - DIVERGING if symmetric else SEQUENTIAL, norm, - labels=False, grid_color="#ddd2bb") + im = draw_map( + ax, + part, + pix, + val, + nside, + DIVERGING if symmetric else SEQUENTIAL, + norm, + labels=False, + grid_color="#ddd2bb", + ) _colorbar(fig, im, axes, label, symmetric) return S.save(fig, out, "full" if layout == "wide" else "half") diff --git a/src/sp_validation/dashboard/style.py b/src/sp_validation/dashboard/style.py index 5b088a9c2..685c3e9ca 100644 --- a/src/sp_validation/dashboard/style.py +++ b/src/sp_validation/dashboard/style.py @@ -153,8 +153,15 @@ def note(ax, text, loc="lower right", color=INK_MUTED, fontsize=8): x, ha = (0.04, "left") if "left" in loc else (0.96, "right") y, va = (0.95, "top") if "upper" in loc else (0.05, "bottom") ax.text( - x, y, text, transform=ax.transAxes, ha=ha, va=va, color=color, - fontsize=fontsize, zorder=10, + x, + y, + text, + transform=ax.transAxes, + ha=ha, + va=va, + color=color, + fontsize=fontsize, + zorder=10, ) @@ -163,8 +170,14 @@ def blind_stamp(fig, blind, where="top"): y, va = (1.0, "top") if where == "top" else (0.0, "bottom") x, ha = (1.0, "right") if where == "top" else (0.0, "left") fig.text( - x, y, f"BLINDED · {blind}", ha=ha, va=va, fontsize=6.5, - family="monospace", color=CINNABAR, + x, + y, + f"BLINDED · {blind}", + ha=ha, + va=va, + fontsize=6.5, + family="monospace", + color=CINNABAR, ) diff --git a/src/sp_validation/dashboard/versions.py b/src/sp_validation/dashboard/versions.py index 94e9a22b4..71deed552 100644 --- a/src/sp_validation/dashboard/versions.py +++ b/src/sp_validation/dashboard/versions.py @@ -29,15 +29,34 @@ def _boxes(ax, x_left, x_right, values, ref): """Box spanning ``values`` (versions x bins) per bin, bar at ``ref``.""" lo, hi = values.min(0), values.max(0) for xl, xr, a, b, r in zip(x_left, x_right, lo, hi, ref): - ax.add_patch(Rectangle((xl, a), xr - xl, b - a, fill=False, - edgecolor=S.INK, lw=0.8, alpha=0.75, zorder=1)) + ax.add_patch( + Rectangle( + (xl, a), + xr - xl, + b - a, + fill=False, + edgecolor=S.INK, + lw=0.8, + alpha=0.75, + zorder=1, + ) + ) ax.hlines(r, xl, xr, color=S.INK, lw=1.3, zorder=1) def _series(ax, x, y, yerr, i, color, label=None): - ax.errorbar(x, y, yerr=yerr, marker=S.VERSION_MARKERS[i % 4], color=color, - markerfacecolor="none", markeredgecolor=color, label=label, - zorder=2, **S.DATA_VECTOR_MARK) + ax.errorbar( + x, + y, + yerr=yerr, + marker=S.VERSION_MARKERS[i % 4], + color=color, + markerfacecolor="none", + markeredgecolor=color, + label=label, + zorder=2, + **S.DATA_VECTOR_MARK, + ) def _log_offsets(n, spread=0.05): @@ -55,19 +74,32 @@ def pure_eb(datasets, out, scale_cut): start, stop = bins_from_scale_cut(ref["left_edges"], ref["right_edges"], scale_cut) shade = (ref["left_edges"][start], ref["right_edges"][stop - 1]) with S.figure_style(): - fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.0), sharex=True, - height_ratios=(3, 2)) + fig, axes = plt.subplots( + 2, 2, figsize=(7.2, 4.0), sharex=True, height_ratios=(3, 2) + ) for col, xi in enumerate(("xip", "xim")): top = np.array([theta * d[xi] / XI_SCALE for _, d in datasets]) bot = np.array([d[f"{xi}_B"] / d[f"sigma_{xi}_B"] for _, d in datasets]) _boxes(axes[0, col], theta / pad, theta * pad, top, top[0]) _boxes(axes[1, col], theta / pad, theta * pad, bot, bot[0]) for i, (label, d) in enumerate(datasets): - _series(axes[0, col], theta * off[i], top[i], - theta * d[f"sigma_{xi}"] / XI_SCALE, i, colors[i], - label if col == 0 else None) - _series(axes[1, col], theta * off[i], bot[i], np.ones_like(theta), - i, colors[i]) + _series( + axes[0, col], + theta * off[i], + top[i], + theta * d[f"sigma_{xi}"] / XI_SCALE, + i, + colors[i], + label if col == 0 else None, + ) + _series( + axes[1, col], + theta * off[i], + bot[i], + np.ones_like(theta), + i, + colors[i], + ) sign = "+" if xi == "xip" else "-" S.label_box(axes[0, col], rf"$\xi_{sign}$") S.zero_line(axes[1, col]) @@ -98,16 +130,29 @@ def cosebis(datasets, out, full_cut=(1.0, 250.0)): with S.figure_style(): fig, axes = plt.subplots(2, 1, figsize=(7.2, 3.6), sharex=True, sharey=True) for ax, cut in zip(axes, (full_cut, fid)): - vals = np.array([d["cuts"][cut]["Bn"] / d["cuts"][cut]["sigma_B"] - for _, d in datasets]) + vals = np.array( + [d["cuts"][cut]["Bn"] / d["cuts"][cut]["sigma_B"] for _, d in datasets] + ) _boxes(ax, modes - half, modes + half, vals, vals[0]) for i, (label, _) in enumerate(datasets): - _series(ax, modes + offsets[i], vals[i], np.ones(nmodes), i, - colors[i], label if cut == full_cut else None) + _series( + ax, + modes + offsets[i], + vals[i], + np.ones(nmodes), + i, + colors[i], + label if cut == full_cut else None, + ) S.zero_line(ax) ax.set_ylabel(r"$B_n / \sigma_n$") - ax.set_title(rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", loc="right", - fontsize=8.5, color=S.INK_MUTED, pad=3) + ax.set_title( + rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", + loc="right", + fontsize=8.5, + color=S.INK_MUTED, + pad=3, + ) axes[-1].set_xlim(0.4, nmodes + 0.6) axes[-1].set_xticks(modes) axes[-1].set_xlabel(r"COSEBI mode $n$") @@ -131,8 +176,15 @@ def pseudo_cl(datasets, out, ell_cut): vals = np.array([d[key] / d[f"sigma_{key}"] for _, d in datasets]) _boxes(ax, ell - half, ell + half, vals, vals[0]) for i, (label, _) in enumerate(datasets): - _series(ax, ell + offsets[i] * width, vals[i], np.ones_like(ell), i, - colors[i], label if key == "BB" else None) + _series( + ax, + ell + offsets[i] * width, + vals[i], + np.ones_like(ell), + i, + colors[i], + label if key == "BB" else None, + ) S.zero_line(ax) ax.set_xscale("function", functions=(np.sqrt, np.square)) ax.set_xlim(ell.min() * 0.85, ell.max() * 1.04) diff --git a/src/sp_validation/dashboard/weights.py b/src/sp_validation/dashboard/weights.py index cdc5b21e0..38d27d37e 100644 --- a/src/sp_validation/dashboard/weights.py +++ b/src/sp_validation/dashboard/weights.py @@ -44,11 +44,14 @@ def weights(cells, out): with S.figure_style(): fig, ax = plt.subplot_mosaic( [["top", ".", "spec"], ["grid", "side", "spec"]], - figsize=(7.2, 3.5), width_ratios=(4, 0.7, 4.2), height_ratios=(0.8, 4), + figsize=(7.2, 3.5), + width_ratios=(4, 0.7, 4.2), + height_ratios=(0.8, 4), ) g = ax["grid"] - mesh = g.pcolormesh(snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, - linewidth=0.25) + mesh = g.pcolormesh( + snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, linewidth=0.25 + ) g.set_xscale("log") g.set_yscale("log") g.set_xlim(snr[0], snr[-1]) @@ -56,32 +59,46 @@ def weights(cells, out): g.set_xlabel("signal-to-noise") g.set_ylabel(r"size ratio $T_{\rm gal} / T_{\rm PSF}$") g.xaxis.set_major_formatter(log_fmt) - g.xaxis.set_minor_formatter(mpl.ticker.FuncFormatter( - lambda v, _: f"${v:g}$" if v in (30, 300) else "")) + g.xaxis.set_minor_formatter( + mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$" if v in (30, 300) else "") + ) g.yaxis.set_major_formatter(log_fmt) - g.yaxis.set_minor_formatter(mpl.ticker.FuncFormatter( - lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2) else "")) + g.yaxis.set_minor_formatter( + mpl.ticker.FuncFormatter( + lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2) else "" + ) + ) g.tick_params(which="both", top=False, right=False) - cbar = fig.colorbar(mesh, ax=g, location="bottom", aspect=30, pad=0.02, - fraction=0.06) + cbar = fig.colorbar( + mesh, ax=g, location="bottom", aspect=30, pad=0.02, fraction=0.06 + ) cbar.set_label(r"cell weight $w = R^2 / \sigma_e^2$") cbar.outline.set_linewidth(0.5) # Marginal shares of galaxies along each grid axis. bar = dict(color=S.NEUTRAL_LIGHT, edgecolor=S.RAG, linewidth=0.3) - ax["top"].bar(snr[:-1], 100 * frac.sum(1), width=np.diff(snr), align="edge", - **bar) + ax["top"].bar( + snr[:-1], 100 * frac.sum(1), width=np.diff(snr), align="edge", **bar + ) ax["top"].set_xscale("log") ax["top"].set_xlim(g.get_xlim()) - ax["side"].barh(size[:-1], 100 * frac.sum(0), height=np.diff(size), - align="edge", **bar) + ax["side"].barh( + size[:-1], 100 * frac.sum(0), height=np.diff(size), align="edge", **bar + ) ax["side"].set_yscale("log") ax["side"].set_ylim(g.get_ylim()) for name, axis in (("top", "x"), ("side", "y")): m = ax[name] - m.tick_params(axis=axis, which="both", labelbottom=False, - labelleft=False, bottom=False, left=False, top=False, - right=False) + m.tick_params( + axis=axis, + which="both", + labelbottom=False, + labelleft=False, + bottom=False, + left=False, + top=False, + right=False, + ) for side in ("top", "right"): m.spines[side].set_visible(False) m.tick_params(which="both", top=False, right=False, labelsize=7) @@ -97,6 +114,7 @@ def weights(cells, out): spec.set_ylim(0, None) spec.set_xlim(0, values.max() * 1.03) spec.tick_params(which="both", top=False, right=False) - spec.set_title(f"{len(values)} distinct values, one per cell", loc="left", - fontsize=9) + spec.set_title( + f"{len(values)} distinct values, one per cell", loc="left", fontsize=9 + ) return S.save(fig, out, "full") diff --git a/src/sp_validation/tests/test_dashboard_figures.py b/src/sp_validation/tests/test_dashboard_figures.py index 9e685f464..ba49db161 100644 --- a/src/sp_validation/tests/test_dashboard_figures.py +++ b/src/sp_validation/tests/test_dashboard_figures.py @@ -33,19 +33,26 @@ def test_zenithal_round_trip(rotation): def test_zenithal_ra_increases_to_the_left(): - x, _ = Zenithal(200.0, 40.0).forward(np.array([199.0, 201.0]), np.array([40.0, 40.0])) + x, _ = Zenithal(200.0, 40.0).forward( + np.array([199.0, 201.0]), np.array([40.0, 40.0]) + ) assert x[1] < x[0] def test_footprint_parts_separates_patch_and_strip(): nside = 256 - patch = hp.query_disc(nside, hp.ang2vec(220, 55, lonlat=True), np.radians(10), - nest=True) + patch = hp.query_disc( + nside, hp.ang2vec(220, 55, lonlat=True), np.radians(10), nest=True + ) ra = np.mod(np.linspace(-30, 60, 4000), 360) - strip = np.unique(np.concatenate([ - hp.ang2pix(nside, ra, np.full_like(ra, d), lonlat=True, nest=True) - for d in np.arange(30, 34, 0.2) - ])) + strip = np.unique( + np.concatenate( + [ + hp.ang2pix(nside, ra, np.full_like(ra, d), lonlat=True, nest=True) + for d in np.arange(30, 34, 0.2) + ] + ) + ) parts = footprint_parts(np.concatenate([patch, strip]), nside) assert len(parts) == 2 assert parts[0].sum() == len(patch) From ddb348821c62df26f0f97f9cdb6c73a4b6a04338 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Mon, 26 Oct 2026 19:28:05 -0400 Subject: [PATCH 21/53] feat(dashboard): four-section page, Paper II PTE table, C_ell naming, weight heatmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sections: data vectors (xi± and l*C_l), B modes (PTE table, the Paper II pure E/B, COSEBIs and C_l BB/EB figures, folded scale-cut maps and version comparisons), PSF leakage, and weights/footprint/covariances. ptes.py builds Paper II's PTE table: fiducial and full-range rows; COSEBIs n <= 6 and n <= 20, pure-B xi+, xi-, joint, and C_l BB columns. The B-mode figures return to Paper II's layouts with n > 6 shaded for COSEBIs. The weight figure is the cell heatmap with the catalogue columns on its axes. Shared header and colour-bar helpers carry one theme across all figures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011rD7z3XSA9PmjQyPpNcG49 --- src/sp_validation/dashboard/covariance.py | 8 +- src/sp_validation/dashboard/data_vectors.py | 301 ++++++++-------- src/sp_validation/dashboard/products.py | 5 +- src/sp_validation/dashboard/psf.py | 6 +- src/sp_validation/dashboard/ptes.py | 82 +++++ src/sp_validation/dashboard/render.py | 368 +++++++++++--------- src/sp_validation/dashboard/scale_cuts.py | 4 +- src/sp_validation/dashboard/sky.py | 9 +- src/sp_validation/dashboard/style.py | 37 +- src/sp_validation/dashboard/versions.py | 10 +- src/sp_validation/dashboard/weights.py | 113 +++--- 11 files changed, 526 insertions(+), 417 deletions(-) create mode 100644 src/sp_validation/dashboard/ptes.py diff --git a/src/sp_validation/dashboard/covariance.py b/src/sp_validation/dashboard/covariance.py index 36a088b3c..016b60c16 100644 --- a/src/sp_validation/dashboard/covariance.py +++ b/src/sp_validation/dashboard/covariance.py @@ -41,11 +41,11 @@ def correlation(cov, blocks, out, label): ax.set_yticks(centres) ax.set_yticklabels(names, fontsize=8.5) ax.tick_params(which="both", length=0, top=False, right=False) - ax.set_title(label, loc="left", fontsize=9.5) - cbar = fig.colorbar(image, ax=ax, shrink=0.92, aspect=22, pad=0.03) + S.header(ax, label) + cbar = S.colorbar( + fig, image, ax, "correlation coefficient", shrink=0.92, aspect=22, pad=0.03 + ) cbar.set_ticks([-1, -0.5, 0, 0.5, 1]) - cbar.outline.set_linewidth(0.6) - cbar.set_label("correlation coefficient") return S.save(fig, out, "half") diff --git a/src/sp_validation/dashboard/data_vectors.py b/src/sp_validation/dashboard/data_vectors.py index 15153bdc9..36db37f92 100644 --- a/src/sp_validation/dashboard/data_vectors.py +++ b/src/sp_validation/dashboard/data_vectors.py @@ -1,51 +1,62 @@ -"""The correlation-function hero figure and the three B-mode data vectors. +"""Data-vector figures: the ξ± and C_ℓ two-point functions, and the three +B-mode data vectors as Paper II draws them. Each function draws one figure from loaded products (see :mod:`sp_validation.dashboard.products`) and writes it to ``out``. The B-mode -figures follow Paper II's single-data-vector figures (pure E/B Fig. 2, -COSEBIs Fig. 3, pseudo-C_ell Fig. 4): same estimator-family hues, marker -fills, normalisation and grey shading of excluded scales. For the dashboard's -row of three they share one layout -- two stacked panels on a common x axis --- and carry their null-test PTEs in the panel headers. +figures reproduce Paper II's single-data-vector figures -- pure E/B (Fig. 2), +COSEBIs (Fig. 3), C_ℓ BB/EB (Fig. 4) -- with the same estimator-family hues, +marker fills, normalisation, and grey shading of what lies outside the +fiducial selection; their PTEs live in the PTE table, not in the figures. +``layout="narrow"`` stacks multi-panel figures for phones. """ import matplotlib.pyplot as plt import numpy as np from matplotlib.lines import Line2D -from scipy import stats from sp_validation.b_modes import bins_from_scale_cut from . import style as S XI_SCALE = 1e-4 -THIRD_HEIGHT = 3.3 -def _theta_ticks(ax): - ax.set_xticks([1, 10, 100]) - ax.set_xticklabels(["$1$", "$10$", "$100$"]) +def _legend(fig, handles, ncol): + fig.legend( + handles=handles, + loc="outside upper center", + ncol=ncol, + handletextpad=0.2, + columnspacing=1.0, + borderaxespad=0.2, + ) -def _pte_header(ax, text): - ax.set_title(text, loc="right", fontsize=8.5, color=S.INK_MUTED, pad=3) +def _marker(marker, color, label, filled=True, alpha=1.0): + return Line2D( + [], + [], + ls="none", + marker=marker, + color=color, + alpha=alpha, + markerfacecolor=color if filled else "none", + markeredgecolor=color, + markersize=4, + markeredgewidth=0.8, + label=label, + ) def xi_pm(xi, out, layout="wide", blind=None): - """Blinded reporting-grid ξ± (θξ × 10⁴) with analytic error bars. - - ``layout="wide"`` puts ξ+ and ξ− side by side for the page column; - ``"narrow"`` stacks them for phones. - """ + """Blinded reporting-grid θξ± with analytic error bars.""" with S.figure_style(): if layout == "wide": fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), sharey=True) - size = "full" else: fig, axes = plt.subplots(2, 1, figsize=(3.6, 4.6), sharex=True) - size = "half" theta = xi["theta"] - for ax, key, label in zip(axes, ("xip", "xim"), (r"$\xi_+$", r"$\xi_-$")): + for ax, key, sign in zip(axes, ("xip", "xim"), "+-"): S.zero_line(ax) ax.errorbar( theta, @@ -53,16 +64,15 @@ def xi_pm(xi, out, layout="wide", blind=None): yerr=theta * xi[f"sigma_{key}"] / XI_SCALE, marker="o", color=S.COLOR_TOTAL, - markerfacecolor=S.COLOR_TOTAL, **{**S.DATA_VECTOR_MARK, "markersize": 3.8}, ) S.theta_axis(ax) - _theta_ticks(ax) - S.label_box(ax, label, loc="upper left", fontsize=11) + S.theta_ticks(ax) + S.label_box(ax, rf"$\xi_{sign}$") for ax in axes if layout == "wide" else axes[-1:]: ax.set_xlabel(r"$\theta$ [arcmin]") for ax in axes[:1] if layout == "wide" else axes: - ax.set_ylabel(r"$\theta\,\xi \times 10^{4}$") + ax.set_ylabel(r"$\theta\,\xi_\pm \times 10^{4}$") lo = min(np.min(theta * (xi[k] - xi[f"sigma_{k}"])) for k in ("xip", "xim")) hi = max(np.max(theta * (xi[k] + xi[f"sigma_{k}"])) for k in ("xip", "xim")) pad = 0.08 * (hi - lo) @@ -70,115 +80,138 @@ def xi_pm(xi, out, layout="wide", blind=None): if blind: fig.suptitle(" ", fontsize=7) S.blind_stamp(fig, blind) - return S.save(fig, out, size) + return S.save(fig, out, "full" if layout == "wide" else "half") -def _jitter(theta, i, n, spread=0.07): - return theta * np.exp(spread * (i - (n - 1) / 2)) +def _cl_cut_mask(cl, ell_cut): + return (cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1]) + +def _cl_shade(cl, ell_cut): + mask = _cl_cut_mask(cl, ell_cut) + return cl["ell_lo"][mask][0], cl["ell_hi"][mask][-1] -def pure_eb(pe, out, scale_cut, ptes, blind=None): - """Pure E / B / ambiguous decomposition of ξ± (Paper II Fig. 2), stacked.""" + +def ell_cl(cl, out, ell_cut, layout="wide", blind=None): + """ℓ C_ℓ for EE, BB and EB on a linear ℓ axis (EE blinded).""" + ell = cl["ell"] series = [ - ("", "o", S.COLOR_TOTAL, 1.0, True, "total"), - ("_E", "s", S.COLOR_E, 0.5, True, "$E$"), - ("_B", "X", S.COLOR_B, 1.0, True, "$B$"), - ("_amb", "v", S.COLOR_AMB, 0.55, True, "amb."), + ("EE", "o", True, S.COLOR_TOTAL, r"$EE$"), + ("BB", "o", True, S.COLOR_CL_BB, r"$BB$"), + ("EB", "D", False, S.COLOR_CL_EB, r"$EB$"), ] + top = np.max(ell * (cl["EE"] + cl["sigma_EE"])) + p = int(np.floor(np.log10(top))) + scale = 10.0**p + size = (7.2, 2.7) if layout == "wide" else (3.6, 2.7) with S.figure_style(): - fig, axes = plt.subplots(2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True) - theta = pe["theta"] - start, stop = bins_from_scale_cut( - pe["left_edges"], pe["right_edges"], scale_cut - ) - shade = (pe["left_edges"][start], pe["right_edges"][stop - 1]) - values = [] + fig, ax = plt.subplots(figsize=size) + S.zero_line(ax) + bandwidth = cl["ell_hi"] - cl["ell_lo"] + 1 + for (key, marker, filled, color, _), off in zip(series, (-0.18, 0.0, 0.18)): + ax.errorbar( + ell + off * bandwidth, + ell * cl[key] / scale, + yerr=ell * cl[f"sigma_{key}"] / scale, + marker=marker, + color=color, + markerfacecolor=color if filled else "none", + markeredgecolor=color, + **S.DATA_VECTOR_MARK, + ) + ax.set_xlim(0, cl["ell_hi"][-1] + 30) + S.shade_outside(ax, *_cl_shade(cl, ell_cut)) + ax.set_xlabel(r"$\ell$") + ax.set_ylabel(rf"$\ell\, C_\ell \times 10^{{{-p}}}$") + S.label_box(ax, r"$C_\ell$") + _legend(fig, [_marker(m, c, lab, f) for _, m, f, c, lab in series], 3) + if blind: + S.blind_stamp(fig, blind, where="bottom") + return S.save(fig, out, "full" if layout == "wide" else "half") + + +def _jitter(theta, i, n, spread=0.07): + return theta * np.exp(spread * (i - (n - 1) / 2)) + + +PURE_EB_SERIES = [ + ("", "o", S.COLOR_TOTAL, 1.0, r"$\xi_\pm$ (total)"), + ("_E", "s", S.COLOR_E, 0.5, r"$\xi_\pm^E$"), + ("_B", "X", S.COLOR_B, 1.0, r"$\xi_\pm^B$"), + ("_amb", "v", S.COLOR_AMB, 0.55, r"$\xi_\pm^{\rm amb}$"), +] + + +def pure_eb(pe, out, scale_cut, layout="wide", blind=None): + """Pure E / B / ambiguous decomposition of θξ± (Paper II Fig. 2).""" + theta = pe["theta"] + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], scale_cut) + shade = (pe["left_edges"][start], pe["right_edges"][stop - 1]) + with S.figure_style(): + if layout == "wide": + fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.7), sharey=True) + else: + fig, axes = plt.subplots(2, 1, figsize=(3.6, 4.6), sharex=True, sharey=True) + bounds = [] for ax, xi, sign in zip(axes, ("xip", "xim"), "+-"): S.zero_line(ax) - for i, (suffix, marker, color, alpha, filled, _) in enumerate(series): + for i, (suffix, marker, color, alpha, _) in enumerate(PURE_EB_SERIES): y = theta * pe[f"{xi}{suffix}"] / XI_SCALE dy = theta * pe[f"sigma_{xi}{suffix}"] / XI_SCALE - values.append((y - dy, y + dy)) + bounds.append((y - dy)[start:stop].min()) + bounds.append((y + dy)[start:stop].max()) ax.errorbar( - _jitter(theta, i, len(series)), + _jitter(theta, i, len(PURE_EB_SERIES)), y, yerr=dy, marker=marker, color=color, alpha=alpha, - markerfacecolor=color if filled else "none", **S.DATA_VECTOR_MARK, ) S.theta_axis(ax) - _theta_ticks(ax) + S.theta_ticks(ax) S.shade_outside(ax, *shade) + S.label_box(ax, rf"$\xi_{sign}$") + for ax in axes if layout == "wide" else axes[-1:]: + ax.set_xlabel(r"$\theta$ [arcmin]") + for ax in axes[:1] if layout == "wide" else axes: ax.set_ylabel(r"$\theta\,\xi \times 10^{4}$") - S.label_box(ax, rf"$\xi_{sign}$", loc="upper left") - _pte_header(ax, rf"$\xi_{sign}^B$ PTE ${S.fmt_pte(ptes.get(xi + '_B'))}$") - axes[-1].set_xlabel(r"$\theta$ [arcmin]") - # Shared y range from the in-cut points, so the excluded large-scale - # ambiguous modes don't set the scale. - inside = np.zeros(len(theta), bool) - inside[start:stop] = True - lo = min(v[0][inside].min() for v in values) - hi = max(v[1][inside].max() for v in values) + # The in-cut points set the range, as in Paper II. + lo, hi = min(bounds), max(bounds) pad = 0.12 * (hi - lo) - for ax in axes: - ax.set_ylim(lo - pad, hi + 2.5 * pad) - handles = [ - Line2D( - [], [], ls="none", marker=m, color=c, alpha=a, markersize=4, label=lab - ) - for _, m, c, a, _, lab in series - ] - fig.legend( - handles=handles, - loc="outside upper center", - ncol=4, - handletextpad=0.1, - columnspacing=0.9, - borderaxespad=0.1, + axes[0].set_ylim(lo - pad, hi + 2.2 * pad) + _legend( + fig, [_marker(m, c, lab, alpha=a) for _, m, c, a, lab in PURE_EB_SERIES], 4 ) - if "combined" in ptes: - fig.text( - 0.99, - 0.003, - rf"joint $\xi_\pm^B$ PTE " - rf"${S.fmt_pte(ptes['combined'])}$", - ha="right", - va="bottom", - fontsize=8, - color=S.INK_MUTED, - ) if blind: S.blind_stamp(fig, blind, where="bottom") - return S.save(fig, out, "third") + return S.save(fig, out, "full" if layout == "wide" else "half") -def cosebis(co, out, full_cut=(1.0, 250.0)): +def cosebis(co, out, full_cut=(1.0, 250.0), n_subset=6): """B-mode COSEBIs in units of their uncertainty (Paper II Fig. 3). - Fiducial cuts (filled, COSEBIs purple) over all scales (open, light tint), - each in its own panel. + Fiducial cuts filled in the COSEBIs purple, the full range open in its + tint; modes beyond the ``n <= n_subset`` subset are shaded, the way scales + outside the fiducial cuts are shaded elsewhere. """ fid = co["fiducial"] rows = [ - (fid, "o", True, S.COLOR_COSEBIS), - (full_cut, "D", False, S.COLOR_COSEBIS_ALL), + (fid, -0.15, "o", True, S.COLOR_COSEBIS, "fiducial"), + (full_cut, 0.15, "D", False, S.COLOR_COSEBIS_ALL, "full range"), ] n = np.arange(1, co["nmodes"] + 1) with S.figure_style(): - fig, axes = plt.subplots( - 2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True - ) + fig, ax = plt.subplots(figsize=(3.6, 2.45)) extent = 0 - for ax, (cut, marker, filled, color) in zip(axes, rows): + handles = [] + for cut, off, marker, filled, color, name in rows: r = co["cuts"][cut] y = r["Bn"] / r["sigma_B"] extent = max(extent, np.max(np.abs(y)) + 1) - S.zero_line(ax) ax.errorbar( - n, + n + off, y, yerr=np.ones_like(y), marker=marker, @@ -187,48 +220,25 @@ def cosebis(co, out, full_cut=(1.0, 250.0)): markeredgecolor=color, **S.DATA_VECTOR_MARK, ) - ax.set_ylabel(r"$B_n / \sigma_n$") - S.label_box( - ax, - rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", - loc="upper left", - fontsize=8.5, + handles.append( + _marker(marker, color, rf"{name} ${cut[0]:g}'\!-\!{cut[1]:g}'$", filled) ) - _pte_header(ax, rf"$B_n$ PTE ${S.fmt_pte(r['pte_B'])}$") - lim = np.ceil(extent + 0.6) - axes[0].set_ylim(-lim, lim + 1.2) - axes[0].yaxis.set_major_locator(plt.MultipleLocator(2)) - axes[-1].set_xlim(0.4, n[-1] + 0.6) - axes[-1].set_xticks(n[::2] if len(n) > 12 else n) - axes[-1].set_xlabel(r"COSEBI mode $n$") - _spacer_legend(fig) - return S.save(fig, out, "third") + S.zero_line(ax) + ax.set_xlim(0.4, n[-1] + 0.6) + ax.axvspan(n_subset + 0.5, n[-1] + 0.6, **S.EXCLUDED_SHADE) + ax.set_xticks(n) + ax.set_xticklabels([f"${i}$" if i % 2 == 0 else "" for i in n]) + ax.set_xlabel(r"COSEBI mode $n$") + ax.set_ylabel(r"$B_n / \sigma_n$") + lim = np.ceil(extent + 0.4) + ax.set_ylim(-lim, lim) + ax.yaxis.set_major_locator(plt.MultipleLocator(2)) + S.label_box(ax, "COSEBIs") + _legend(fig, handles, 2) + return S.save(fig, out, "half") -def _spacer_legend(fig): - """Reserve the legend band the pure E/B figure uses, keeping rows aligned.""" - fig.legend( - handles=[Line2D([], [], ls="none", label=" ")], - loc="outside upper center", - handlelength=0, - ) - - -def _cl_cut_mask(cl, ell_cut): - return (cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1]) - - -def cl_pte(cl, key, ell_cut=None): - """χ² PTE of one spectrum against zero, over bins inside ``ell_cut``.""" - mask = ( - np.ones(len(cl["ell"]), bool) if ell_cut is None else _cl_cut_mask(cl, ell_cut) - ) - d = cl[key][mask] - cov = cl["cov"][f"{key}_{key}"][np.ix_(mask, mask)] - return float(stats.chi2.sf(d @ np.linalg.solve(cov, d), mask.sum())) - - -def pseudo_cl(cl, out, ell_cut): +def cl_bmodes(cl, out, ell_cut): """C_ℓ^BB and C_ℓ^EB in units of their uncertainty (Paper II Fig. 4).""" rows = [ ("BB", "o", True, S.COLOR_CL_BB, r"$C_\ell^{BB}$"), @@ -236,9 +246,7 @@ def pseudo_cl(cl, out, ell_cut): ] ell = cl["ell"] with S.figure_style(): - fig, axes = plt.subplots( - 2, 1, figsize=(3.0, THIRD_HEIGHT), sharex=True, sharey=True - ) + fig, axes = plt.subplots(2, 1, figsize=(3.6, 2.9), sharex=True, sharey=True) extent = 0 for ax, (key, marker, filled, color, label) in zip(axes, rows): y = cl[key] / cl[f"sigma_{key}"] @@ -255,25 +263,16 @@ def pseudo_cl(cl, out, ell_cut): **S.DATA_VECTOR_MARK, ) ax.set_xscale("function", functions=(np.sqrt, np.square)) - ax.set_ylabel(r"$C_\ell / \sigma$") - S.label_box(ax, label, loc="upper left") - _pte_header( - ax, - rf"PTE ${S.fmt_pte(cl_pte(cl, key, ell_cut))}$" - rf" (all $\ell$: ${S.fmt_pte(cl_pte(cl, key))}$)", - ) - mask = _cl_cut_mask(cl, ell_cut) - lo = cl["ell_lo"][mask][0] - hi = cl["ell_hi"][mask][-1] + ax.set_ylabel(rf"{label}$/\sigma$") + S.label_box(ax, label) for ax in axes: - ax.set_xlim(ell.min() * 0.85, ell.max() * 1.04) - S.shade_outside(ax, lo, hi) + ax.set_xlim(ell.min() * 0.85, cl["ell_hi"][-1] * 1.02) + S.shade_outside(ax, *_cl_shade(cl, ell_cut)) ax.set_xticks([100, 400, 900, 1600]) ax.set_xticklabels(["$100$", "$400$", "$900$", "$1600$"]) ax.set_xticks([i * 100 for i in range(1, 21)], minor=True) - lim = np.ceil(extent + 0.6) - axes[0].set_ylim(-lim, lim + 1.2) + lim = np.ceil(extent + 0.4) + axes[0].set_ylim(-lim, lim) axes[0].yaxis.set_major_locator(plt.MultipleLocator(2)) axes[-1].set_xlabel(r"$\ell$") - _spacer_legend(fig) - return S.save(fig, out, "third") + return S.save(fig, out, "half") diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index 80e9d3dbf..dfea896b1 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -60,7 +60,8 @@ def cosebis(self): return _one(self.cv / f"{self.version}_cosebis_minsep=*_data.npz") @property - def pseudo_cl_cov(self): + def cl_cov(self): + """NaMaster covariance of the C_ell band powers.""" prefix = f"pseudo_cl_cov_{self.version}_" binning = re.compile(r"(powspace|logspace|linear)_nbins=\d+\.fits") matches = [ @@ -208,7 +209,7 @@ def powspace_edges(ell_eff, lmin=None, lmax=2048, power=0.5): return lo, hi -def load_pseudo_cl(sacc_path, cov_path, blind): +def load_cl(sacc_path, cov_path, blind): """Band powers, their multipole edges, and the NaMaster covariance blocks.""" from astropy.io import fits diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py index b4cfb2a03..efee35c08 100644 --- a/src/sp_validation/dashboard/psf.py +++ b/src/sp_validation/dashboard/psf.py @@ -232,12 +232,10 @@ def rho_stats(rt, out): ax.set_xticklabels(["$1$", "$10$", "$100$"]) ax.set_xlabel(r"$\theta$ [arcmin]") ax.legend(loc="upper right", ncol=3, handletextpad=0.2, columnspacing=0.8) - ax.set_title(label, loc="left", fontsize=9) + S.header(ax, label) axes[0].set_ylabel(r"$|\rho_{k,+}(\theta)|$") axes[0].set_ylim(top=axes[0].get_ylim()[1] * 8) - axes[1].set_title( - r"open markers: $\rho < 0$", loc="right", fontsize=8.5, color=S.INK_MUTED - ) + S.header(axes[1], r"open markers: $\rho < 0$", side="right") return S.save(fig, out, "full") diff --git a/src/sp_validation/dashboard/ptes.py b/src/sp_validation/dashboard/ptes.py new file mode 100644 index 000000000..61f07cc10 --- /dev/null +++ b/src/sp_validation/dashboard/ptes.py @@ -0,0 +1,82 @@ +"""B-mode PTE table in the layout of Paper II's PTE table. + +One row per catalogue version and scale selection (fiducial, full range); +columns COSEBIs B_n for n <= 6 and n <= 20, pure-B ξ+, ξ− and joint ξ±, and +C_ℓ^BB. Pure E/B PTEs come from the cached scale-cut matrices and the +COSEBIs n <= 20 PTE from the cached transform; the COSEBIs n <= 6 and C_ℓ^BB +PTEs are the same χ² against zero over the corresponding subset of the cached +data vector and covariance (no Hartlap factor: both covariances are +analytic). +""" + +import numpy as np +from scipy import stats + +from sp_validation.b_modes import bins_from_scale_cut + +# Column labels as (symbol, subscript, superscript, qualifier) so the page +# can typeset them with /. +COLUMNS = [ + {"key": "cosebis_n6", "group": "COSEBIs", "label": ["B", "n", "", "n ≤ 6"]}, + {"key": "cosebis_n20", "group": "COSEBIs", "label": ["B", "n", "", "n ≤ 20"]}, + {"key": "xip_B", "group": "Pure E/B", "label": ["ξ", "+", "B", ""]}, + {"key": "xim_B", "group": "Pure E/B", "label": ["ξ", "−", "B", ""]}, + {"key": "combined", "group": "Pure E/B", "label": ["ξ", "±", "B", "joint"]}, + {"key": "cl_BB", "group": "Cℓ", "label": ["C", "ℓ", "BB", ""]}, +] + + +def chi2_pte(data, cov): + return float(stats.chi2.sf(data @ np.linalg.solve(cov, data), len(data))) + + +def cosebis_pte(cut, nmodes): + """B_n PTE over the first ``nmodes`` modes of one cached cut.""" + n = len(cut["Bn"]) + block = cut["cov"][n:, n:] + return chi2_pte(cut["Bn"][:nmodes], block[:nmodes, :nmodes]) + + +def cl_pte(cl, key="BB", ell_cut=None): + """C_ℓ PTE over bands lying wholly inside ``ell_cut`` (all bands if None).""" + mask = np.ones(len(cl["ell"]), bool) + if ell_cut is not None: + mask = (cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1]) + return chi2_pte(cl[key][mask], cl["cov"][f"{key}_{key}"][np.ix_(mask, mask)]) + + +def rows(pe, co, cl, theta_cut, ell_cut, full_cut): + """The fiducial and full-range rows for one version.""" + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], theta_cut) + n_theta = len(pe["theta"]) + selections = [ + ("Fiducial", (start, stop - 1), co["fiducial"], ell_cut), + ("Full range", (0, n_theta - 1), full_cut, None), + ] + out = [] + for name, (i, j), co_cut, l_cut in selections: + cut = co["cuts"][co_cut] + values = { + "cosebis_n6": cosebis_pte(cut, 6), + "cosebis_n20": cosebis_pte(cut, co["nmodes"]), + **{ + k: float(pe["pte_matrices"][k][i, j]) + for k in ("xip_B", "xim_B", "combined") + }, + "cl_BB": cl_pte(cl, "BB", l_cut), + } + ell_range = ( + f"{l_cut[0]} ≤ ℓ ≤ {l_cut[1]}" + if l_cut + else f"{cl['ell_lo'][0]} ≤ ℓ ≤ {cl['ell_hi'][-1]}" + ) + out.append( + { + "selection": name, + "theta": f"{pe['left_edges'][i]:.0f}′–{pe['right_edges'][j]:.0f}′", + "cosebis_theta": f"{co_cut[0]:g}′–{co_cut[1]:g}′", + "ell": ell_range, + "values": values, + } + ) + return out diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index bcfd18698..b83d6218c 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -2,9 +2,10 @@ Reads only cached products (see :mod:`sp_validation.dashboard.products`) and writes the PNGs plus ``figures.json``, the manifest the page generator lays -out: one entry per figure with its section, caption and size class, and the -headline B-mode PTEs. A run with two or more versions also gets the version -comparisons; the first version is the reference for every other figure. +out: the sections in page order, one entry per figure (section, optional +folded group, caption, size class) and the B-mode PTE table. A run with two +or more versions also gets the version comparisons and one pair of PTE rows +per version; the first version is the reference for every other figure. Usage (inside the container):: @@ -23,27 +24,28 @@ from . import data_vectors as DV from . import products as P from . import psf as PSF +from . import ptes, sky from . import scale_cuts as SC -from . import sky from . import versions as V from . import weights as W SECTIONS = [ - ("xi", "Shear correlation functions"), - ("bmodes", "B-mode null tests"), - ("scale_cuts", "PTEs across scale cuts"), - ("covariance", "Covariances"), - ("psf", "PSF leakage"), - ("weights", "Weights"), - ("sky", "Footprint and field maps"), - ("versions", "Catalogue versions"), + ("data_vectors", "Data vectors"), + ("bmodes", "B modes"), + ("leakage", "PSF leakage"), + ("survey", "Weights, footprint and covariances"), ] +GROUPS = { + "scale_cuts": "PTEs across scale cuts", + "versions": "Catalogue versions", + "covariance": "Covariances", +} def run_config(run_dir): """Scale cuts from the run's recorded configuration, when present.""" prov = Path(run_dir) / "provenance.json" - cuts = {"theta": (12.0, 83.0), "ell": (300, 1600), "cosebis_full": (1.0, 250.0)} + cuts = {"theta": (12.0, 83.0), "ell": (300, 1600), "theta_full": (1.0, 250.0)} if prov.exists(): cfg = yaml.safe_load(json.loads(prov.read_text())["config"]) cuts["theta"] = tuple(map(float, cfg["cosmo_val"]["fiducial_scale_cut"])) @@ -51,7 +53,7 @@ def run_config(run_dir): int(cfg["cl"]["fiducial_ell_min"]), int(cfg["cl"]["fiducial_ell_max"]), ) - cuts["cosebis_full"] = ( + cuts["theta_full"] = ( float(cfg["cosebis"]["theta_min"]), float(cfg["cosebis"]["theta_max"]), ) @@ -69,169 +71,173 @@ def __init__(self, out): def path(self, name): return self.out / f"{name}.png" - def add(self, fid, section, caption, size, narrow=False, alt=None): + def add(self, fid, section, caption, size, narrow=False, group=None): entry = { "id": fid, "section": section, + "group": group, "file": f"{fid}.png", "caption": caption, "size": size, - "alt": alt or caption, + "alt": caption, } if narrow: entry["narrow"] = f"{fid}_narrow.png" self.entries.append(entry) +def load_version(run_dir, version, maps_dir): + r = P.RunProducts(run_dir, version, maps_dir) + blind = P.blind_of(r.assembled) + return ( + r, + blind, + { + "pe": P.load_pure_eb(r.pure_eb, blind), + "co": P.load_cosebis(r.cosebis, blind), + "cl": P.load_cl(r.assembled, r.cl_cov, blind), + }, + ) + + def render(run_dir, versions, out, maps_dir=None): cuts = run_config(run_dir) - ref = P.RunProducts(run_dir, versions[0], maps_dir) - blind = P.blind_of(ref.assembled) - figs = Figures(out) + theta_cut, ell_cut, full_cut = cuts["theta"], cuts["ell"], cuts["theta_full"] + loaded = [load_version(run_dir, v, maps_dir) for v in versions] + ref, blind, d = loaded[0] + pe, co, cl = d["pe"], d["co"], d["cl"] xi = P.load_xi(ref.assembled, blind) - pe = P.load_pure_eb(ref.pure_eb, blind) - co = P.load_cosebis(ref.cosebis, blind) - cl = P.load_pseudo_cl(ref.assembled, ref.pseudo_cl_cov, blind) - summary = ( - P.load_json(ref.summary).get(versions[0], {}) if ref.summary.exists() else {} - ) - theta_cut, ell_cut = cuts["theta"], cuts["ell"] - tcut = rf"{theta_cut[0]:g}′–{theta_cut[1]:g}′" + figs = Figures(out) + tcut = f"{theta_cut[0]:g}′–{theta_cut[1]:g}′" + lcut = f"{ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}" - # 1. hero - DV.xi_pm(xi, figs.path("xi_pm"), blind=blind) - DV.xi_pm(xi, figs.path("xi_pm_narrow"), layout="narrow", blind=blind) + # Data vectors + for layout, suffix in (("wide", ""), ("narrow", "_narrow")): + DV.xi_pm(xi, figs.path("xi_pm" + suffix), layout=layout, blind=blind) + DV.ell_cl(cl, figs.path("cl" + suffix), ell_cut, layout=layout, blind=blind) figs.add( "xi_pm", - "xi", - "Blinded ξ± on the 20-bin reporting grid; error bars from the analytic covariance.", + "data_vectors", + "Blinded ξ± on the 20-bin reporting grid; errors from the analytic covariance.", + "full", + narrow=True, + ) + figs.add( + "cl", + "data_vectors", + f"Cℓ (NaMaster, mode-decoupled) as ℓCℓ: EE (blinded), BB and EB. Shaded: outside {lcut}.", "full", narrow=True, ) - # 2. B-mode data vectors - ptes = {k: summary.get(k) for k in ("xip_B", "xim_B", "combined")} - DV.pure_eb(pe, figs.path("bmode_pure_eb"), theta_cut, ptes, blind=blind) + # B modes + DV.pure_eb(pe, figs.path("bmode_pure_eb"), theta_cut, blind=blind) + DV.pure_eb( + pe, figs.path("bmode_pure_eb_narrow"), theta_cut, layout="narrow", blind=blind + ) figs.add( "bmode_pure_eb", "bmodes", - f"Pure-mode decomposition of ξ±; B (crimson) is the null test. Shaded: outside {tcut}.", - "third", + f"Pure-mode decomposition of ξ±. Shaded: outside the fiducial {tcut}; full range 1′–250′.", + "full", + narrow=True, ) - DV.cosebis(co, figs.path("bmode_cosebis"), cuts["cosebis_full"]) + DV.cosebis(co, figs.path("bmode_cosebis"), full_cut) figs.add( "bmode_cosebis", "bmodes", - "COSEBIs B-modes in units of their uncertainty, at the fiducial cut and over all scales.", - "third", + f"COSEBIs Bₙ/σₙ at the fiducial {tcut} (filled) and over the full range (open). " + "Shaded: modes beyond n = 6.", + "half", ) - DV.pseudo_cl(cl, figs.path("bmode_cl"), ell_cut) + DV.cl_bmodes(cl, figs.path("bmode_cl"), ell_cut) figs.add( "bmode_cl", "bmodes", - f"Pseudo-Cℓ BB and EB in units of their uncertainty. Shaded: outside {ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}.", - "third", + f"Cℓ BB and EB in units of their uncertainty. Shaded: outside {lcut}.", + "half", ) - fid_co = co["cuts"][co["fiducial"]]["pte_B"] - headline = [ - { - "label": "ξ± pure B, joint", - "symbol": "xi", - "pte": ptes.get("combined"), - "range": tcut, - }, - { - "label": "COSEBIs B", - "symbol": "cosebis", - "pte": fid_co, - "range": rf"{co['fiducial'][0]:g}′–{co['fiducial'][1]:g}′", - }, - { - "label": "pseudo-Cℓ BB", - "symbol": "cl", - "pte": DV.cl_pte(cl, "BB", ell_cut), - "range": f"{ell_cut[0]} ≤ ℓ ≤ {ell_cut[1]}", - }, - ] - - # 3. scale cuts - SC.config_space(pe, figs.path("ptes_config"), theta_cut) - SC.config_space(pe, figs.path("ptes_config_narrow"), theta_cut, layout="narrow") + for layout, suffix in (("wide", ""), ("narrow", "_narrow")): + SC.config_space(pe, figs.path("ptes_config" + suffix), theta_cut, layout=layout) + SC.harmonic_and_cosebis( + cl, co, figs.path("ptes_harmonic" + suffix), ell_cut, layout=layout + ) figs.add( "ptes_config", - "scale_cuts", - "Configuration-space B-mode PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", + "bmodes", + "Pure-B PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", "full", narrow=True, - ) - SC.harmonic_and_cosebis(cl, co, figs.path("ptes_harmonic"), ell_cut) - SC.harmonic_and_cosebis( - cl, co, figs.path("ptes_harmonic_narrow"), ell_cut, layout="narrow" + group="scale_cuts", ) figs.add( "ptes_harmonic", - "scale_cuts", - "Cℓ BB PTE for every band range, and the COSEBIs B PTE for each cached angular cut.", + "bmodes", + "Cℓ BB PTE for every band range, and COSEBIs B PTE (n ≤ 20) for each cached cut.", "full", narrow=True, + group="scale_cuts", ) + if len(versions) > 1: + labelled = [(v, dd) for v, (_, _, dd) in zip(versions, loaded)] + V.pure_eb( + [(v, dd["pe"]) for v, dd in labelled], + figs.path("versions_pure_eb"), + theta_cut, + ) + V.cosebis( + [(v, dd["co"]) for v, dd in labelled], + figs.path("versions_cosebis"), + full_cut, + ) + V.cl([(v, dd["cl"]) for v, dd in labelled], figs.path("versions_cl"), ell_cut) + figs.add( + "versions_pure_eb", + "bmodes", + "θξ± and pure-B ξ±/σ across versions; boxes span the versions, bars mark the reference.", + "full", + group="versions", + ) + figs.add( + "versions_cosebis", + "bmodes", + "COSEBIs Bₙ/σₙ across versions.", + "full", + group="versions", + ) + figs.add( + "versions_cl", + "bmodes", + "Cℓ BB/σ and EB/σ across versions.", + "full", + group="versions", + ) - # 4. covariances - n = len(xi["theta"]) - CV.correlation( - xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), r"$\xi_\pm$ reporting grid" - ) - figs.add( - "cov_xi", - "covariance", - "ξ± reporting-grid covariance (analytic, masked).", - "half", - ) - CV.correlation( - pe["cov"], - CV.pure_eb_blocks(len(pe["theta"])), - figs.path("cov_pure_eb"), - "pure E/B modes", - ) - figs.add( - "cov_pure_eb", "covariance", "Pure E, B and ambiguous modes of ξ±.", "half" - ) - fc = co["fiducial"] - CV.correlation( - co["cuts"][fc]["cov"], - CV.cosebis_blocks(co["nmodes"]), - figs.path("cov_cosebis"), - rf"COSEBIs, ${fc[0]:g}'\!-\!{fc[1]:g}'$", - ) - figs.add( - "cov_cosebis", - "covariance", - "COSEBIs E and B modes at the fiducial cut.", - "half", - ) - CV.correlation( - CV.cl_matrix(cl["cov"]), - CV.cl_blocks(len(cl["ell"])), - figs.path("cov_cl"), - r"pseudo-$C_\ell$", - ) - figs.add( - "cov_cl", "covariance", "Pseudo-Cℓ EE, EB and BB (Gaussian, NaMaster).", "half" - ) + table = { + "threshold": 0.05, + "columns": ptes.COLUMNS, + "rows": [ + {"version": v, **row} + for v, (_, _, dd) in zip(versions, loaded) + for row in ptes.rows( + dd["pe"], dd["co"], dd["cl"], theta_cut, ell_cut, full_cut + ) + ], + } - # 5. PSF leakage + # PSF leakage rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit) PSF.tau_fit(rt, figs.path("psf_tau")) figs.add( "psf_tau", - "psf", + "leakage", "Galaxy–PSF correlations τ₀,₂,₅ with the posterior band of the α, β, η leakage model.", "full", ) PSF.posterior(rt, figs.path("psf_posterior")) figs.add( "psf_posterior", - "psf", + "leakage", "Posterior of the leakage parameters (68 and 95%).", "half", ) @@ -239,44 +245,45 @@ def render(run_dir, versions, out, maps_dir=None): PSF.alpha_estimates(leakage, rt, figs.path("psf_alpha")) figs.add( "psf_alpha", - "psf", + "leakage", "Every estimate of the leakage coefficient: object-wise, scale-dependent and ρ/τ fit.", "half", ) PSF.rho_stats(rt, figs.path("psf_rho")) figs.add( - "psf_rho", "psf", "ρ statistics of the PSF model and its residuals.", "full" + "psf_rho", "leakage", "ρ statistics of the PSF model and its residuals.", "full" ) PSF.xi_sys_ratio(rt, xi, figs.path("psf_xi_sys")) figs.add( "psf_xi_sys", - "psf", + "leakage", "Propagated PSF systematic relative to the blinded ξ±; dotted lines at ±10%.", "full", ) - # 6. weights + # Weights, footprint and field maps, covariances if ref.weight_cells is not None: cells = dict(np.load(ref.weight_cells, allow_pickle=False)) W.weights(cells, figs.path("weights")) + W.weights(cells, figs.path("weights_narrow"), layout="narrow") s = W.summary(cells) figs.add( "weights", - "weights", - f"Each galaxy carries the weight R²/σₑ² of its SNR × size cell, so the " - f"{s['n_values']} distinct values are the {s['n_occupied']} occupied cells; " - f"histograms of w are combs, not clipping.", + "survey", + f"Shape weight on its {int(np.sqrt(s['n_cells']))}×{int(np.sqrt(s['n_cells']))} " + f"signal-to-noise × size grid: every galaxy in a cell carries that cell's " + f"weight, so the catalogue has {s['n_values']} distinct weights " + f"({s['w_min']:.2f} to {s['w_max']:.1f}).", "full", + narrow=True, ) - - # 7. sky if ref.field_maps is not None: maps = dict(np.load(ref.field_maps, allow_pickle=False)) footprint = dict(np.load(ref.cv / f"footprint_{versions[0]}.npz")) sky.footprint_map(maps, footprint, figs.path("sky_footprint")) figs.add( "sky_footprint", - "sky", + "survey", "Effective source density in a zenithal equal-area projection centred on the patch.", "full", ) @@ -291,7 +298,7 @@ def render(run_dir, versions, out, maps_dir=None): ) figs.add( "sky_galaxies", - "sky", + "survey", "Galaxy fields: ellipticity dispersion, mean weight and weighted mean e₁, e₂.", "full", narrow=True, @@ -299,56 +306,71 @@ def render(run_dir, versions, out, maps_dir=None): dt = sky.median_quantity(maps, "dT_over_T", 128) figs.add( "sky_psf", - "sky", + "survey", "PSF fields: model FWHM at galaxies, and star-minus-model size and ellipticity " f"residuals (median size residual {100 * dt:+.2f}%).", "full", narrow=True, ) - - # 8. versions - if len(versions) > 1: - runs = [P.RunProducts(run_dir, v, maps_dir) for v in versions] - blinds = [P.blind_of(r.assembled) for r in runs] - V.pure_eb( - [ - (v, P.load_pure_eb(r.pure_eb, b)) - for v, r, b in zip(versions, runs, blinds) - ], - figs.path("versions_pure_eb"), - theta_cut, - ) - figs.add( - "versions_pure_eb", - "versions", - "ξ± and pure-B ξ±/σ across versions; boxes span the versions, bars mark the reference.", - "full", - ) - V.cosebis( - [ - (v, P.load_cosebis(r.cosebis, b)) - for v, r, b in zip(versions, runs, blinds) - ], - figs.path("versions_cosebis"), - cuts["cosebis_full"], - ) - figs.add("versions_cosebis", "versions", "COSEBIs B/σ across versions.", "full") - V.pseudo_cl( - [ - (v, P.load_pseudo_cl(r.assembled, r.pseudo_cl_cov, b)) - for v, r, b in zip(versions, runs, blinds) - ], - figs.path("versions_cl"), - ell_cut, - ) - figs.add("versions_cl", "versions", "Cℓ BB/σ and EB/σ across versions.", "full") + n = len(xi["theta"]) + CV.correlation( + xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), r"$\xi_\pm$ reporting grid" + ) + figs.add( + "cov_xi", + "survey", + "ξ± reporting-grid covariance (analytic, masked).", + "half", + group="covariance", + ) + CV.correlation( + pe["cov"], + CV.pure_eb_blocks(len(pe["theta"])), + figs.path("cov_pure_eb"), + "pure E/B modes", + ) + figs.add( + "cov_pure_eb", + "survey", + "Pure E, B and ambiguous modes of ξ±.", + "half", + group="covariance", + ) + fc = co["fiducial"] + CV.correlation( + co["cuts"][fc]["cov"], + CV.cosebis_blocks(co["nmodes"]), + figs.path("cov_cosebis"), + rf"COSEBIs, ${fc[0]:g}'\!-\!{fc[1]:g}'$", + ) + figs.add( + "cov_cosebis", + "survey", + "COSEBIs E and B modes at the fiducial cut.", + "half", + group="covariance", + ) + CV.correlation( + CV.cl_matrix(cl["cov"]), + CV.cl_blocks(len(cl["ell"])), + figs.path("cov_cl"), + r"$C_\ell$", + ) + figs.add( + "cov_cl", + "survey", + "Cℓ EE, EB and BB (Gaussian, NaMaster).", + "half", + group="covariance", + ) manifest = { - "schema_version": 1, + "schema_version": 2, "versions": list(versions), "blind": blind, "sections": [{"id": s, "title": t} for s, t in SECTIONS], - "headline_ptes": headline, + "groups": GROUPS, + "pte_table": table, "figures": figs.entries, } (figs.out / "figures.json").write_text( diff --git a/src/sp_validation/dashboard/scale_cuts.py b/src/sp_validation/dashboard/scale_cuts.py index 77f910828..8a9a906df 100644 --- a/src/sp_validation/dashboard/scale_cuts.py +++ b/src/sp_validation/dashboard/scale_cuts.py @@ -70,7 +70,7 @@ def _heatmap(ax, pte, lo_labels, hi_labels, fid, label, cmap, norm): (i - 0.5, j - 0.5), 1, 1, fill=False, edgecolor=S.INK, lw=1.6, zorder=5 ) ) - S.label_box(ax, label, loc="lower right", fontsize=9) + S.label_box(ax, label, loc="lower right") return mesh @@ -184,4 +184,4 @@ def _cosebis_cuts(ax, co, cmap, norm, threshold=S.PTE_THRESHOLD): ax.set_xticklabels([f"${t:g}$" for t in ticks]) ax.tick_params(which="both", top=False, right=False, left=False) ax.set_xlabel(r"$B_n$ PTE ($n \leq %d$)" % co["nmodes"]) - S.label_box(ax, "COSEBIs", loc="lower right", fontsize=9) + S.label_box(ax, "COSEBIs", loc="lower right") diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py index 37cdc23b0..1cdf1d73e 100644 --- a/src/sp_validation/dashboard/sky.py +++ b/src/sp_validation/dashboard/sky.py @@ -403,20 +403,19 @@ def _robust_limits(values, symmetric=False, q=(1, 99)): def _colorbar(fig, im, ax, label, symmetric): - cbar = fig.colorbar( + cbar = S.colorbar( + fig, im, - ax=ax, + ax, + label, orientation="horizontal", fraction=0.05, pad=0.02, aspect=28, extend="both", ) - cbar.outline.set_linewidth(0.5) - cbar.ax.tick_params(labelsize=7.5, length=2, top=False) cbar.locator = mpl.ticker.MaxNLocator(5, symmetric=symmetric) cbar.update_ticks() - cbar.set_label(label, fontsize=8.5) return cbar diff --git a/src/sp_validation/dashboard/style.py b/src/sp_validation/dashboard/style.py index 685c3e9ca..04a667f2b 100644 --- a/src/sp_validation/dashboard/style.py +++ b/src/sp_validation/dashboard/style.py @@ -181,6 +181,38 @@ def blind_stamp(fig, blind, where="top"): ) +def header(ax, text, side="left"): + """Panel header above the frame: the panel's subject on the left in ink, + a qualifier (a cut, a sign convention) on the right in muted ink.""" + if side == "left": + ax.set_title(text, loc="left", fontsize=9, color=INK, pad=4) + else: + ax.set_title(text, loc="right", fontsize=8.5, color=INK_MUTED, pad=4) + + +def colorbar(fig, mappable, ax, label, **kwargs): + """Colour bar in the house style: thin outline, small ticks, ink label.""" + cbar = fig.colorbar(mappable, ax=ax, **kwargs) + cbar.outline.set_linewidth(0.6) + cbar.ax.tick_params( + labelsize=8, length=2.5, width=0.6, which="both", direction="out" + ) + cbar.set_label(label, fontsize=9) + if cbar.solids is not None: + cbar.solids.set_edgecolor("face") + return cbar + + +def frame_only(ax): + """Frame without mirrored ticks, for images (heatmaps, maps, matrices).""" + ax.tick_params(which="both", top=False, right=False) + + +def theta_ticks(ax): + ax.set_xticks([1, 10, 100]) + ax.set_xticklabels(["$1$", "$10$", "$100$"]) + + def zero_line(ax): ax.axhline(0, color=INK, ls=(0, (4, 3)), lw=0.7, alpha=0.6, zorder=1) @@ -240,14 +272,11 @@ def pte_norm(threshold=PTE_THRESHOLD): def pte_colorbar(fig, mappable, ax, threshold=PTE_THRESHOLD, **kwargs): - cbar = fig.colorbar(mappable, ax=ax, spacing="proportional", **kwargs) + cbar = colorbar(fig, mappable, ax, "PTE", spacing="proportional", **kwargs) ticks = [threshold, 0.5, 1 - threshold] cbar.set_ticks(ticks) cbar.set_ticklabels([f"${t:g}$" for t in ticks]) cbar.minorticks_off() - cbar.outline.set_linewidth(0.6) - cbar.solids.set_edgecolor("face") - cbar.set_label("PTE") return cbar diff --git a/src/sp_validation/dashboard/versions.py b/src/sp_validation/dashboard/versions.py index 71deed552..6a3f54b95 100644 --- a/src/sp_validation/dashboard/versions.py +++ b/src/sp_validation/dashboard/versions.py @@ -146,13 +146,7 @@ def cosebis(datasets, out, full_cut=(1.0, 250.0)): ) S.zero_line(ax) ax.set_ylabel(r"$B_n / \sigma_n$") - ax.set_title( - rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", - loc="right", - fontsize=8.5, - color=S.INK_MUTED, - pad=3, - ) + S.header(ax, rf"${cut[0]:g}' < \theta < {cut[1]:g}'$", side="right") axes[-1].set_xlim(0.4, nmodes + 0.6) axes[-1].set_xticks(modes) axes[-1].set_xlabel(r"COSEBI mode $n$") @@ -160,7 +154,7 @@ def cosebis(datasets, out, full_cut=(1.0, 250.0)): return S.save(fig, out, "full") -def pseudo_cl(datasets, out, ell_cut): +def cl(datasets, out, ell_cut): """C_ℓ^BB/σ and C_ℓ^EB/σ across versions.""" n = len(datasets) colors = version_colors(n) diff --git a/src/sp_validation/dashboard/weights.py b/src/sp_validation/dashboard/weights.py index 38d27d37e..263b7bfe3 100644 --- a/src/sp_validation/dashboard/weights.py +++ b/src/sp_validation/dashboard/weights.py @@ -33,31 +33,42 @@ def summary(cells): } -def weights(cells, out): - """Weight on the SNR x size-ratio grid, and the distinct-weight spectrum.""" +def _tt(text): + """Catalogue column names in typewriter mathtext.""" + return r"$\mathtt{" + text.replace("_", r"\_") + "}$" + + +def weights(cells, out, layout="wide"): + """Galaxy weight over the SNR x size-ratio grid it is defined on. + + Colour is each cell's weight; the axis labels name the catalogue columns + binned and the colour-bar label defines the weight. + """ snr, size = cells["snr_edges"], cells["size_ratio_edges"] - count = cells["count"].astype(float) - frac = count / count.sum() + count = cells["count"] w = np.where(count > 0, cells["w_min"], np.nan) - values, mult = cells["w_values"], cells["w_multiplicity"] log_fmt = mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$") with S.figure_style(): - fig, ax = plt.subplot_mosaic( - [["top", ".", "spec"], ["grid", "side", "spec"]], - figsize=(7.2, 3.5), - width_ratios=(4, 0.7, 4.2), - height_ratios=(0.8, 4), - ) - g = ax["grid"] + wide = layout == "wide" + fig, g = plt.subplots(figsize=(7.2, 3.7) if wide else (3.6, 4.4)) mesh = g.pcolormesh( - snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, linewidth=0.25 + snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, linewidth=0.3 ) g.set_xscale("log") g.set_yscale("log") g.set_xlim(snr[0], snr[-1]) g.set_ylim(size[0], size[-1]) - g.set_xlabel("signal-to-noise") - g.set_ylabel(r"size ratio $T_{\rm gal} / T_{\rm PSF}$") + sep = ": " if wide else "\n" + g.set_xlabel( + "signal-to-noise ratio" + + sep + + _tt("snr = NGMIX_FLUX_NOSHEAR / NGMIX_FLUX_ERR_NOSHEAR"), + fontsize=10 if wide else 7.5, + ) + g.set_ylabel( + "size ratio" + sep + _tt("NGMIX_T_NOSHEAR / NGMIX_T_PSF_RECONV_NOSHEAR"), + fontsize=8.5 if wide else 7.5, + ) g.xaxis.set_major_formatter(log_fmt) g.xaxis.set_minor_formatter( mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$" if v in (30, 300) else "") @@ -65,56 +76,30 @@ def weights(cells, out): g.yaxis.set_major_formatter(log_fmt) g.yaxis.set_minor_formatter( mpl.ticker.FuncFormatter( - lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2) else "" + lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2, 3) else "" ) ) - g.tick_params(which="both", top=False, right=False) - cbar = fig.colorbar( - mesh, ax=g, location="bottom", aspect=30, pad=0.02, fraction=0.06 - ) - cbar.set_label(r"cell weight $w = R^2 / \sigma_e^2$") - cbar.outline.set_linewidth(0.5) - - # Marginal shares of galaxies along each grid axis. - bar = dict(color=S.NEUTRAL_LIGHT, edgecolor=S.RAG, linewidth=0.3) - ax["top"].bar( - snr[:-1], 100 * frac.sum(1), width=np.diff(snr), align="edge", **bar - ) - ax["top"].set_xscale("log") - ax["top"].set_xlim(g.get_xlim()) - ax["side"].barh( - size[:-1], 100 * frac.sum(0), height=np.diff(size), align="edge", **bar + S.frame_only(g) + S.header( + g, + rf"{_tt('w_des')}: one weight per cell" + + ( + rf", ${count.shape[0]} \times {count.shape[1]}$ log-spaced cells" + if wide + else "" + ), ) - ax["side"].set_yscale("log") - ax["side"].set_ylim(g.get_ylim()) - for name, axis in (("top", "x"), ("side", "y")): - m = ax[name] - m.tick_params( - axis=axis, - which="both", - labelbottom=False, - labelleft=False, - bottom=False, - left=False, - top=False, - right=False, - ) - for side in ("top", "right"): - m.spines[side].set_visible(False) - m.tick_params(which="both", top=False, right=False, labelsize=7) - m.locator_params(axis="y" if name == "top" else "x", nbins=2) - ax["top"].set_ylabel("%", fontsize=8) - ax["side"].set_xlabel("%", fontsize=8) - - spec = ax["spec"] - share = mult / mult.sum() - spec.vlines(values, 0, share * 100, color=S.INK, lw=0.5, alpha=0.85) - spec.set_xlabel(r"weight $w$") - spec.set_ylabel("galaxies at this weight [%]") - spec.set_ylim(0, None) - spec.set_xlim(0, values.max() * 1.03) - spec.tick_params(which="both", top=False, right=False) - spec.set_title( - f"{len(values)} distinct values, one per cell", loc="left", fontsize=9 + S.colorbar( + fig, + mesh, + g, + r"$w = \bar R^{2} / \sigma_e^{2}$" + "\n" + r"$\bar R = \frac{1}{2}\langle R_{11} + R_{22}\rangle$ (metacal)," + "\n" + r"$\sigma_e^{2} = \frac{1}{2}\langle e_1^{2} + e_2^{2}\rangle$" + " (uncalibrated), per cell", + aspect=22, + pad=0.02, + location="right" if wide else "bottom", ) - return S.save(fig, out, "full") + return S.save(fig, out, "full" if wide else "half") From 57f13f599e80e5cdde820434e06869a57d5aa8f6 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:28:44 -0400 Subject: [PATCH 22/53] fix(dashboard): normalize stellar size residual by PSF model --- src/sp_validation/dashboard/field_maps.py | 4 +-- src/sp_validation/dashboard/render.py | 27 ++++++++++++------- src/sp_validation/dashboard/sky.py | 5 ++-- .../tests/test_dashboard_figures.py | 14 +++++++++- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/sp_validation/dashboard/field_maps.py b/src/sp_validation/dashboard/field_maps.py index 736022329..3cfabe6af 100644 --- a/src/sp_validation/dashboard/field_maps.py +++ b/src/sp_validation/dashboard/field_maps.py @@ -74,7 +74,7 @@ "n_star", "sum_de1", "sum_de2", - "sum_dT_over_T", + "sum_dT_over_T_psf", "sum_T_psf", ] @@ -177,7 +177,7 @@ def star_sums(path, nside): ("n_star", None), ("sum_de1", stars["HSM_G1_STAR"] - stars["HSM_G1_PSF"]), ("sum_de2", stars["HSM_G2_STAR"] - stars["HSM_G2_PSF"]), - ("sum_dT_over_T", (t_star - t_psf) / t_star), + ("sum_dT_over_T_psf", (t_star - t_psf) / t_psf), ("sum_T_psf", t_psf), ): _accumulate(sums[key], pix, value, npix) diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index b83d6218c..9b45b77c6 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -287,7 +287,12 @@ def render(run_dir, versions, out, maps_dir=None): "Effective source density in a zenithal equal-area projection centred on the patch.", "full", ) - for which in ("galaxies", "psf"): + field_sets = ["galaxies"] + if "sum_dT_over_T_psf" in maps: + field_sets.append("psf") + else: + print("Skipping sky_psf: field maps lack model-normalised size residuals") + for which in field_sets: sky.field_maps(maps, figs.path(f"sky_{which}"), which, footprint) sky.field_maps( maps, @@ -303,15 +308,17 @@ def render(run_dir, versions, out, maps_dir=None): "full", narrow=True, ) - dt = sky.median_quantity(maps, "dT_over_T", 128) - figs.add( - "sky_psf", - "survey", - "PSF fields: model FWHM at galaxies, and star-minus-model size and ellipticity " - f"residuals (median size residual {100 * dt:+.2f}%).", - "full", - narrow=True, - ) + if "psf" in field_sets: + dt = sky.median_quantity(maps, "dT_over_T", 128) + figs.add( + "sky_psf", + "survey", + "PSF fields: model FWHM at galaxies, and star-minus-model size and " + "ellipticity residuals for PSF validation stars (valid HSM); " + f"size residual normalised by the model (median {100 * dt:+.2f}%).", + "full", + narrow=True, + ) n = len(xi["theta"]) CV.correlation( xi["cov"], CV.xi_blocks(n), figs.path("cov_xi"), r"$\xi_\pm$ reporting grid" diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py index 1cdf1d73e..41342d088 100644 --- a/src/sp_validation/dashboard/sky.py +++ b/src/sp_validation/dashboard/sky.py @@ -384,7 +384,8 @@ def quantity(maps, name, nside_out, footprint=None): values = _ratio(sums["sum_w_fwhm_psf"], sums["sum_w"]) keep = sums["n_gal"] > 0 elif name in ("de1", "de2", "dT_over_T"): - values = _ratio(sums[f"sum_{name}"], sums["n_star"]) + key = "sum_dT_over_T_psf" if name == "dT_over_T" else f"sum_{name}" + values = _ratio(sums[key], sums["n_star"]) keep = sums["n_star"] >= 5 else: raise KeyError(name) @@ -466,7 +467,7 @@ def _aspect(parts): ( "dT_over_T", 128, - r"stars: $\langle (T_\star - T_{\rm PSF})/T_\star \rangle$", + r"stars: $\langle (T_\star - T_{\rm PSF})/T_{\rm PSF} \rangle$", True, ), ("de1", 128, r"stars: $\langle e_{1,\star} - e_{1,{\rm PSF}} \rangle$", True), diff --git a/src/sp_validation/tests/test_dashboard_figures.py b/src/sp_validation/tests/test_dashboard_figures.py index ba49db161..0f5426f24 100644 --- a/src/sp_validation/tests/test_dashboard_figures.py +++ b/src/sp_validation/tests/test_dashboard_figures.py @@ -6,7 +6,19 @@ import pytest from sp_validation.dashboard.field_maps import weight_cell_index, weight_grid_edges -from sp_validation.dashboard.sky import Zenithal, coarsen, footprint_parts +from sp_validation.dashboard.sky import Zenithal, coarsen, footprint_parts, quantity + + +def test_size_residual_uses_model_normalised_sum(): + maps = { + "nside": 128, + "pixels": np.array([0]), + "n_star": np.array([10.0]), + "sum_dT_over_T_psf": np.array([-2.0]), + } + pixels, values = quantity(maps, "dT_over_T", 128) + np.testing.assert_array_equal(pixels, [0]) + np.testing.assert_allclose(values, [-0.2]) def test_weight_cells_match_calibration_binning(): From 1a265beb2a898c9ff9375908882c2df2f07eb019 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:33:05 -0400 Subject: [PATCH 23/53] Add position-shear correlation estimator --- src/sp_validation/dashboard/position_shear.py | 68 ++++++++++++++++++ .../tests/test_position_shear.py | 72 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/sp_validation/dashboard/position_shear.py create mode 100644 src/sp_validation/tests/test_position_shear.py diff --git a/src/sp_validation/dashboard/position_shear.py b/src/sp_validation/dashboard/position_shear.py new file mode 100644 index 000000000..6364be96b --- /dev/null +++ b/src/sp_validation/dashboard/position_shear.py @@ -0,0 +1,68 @@ +"""TreeCorr position–shear estimator and its joint jackknife covariance. + +The caller constructs both catalogues using the same patch centres. This module +is deliberately independent of catalogue layout and does not generate randoms. +""" + +import numpy as np +import treecorr + + +def measure( + lenses, + sources, + *, + randoms=None, + min_sep=1.0, + max_sep=250.0, + nbins=20, + num_threads=None, +): + """Measure tangential and cross shear, optionally subtracting random lenses. + + All catalogues must use the same spatial patch centres. The returned + covariance has block order (tangential bins, cross bins), including the + covariance between the two and, when supplied, the random subtraction. + Separations are in arcminutes. + """ + config = dict( + min_sep=min_sep, + max_sep=max_sep, + nbins=nbins, + sep_units="arcmin", + var_method="jackknife", + ) + correlations = [] + for positions in (lenses,) if randoms is None else (lenses, randoms): + corr = treecorr.NGCorrelation(config) + corr.process(positions, sources, num_threads=num_threads) + correlations.append(corr) + + def vector(corrs): + result = np.concatenate((corrs[0].xi, corrs[0].xi_im)) + if len(corrs) == 2: + result = result - np.concatenate((corrs[1].xi, corrs[1].xi_im)) + return result + + values = vector(correlations) + covariance = treecorr.estimate_multi_cov( + correlations, "jackknife", func=vector, cross_patch_weight="simple" + ) + error = np.sqrt(np.maximum(np.diag(covariance), 0)) + dg = correlations[0] + result = dict( + theta=np.exp(dg.meanlogr), + xi_t=values[:nbins], + xi_x=values[nbins:], + error_t=error[:nbins], + error_x=error[nbins:], + covariance=covariance, + npairs=dg.npairs.copy(), + weight=dg.weight.copy(), + ) + if randoms is not None: + result.update( + random_npairs=correlations[1].npairs.copy(), + random_weight=correlations[1].weight.copy(), + ) + return result diff --git a/src/sp_validation/tests/test_position_shear.py b/src/sp_validation/tests/test_position_shear.py new file mode 100644 index 000000000..97fb65d1c --- /dev/null +++ b/src/sp_validation/tests/test_position_shear.py @@ -0,0 +1,72 @@ +import numpy as np +import treecorr + +from sp_validation.dashboard.position_shear import measure + + +def test_injected_tangential_shear_and_joint_covariance(): + rng = np.random.default_rng(42) + # Four well-separated lens groups, each with sources in a tangential pattern. + centres = np.array([[0, 0], [4, 0], [0, 4], [4, 4]], dtype=float) + ra, dec, g1, g2 = [], [], [], [] + for x, y in centres: + angle = rng.uniform(0, 2 * np.pi, 200) + radius = rng.uniform(0.03, 0.12, 200) + ra.extend(x + radius * np.cos(angle) / np.cos(np.deg2rad(y))) + dec.extend(y + radius * np.sin(angle)) + g1.extend(-0.05 * np.cos(2 * angle)) + g2.extend(0.05 * np.sin(2 * angle)) + lon, lat = np.deg2rad(centres).T + patch_centers = np.column_stack( + (np.cos(lat) * np.cos(lon), np.cos(lat) * np.sin(lon), np.sin(lat)) + ) + lens = treecorr.Catalog( + ra=centres[:, 0], + dec=centres[:, 1], + ra_units="deg", + dec_units="deg", + patch_centers=patch_centers, + ) + source = treecorr.Catalog( + ra=ra, + dec=dec, + g1=g1, + g2=g2, + ra_units="deg", + dec_units="deg", + patch_centers=patch_centers, + ) + result = measure(lens, source, min_sep=2, max_sep=7, nbins=3, num_threads=1) + np.testing.assert_allclose(result["xi_t"], 0.05, atol=2e-4) + np.testing.assert_allclose(result["xi_x"], 0, atol=1e-4) + assert result["covariance"].shape == (6, 6) + assert np.all(result["npairs"] > 0) + + +def test_random_subtraction_identical_catalogues(): + rng = np.random.default_rng(3) + ra = rng.uniform(0, 3, 500) + dec = rng.uniform(0, 3, 500) + source = treecorr.Catalog( + ra=ra, + dec=dec, + g1=rng.normal(size=500), + g2=rng.normal(size=500), + ra_units="deg", + dec_units="deg", + npatch=6, + rng=rng, + ) + lens = treecorr.Catalog( + ra=ra[::5], + dec=dec[::5], + ra_units="deg", + dec_units="deg", + patch_centers=source.patch_centers, + ) + result = measure( + lens, source, randoms=lens, min_sep=2, max_sep=60, nbins=4, num_threads=1 + ) + np.testing.assert_allclose(result["xi_t"], 0, atol=1e-14) + np.testing.assert_allclose(result["xi_x"], 0, atol=1e-14) + np.testing.assert_allclose(result["covariance"], 0, atol=1e-14) From b1533a198ff35066526e11db72d89f790c3ef779 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:33:38 -0400 Subject: [PATCH 24/53] feat(dashboard): compare PSF systematic estimators --- src/sp_validation/dashboard/psf.py | 35 +++++++++++++++++++++++++++ src/sp_validation/dashboard/render.py | 13 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py index efee35c08..613eeef78 100644 --- a/src/sp_validation/dashboard/psf.py +++ b/src/sp_validation/dashboard/psf.py @@ -265,6 +265,41 @@ def xi_sys_ratio(rt, xi, out, reference=0.1): return S.save(fig, out, "full") +def xi_sys_comparison(rt, out): + """Compare the full fitted systematic with tau_0 squared over rho_0.""" + theta = np.asarray(rt["theta"]) + # Pair-weighted mean separations differ slightly within the same bins. + if not ( + np.allclose(theta, rt["theta_tau"], rtol=0.002) + and np.allclose(theta, rt["fit"]["theta"]) + ): + raise ValueError("rho, tau and PSF fit angular grids differ") + rho = np.asarray(rt["rho"][0]["p"]) + tau = np.asarray(rt["tau"][0]["p"]) + with np.errstate(divide="ignore", invalid="ignore"): + simple = np.where(rho != 0, tau**2 / rho, np.nan) + lo, med, hi = np.percentile(rt["fit"]["samples_plus"], [16, 50, 84], axis=0) + with S.figure_style(): + fig, ax = plt.subplots(figsize=(3.6, 2.8)) + ax.fill_between(theta, lo, hi, color=MODEL, alpha=0.3, lw=0) + ax.plot(theta, med, color=MODEL, label=r"$\alpha,\beta,\eta$ model") + ax.plot( + theta, + simple, + color=SERIES[1], + marker="o", + ms=3, + label=r"$\tau_{0,+}^2/\rho_{0,+}$", + ) + S.zero_line(ax) + S.theta_axis(ax) + ax.set_yscale("symlog", linthresh=1e-8) + ax.set_xlabel(r"$\theta$ [arcmin]") + ax.set_ylabel(r"$\xi^{\rm sys}_+(\theta)$") + ax.legend() + return S.save(fig, out, "half") + + ALPHA_ROWS = [ ("aii_mean", r"$\langle a_{ii} \rangle$", "object-wise"), ("a11", r"$a_{11}$", "object-wise"), diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index 9b45b77c6..ded20a983 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -261,6 +261,19 @@ def render(run_dir, versions, out, maps_dir=None): "full", ) + try: + PSF.xi_sys_comparison(rt, figs.path("psf_xi_sys_comparison")) + except ValueError as exc: + print(f"Skipping psf_xi_sys_comparison: {exc}") + else: + figs.add( + "psf_xi_sys_comparison", + "leakage", + "PSF systematic ξ⁺ from the α–β–η posterior (68% band) compared " + "with τ₀,+²/ρ₀,+; no cosmological signal is shown.", + "half", + ) + # Weights, footprint and field maps, covariances if ref.weight_cells is not None: cells = dict(np.load(ref.weight_cells, allow_pickle=False)) From 423fab28a1c2ec45848964191fc466e2bdcbee42 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:52:16 -0400 Subject: [PATCH 25/53] Reject mismatched jackknife patch centres --- src/sp_validation/dashboard/position_shear.py | 8 +++++++ .../tests/test_position_shear.py | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/sp_validation/dashboard/position_shear.py b/src/sp_validation/dashboard/position_shear.py index 6364be96b..8ce9eb675 100644 --- a/src/sp_validation/dashboard/position_shear.py +++ b/src/sp_validation/dashboard/position_shear.py @@ -25,6 +25,14 @@ def measure( covariance between the two and, when supplied, the random subtraction. Separations are in arcminutes. """ + centers = sources.patch_centers + if len(centers) < 2: + raise ValueError("jackknife requires at least two source patches") + for catalogue in (lenses,) if randoms is None else (lenses, randoms): + other = catalogue.patch_centers + if other.shape != centers.shape or not np.allclose(other, centers): + raise ValueError("all catalogues must share the source patch centres") + config = dict( min_sep=min_sep, max_sep=max_sep, diff --git a/src/sp_validation/tests/test_position_shear.py b/src/sp_validation/tests/test_position_shear.py index 97fb65d1c..ac7c0d8b8 100644 --- a/src/sp_validation/tests/test_position_shear.py +++ b/src/sp_validation/tests/test_position_shear.py @@ -1,4 +1,5 @@ import numpy as np +import pytest import treecorr from sp_validation.dashboard.position_shear import measure @@ -43,6 +44,27 @@ def test_injected_tangential_shear_and_joint_covariance(): assert np.all(result["npairs"] > 0) +def test_rejects_mismatched_patch_centres(): + source = treecorr.Catalog( + ra=[0, 1, 2], + dec=[0, 1, 2], + g1=[0, 0, 0], + g2=[0, 0, 0], + ra_units="deg", + dec_units="deg", + npatch=2, + ) + lens = treecorr.Catalog( + ra=[10, 11, 12], + dec=[0, 1, 2], + ra_units="deg", + dec_units="deg", + npatch=2, + ) + with pytest.raises(ValueError, match="patch centres"): + measure(lens, source, num_threads=1) + + def test_random_subtraction_identical_catalogues(): rng = np.random.default_rng(3) ra = rng.uniform(0, 3, 500) From a30ffdd8653f82ef2cf805afdf18a7c8356765b4 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:53:06 -0400 Subject: [PATCH 26/53] feat(dashboard): plot PSF validation-star residuals --- src/sp_validation/dashboard/star_residuals.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/sp_validation/dashboard/star_residuals.py diff --git a/src/sp_validation/dashboard/star_residuals.py b/src/sp_validation/dashboard/star_residuals.py new file mode 100644 index 000000000..1511bf187 --- /dev/null +++ b/src/sp_validation/dashboard/star_residuals.py @@ -0,0 +1,36 @@ +"""PSF validation-star residual distributions.""" + +import matplotlib.pyplot as plt + +from . import style as S + + +def plot(data, out): + """Draw residual histograms with moments computed over finite detections.""" + with S.figure_style(): + fig = plt.figure(figsize=(7.2, 5.0)) + gs = fig.add_gridspec(2, 2) + axes = [ + fig.add_subplot(gs[0, 0]), + fig.add_subplot(gs[0, 1]), + fig.add_subplot(gs[1, :]), + ] + labels = [ + (r"$\Delta e_1$", r"$\Delta e_2$"), + (r"$\Delta M^{(4)}_1$", r"$\Delta M^{(4)}_2$"), + (r"$\sqrt{T_\star/T_{\rm PSF}}-1$",), + ] + for ax, indices, names in zip(axes, ((0, 1), (2, 3), (4,)), labels): + for i, name in zip(indices, names): + ax.stairs( + data[f"hist_{i}"], + data[f"edges_{i}"], + label=f"{name}: mean {float(data[f'mean_{i}']):.3g}, " + rf"$\sigma$ {float(data[f'std_{i}']):.3g}", + ) + ax.set_yscale("log") + ax.set_xlim((-0.15, 0.15) if indices != (4,) else (-0.3, 0.3)) + ax.set_xlabel("Residual" if len(indices) == 2 else names[0]) + ax.set_ylabel("Star detections") + ax.legend(fontsize=7) + return S.save(fig, out, "full") From ccdfc509ce3ef122914a34db7d9b42f7226930b9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:53:07 -0400 Subject: [PATCH 27/53] feat(dashboard): stream selection-stage magnitude histograms --- src/sp_validation/dashboard/selection_hist.py | 109 ++++++++++++++++++ .../tests/test_dashboard_selection.py | 55 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 src/sp_validation/dashboard/selection_hist.py create mode 100644 src/sp_validation/tests/test_dashboard_selection.py diff --git a/src/sp_validation/dashboard/selection_hist.py b/src/sp_validation/dashboard/selection_hist.py new file mode 100644 index 000000000..6dbe1b47c --- /dev/null +++ b/src/sp_validation/dashboard/selection_hist.py @@ -0,0 +1,109 @@ +"""Stream cumulative selection histograms from a comprehensive catalogue.""" + +import argparse +from pathlib import Path + +import h5py +import numpy as np +import yaml + + +def build(path, config, expected, chunk=500_000): + """Apply recorded data cuts and the noshear metacal cuts cumulatively.""" + cfg = yaml.safe_load(Path(config).read_text()) + cuts = cfg["dat"] + cols = list( + dict.fromkeys( + [x["col_name"] for x in cuts] + + [ + "NGMIX_FLAGS_NOSHEAR", + "NGMIX_FLUX_NOSHEAR", + "NGMIX_FLUX_ERR_NOSHEAR", + "NGMIX_T_NOSHEAR", + "NGMIX_T_PSF_RECONV_NOSHEAR", + "mag", + ] + ) + ) + edges = np.linspace(15, 30, 151) + hist = np.zeros((len(cuts) + 2, len(edges) - 1), dtype=np.int64) + counts = np.zeros(len(cuts) + 2, dtype=np.int64) + with h5py.File(path) as f: + data = f["data"] + for start in range(0, len(data), chunk): + rows = data.fields(cols)[start : start + chunk] + mask = np.ones(len(rows), dtype=bool) + stages = [mask.copy()] + for cut in cuts: + x, v = rows[cut["col_name"]], cut["value"] + kind = cut["kind"] + if kind == "equal": + keep = x == v + elif kind == "not_equal": + keep = x != v + elif kind == "smaller_equal": + keep = x <= v + elif kind == "greater_equal": + keep = x >= v + elif kind == "range": + keep = (x >= v[0]) & (x <= v[1]) + else: + raise ValueError(f"Unsupported selection {kind}") + mask &= keep + stages.append(mask.copy()) + mc = cfg["metacal"] + with np.errstate(divide="ignore", invalid="ignore"): + snr = rows["NGMIX_FLUX_NOSHEAR"] / rows["NGMIX_FLUX_ERR_NOSHEAR"] + size = rows["NGMIX_T_NOSHEAR"] / rows["NGMIX_T_PSF_RECONV_NOSHEAR"] + mask &= ( + (rows["NGMIX_FLAGS_NOSHEAR"] == 0) + & (snr > mc["gal_snr_min"]) + & (snr < mc["gal_snr_max"]) + & (size > mc["gal_rel_size_min"]) + & (size < mc["gal_rel_size_max"]) + ) + stages.append(mask) + for i, stage in enumerate(stages): + counts[i] += stage.sum() + hist[i] += np.histogram(rows["mag"][stage], bins=edges)[0] + if counts[-1] != expected: + raise ValueError(f"Selection retained {counts[-1]} rows; expected {expected}") + return { + "edges": edges, + "hist": hist, + "counts": counts, + "labels": np.array( + ["All detections"] + [c["label"] for c in cuts] + ["Metacal SNR and size"] + ), + } + + +def plot(data, out): + """Plot cumulative histogram steps from the validated product.""" + import matplotlib.pyplot as plt + + from . import style as S + + with S.figure_style(): + fig, ax = plt.subplots(figsize=(7.2, 4.0)) + for hist, label in zip(data["hist"], data["labels"]): + ax.stairs(hist, data["edges"], label=str(label)) + ax.set_yscale("log") + ax.set_xlabel("r magnitude") + ax.set_ylabel("Detections") + ax.legend(ncol=2, fontsize=6) + return S.save(fig, out, "full") + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--catalogue", required=True) + p.add_argument("--config", required=True) + p.add_argument("--expected", type=int, required=True) + p.add_argument("--out", required=True) + args = p.parse_args() + np.savez_compressed(args.out, **build(args.catalogue, args.config, args.expected)) + + +if __name__ == "__main__": + main() diff --git a/src/sp_validation/tests/test_dashboard_selection.py b/src/sp_validation/tests/test_dashboard_selection.py new file mode 100644 index 000000000..484196e7b --- /dev/null +++ b/src/sp_validation/tests/test_dashboard_selection.py @@ -0,0 +1,55 @@ +"""Tests for the cumulative comprehensive-catalogue selection.""" + +import h5py +import numpy as np +import pytest +import yaml + +from sp_validation.dashboard.selection_hist import build + + +def test_cumulative_selection_and_expected_count(tmp_path): + names = [ + "mag", + "FLAGS", + "NGMIX_FLAGS_NOSHEAR", + "NGMIX_FLUX_NOSHEAR", + "NGMIX_FLUX_ERR_NOSHEAR", + "NGMIX_T_NOSHEAR", + "NGMIX_T_PSF_RECONV_NOSHEAR", + ] + rows = np.zeros(4, dtype=[(name, "f8") for name in names]) + rows["mag"] = [20, 21, 22, 23] + rows["FLAGS"] = [0, 0, 3, 0] + rows["NGMIX_FLUX_NOSHEAR"] = [20, 5, 20, 20] + rows["NGMIX_FLUX_ERR_NOSHEAR"] = 1 + rows["NGMIX_T_NOSHEAR"] = 1 + rows["NGMIX_T_PSF_RECONV_NOSHEAR"] = 1 + path = tmp_path / "data.hdf5" + with h5py.File(path, "w") as f: + f["data"] = rows + config = tmp_path / "config.yaml" + config.write_text( + yaml.safe_dump( + { + "dat": [ + { + "col_name": "FLAGS", + "label": "flags", + "kind": "smaller_equal", + "value": 2, + } + ], + "metacal": { + "gal_snr_min": 10, + "gal_snr_max": 500, + "gal_rel_size_min": 0.707, + "gal_rel_size_max": 3, + }, + } + ) + ) + result = build(path, config, expected=2, chunk=2) + np.testing.assert_array_equal(result["counts"], [4, 3, 2]) + with pytest.raises(ValueError, match="expected 3"): + build(path, config, expected=3, chunk=2) From 9336c99e274d32b71233ae7a1538d6ee2b2b52cd Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:53:09 -0400 Subject: [PATCH 28/53] feat(dashboard): plot object leakage and additive-bias regressions --- src/sp_validation/dashboard/object_bias.py | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/sp_validation/dashboard/object_bias.py diff --git a/src/sp_validation/dashboard/object_bias.py b/src/sp_validation/dashboard/object_bias.py new file mode 100644 index 000000000..8683b63bf --- /dev/null +++ b/src/sp_validation/dashboard/object_bias.py @@ -0,0 +1,83 @@ +"""Object-wise PSF leakage and binned additive-bias regressions.""" + +import matplotlib.pyplot as plt +import numpy as np + +from . import style as S +from .sky import DIVERGING + + +def slopes(sums): + """Weighted least-squares slopes with an intercept, from raw sums.""" + sw, sx, sy, sxx, sxy = sums[:5] + with np.errstate(divide="ignore", invalid="ignore"): + return (sxy - sx * sy / sw) / (sxx - sx * sx / sw) + + +def leakage(cells, out): + """Weighted component-wise PSF-ellipticity regressions in each cell.""" + slopes_by_component = np.array([slopes(s) for s in cells["leakage_sums"]]) + with S.figure_style(): + fig, axes = plt.subplots(1, 2, figsize=(7.2, 3)) + for ax, values, title in zip( + axes, slopes_by_component, (r"$\alpha_1$", r"$\alpha_2$") + ): + limit = np.nanpercentile(np.abs(values), 98) + mesh = ax.pcolormesh( + cells["snr_edges"], + cells["resolution_edges"], + values.T, + cmap=DIVERGING, + vmin=-limit, + vmax=limit, + ) + ax.set_xscale("log") + ax.set_xlabel("SNR") + ax.set_ylabel(r"$r_{\rm PSF}/(r_{\rm PSF}+r_{\rm gal})$") + S.colorbar(fig, mesh, ax, title) + return S.save(fig, out, "full") + + +def bias_fit(s): + """Fit weighted mean ellipticity versus property across populated bins.""" + sw, sx, sy, _, _, syy, sw2, count = s + with np.errstate(divide="ignore", invalid="ignore"): + x, y = sx / sw, sy / sw + var = np.maximum(syy / sw - y * y, 0) * sw2 / (sw * sw) + ok = (count > 1) & np.isfinite(x) & np.isfinite(y) & (var > 0) + x, y, weight = x[ok], y[ok], 1 / var[ok] + if len(x) < 3: + return np.nan, np.nan + x -= np.average(x, weights=weight) + den = np.sum(weight * x * x) + return np.sum(weight * x * y) / den, np.sqrt(1 / den) + + +def bias(cells, out): + """Slopes and fit significances for weighted binned means.""" + names = [r"galaxy $\sqrt{T}$" if name == "galaxy size" else name for name in cells["bias_names"]] + fits = np.array([[bias_fit(s) for s in row] for row in cells["bias_sums"]]) + with S.figure_style(): + fig, axes = plt.subplots(1, 2, figsize=(7.2, 4.5), sharey=True) + y = np.arange(len(names)) + for component, color in enumerate(("#3D5BA0", "#BC4538")): + value, err = fits[:, component, 0], fits[:, component, 1] + axes[0].errorbar( + value, + y + (component - 0.5) * 0.18, + xerr=err, + fmt="o", + ms=3, + color=color, + label=rf"$e_{component + 1}$", + ) + axes[1].plot( + value / err, y + (component - 0.5) * 0.18, "o", ms=3, color=color + ) + for ax in axes: + ax.axvline(0, color=S.INK_MUTED, lw=0.7) + axes[0].set_yticks(y, names) + axes[0].set_xlabel("Fitted slope") + axes[1].set_xlabel("Slope / fit uncertainty") + axes[0].legend() + return S.save(fig, out, "full") From a0723f0d7455b8645c24b33b02ca2c48c1954dce Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 05:53:10 -0400 Subject: [PATCH 29/53] feat(dashboard): plot weight cells and response distributions --- src/sp_validation/dashboard/weights.py | 51 +++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/sp_validation/dashboard/weights.py b/src/sp_validation/dashboard/weights.py index 263b7bfe3..2f44b5c14 100644 --- a/src/sp_validation/dashboard/weights.py +++ b/src/sp_validation/dashboard/weights.py @@ -17,6 +17,55 @@ from .sky import SEQUENTIAL +def weights(cells, out, layout="wide"): + """Counts, weights, response and uncalibrated shape noise per cell.""" + count = cells["count"] + with np.errstate(divide="ignore", invalid="ignore"): + response = np.where(count > 0, cells["sum_response"] / count, np.nan) + noise = np.where(count > 0, np.sqrt(cells["sum_uncal_ee"] / count), np.nan) + panels = [ + (np.where(count > 0, count, np.nan), "Number of galaxies"), + (cells["w_min"], "Weight"), + (response, "Mean response"), + (noise, r"$\sqrt{\langle e_1^2+e_2^2\rangle/2}$"), + ] + with S.figure_style(): + fig, axes = plt.subplots( + 2, 2, figsize=(7.2, 5.8) if layout == "wide" else (3.6, 4.5) + ) + for ax, (values, label) in zip(axes.flat, panels): + norm = mpl.colors.LogNorm() if label == "Number of galaxies" else None + mesh = ax.pcolormesh( + cells["snr_edges"], + cells["size_ratio_edges"], + values.T, + cmap=SEQUENTIAL, + norm=norm, + ) + ax.set_xscale("log") + ax.set_yscale("log") + ax.set_xlabel("SNR") + ax.set_ylabel("Galaxy / PSF T") + S.colorbar(fig, mesh, ax, label, pad=0.02) + return S.save(fig, out, "full" if layout == "wide" else "half") + + +def response_histogram(cells, out): + """Selected galaxies' metacal response components.""" + edges = cells["response_edges"] + with S.figure_style(): + fig, ax = plt.subplots(figsize=(3.6, 2.8)) + for hist, label in zip( + cells["response_hist"], (r"$R_{11}$", r"$R_{22}$", r"$R_{12}$", r"$R_{21}$") + ): + ax.stairs(hist, edges, label=label) + ax.set_yscale("log") + ax.set_xlabel("Galaxy shear response") + ax.set_ylabel("Number of galaxies") + ax.legend(ncol=2) + return S.save(fig, out, "half") + + def summary(cells): """Distinct values, grid size and whether every cell holds one weight.""" count = cells["count"] @@ -38,7 +87,7 @@ def _tt(text): return r"$\mathtt{" + text.replace("_", r"\_") + "}$" -def weights(cells, out, layout="wide"): +def _legacy_weights(cells, out, layout="wide"): """Galaxy weight over the SNR x size-ratio grid it is defined on. Colour is each cell's weight; the axis labels name the catalogue columns From 6c5538a131591065910e5c03fe8e78c2d7065401 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:11:11 -0400 Subject: [PATCH 30/53] Add correlation builder for the catalogue-paper tangential tests Streams the #425 cut catalogue and stars.hdf5 once, builds k-means jackknife patches, and measures galaxy shear around PSF stars (merged across exposures), tile centres and Gaia stars, the PSF field around galaxies (lambda_1, lambda_3, random-subtracted) and alpha(theta) in galaxy-magnitude bins. stage_gaia.py stages Gaia DR3 over the UNIONS tiles from a login node by source_id HEALPix ranges. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- scripts/dashboard/stage_gaia.py | 134 +++++ src/sp_validation/dashboard/correlations.py | 559 +++++++++++++++++++ src/sp_validation/tests/test_correlations.py | 96 ++++ 3 files changed, 789 insertions(+) create mode 100644 scripts/dashboard/stage_gaia.py create mode 100644 src/sp_validation/dashboard/correlations.py create mode 100644 src/sp_validation/tests/test_correlations.py diff --git a/scripts/dashboard/stage_gaia.py b/scripts/dashboard/stage_gaia.py new file mode 100644 index 000000000..821b3b914 --- /dev/null +++ b/scripts/dashboard/stage_gaia.py @@ -0,0 +1,134 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["pyvo", "astropy", "healpy", "numpy"] +# /// +"""Stage Gaia DR3 stars (G < 20, RUWE < 1.4) over the UNIONS tiles. + +Runs on a login node (the query executes on the TAP server); compute nodes +read the staged FITS. The region is every HEALPix level-7 (nside 128, +nested) pixel touched by a tile, from one or more tile lists: a text file of +``XXX.YYY`` names or a JSON manifest whose ``tiles`` mapping is keyed by name. +Gaia ``source_id`` encodes the level-12 nested pixel in its high bits, so each +level-7 pixel is a contiguous ``source_id`` range and the query is a union of +indexed range scans, run synchronously in chunks. Writes ``.fits`` and ``.json`` (ADQL template, +pixel list digest, row count). + +Usage:: + + uv run scripts/dashboard/stage_gaia.py --tiles tiles.txt MANIFEST.json \\ + --out catpaper/aux/gaia_dr3_G20_ruwe1p4_unions20k +""" + +import argparse +import hashlib +import json +import time +from pathlib import Path + +import healpy as hp +import numpy as np +import pyvo +from astropy.table import vstack + +TAP = "https://dc.zah.uni-heidelberg.de/tap" +TABLE = "gaia.dr3lite" +LEVEL = 7 +NSIDE = 2**LEVEL +SOURCE_ID_PER_PIXEL = 2**35 * 4 ** (12 - LEVEL) +TILE_HALF_DIAGONAL_DEG = 0.37 # 10000 px x 0.187"/px tiles, half-diagonal +ADQL = ( + "SELECT source_id, ra, dec, phot_g_mean_mag, ruwe FROM {table} " + "WHERE phot_g_mean_mag < 20 AND ruwe < 1.4 AND ({ranges})" +) + + +def read_tiles(path): + path = Path(path) + if path.suffix == ".json": + return list(json.loads(path.read_text())["tiles"]) + return [line.strip() for line in path.read_text().split() if line.strip()] + + +def tile_centre(name): + """CFIS tile centre (cs_util ``cfis.get_tile_coord_from_nixy``).""" + xi, yi = (int(part) for part in name.replace("-", ".").split(".")) + dec = yi / 2 - 90 + return xi / 2 / np.cos(np.deg2rad(dec)), dec + + +def tile_pixels(names): + pixels = set() + radius = np.deg2rad(TILE_HALF_DIAGONAL_DEG) + for name in names: + ra, dec = tile_centre(name) + vec = hp.ang2vec(ra, dec, lonlat=True) + pixels.update(hp.query_disc(NSIDE, vec, radius, inclusive=True, nest=True)) + return np.array(sorted(pixels)) + + +def ranges(pixels): + """Contiguous pixel runs as (first, last) pairs.""" + breaks = np.flatnonzero(np.diff(pixels) != 1) + starts = np.r_[pixels[0], pixels[breaks + 1]] + ends = np.r_[pixels[breaks], pixels[-1]] + return list(zip(starts, ends)) + + +def where(runs): + return " OR ".join( + f"source_id BETWEEN {a * SOURCE_ID_PER_PIXEL} " + f"AND {(b + 1) * SOURCE_ID_PER_PIXEL - 1}" + for a, b in runs + ) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--tiles", nargs="+", required=True) + parser.add_argument("--out", required=True, help="output path without suffix") + parser.add_argument("--pixels-per-query", type=int, default=300) + args = parser.parse_args(argv) + + names = sorted({n for path in args.tiles for n in read_tiles(path)}) + pixels = tile_pixels(names) + print(f"{len(names)} tiles -> {len(pixels)} level-{LEVEL} pixels", flush=True) + service = pyvo.dal.TAPService(TAP) + parts = [] + for start in range(0, len(pixels), args.pixels_per_query): + chunk = pixels[start : start + args.pixels_per_query] + query = ADQL.format(table=TABLE, ranges=where(ranges(chunk))) + for attempt in range(5): + try: + table = service.run_sync(query, maxrec=16_000_000).to_table() + break + except pyvo.dal.DALServiceError as error: + print(f"retry {attempt + 1}: {error}", flush=True) + time.sleep(30) + else: + raise RuntimeError(f"pixels {start}+ failed after retries") + print(f"pixels {start}-{start + len(chunk)}: {len(table)} rows", flush=True) + parts.append(table) + table = vstack(parts) + table.meta.clear() + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + table.write(out.with_suffix(".fits"), overwrite=True) + record = { + "service": TAP, + "table": TABLE, + "adql_template": ADQL.format(table=TABLE, ranges=""), + "healpix_level": LEVEL, + "nest": True, + "n_tiles": len(names), + "tile_lists": [str(Path(p).resolve()) for p in args.tiles], + "n_pixels": int(len(pixels)), + "pixels_sha256": hashlib.sha256(pixels.astype("_.npz`` with θ, the correlations, their +jackknife errors and joint covariance, pair counts and a JSON provenance. + +Usage (inside the container):: + + python -m sp_validation.dashboard.correlations \\ + --catalogue-dir run-r425/catalogue --version SP_v2.0 \\ + --out-dir catpaper/products --gaia gaia_dr3_G20_ruwe1p4_unions20k.fits +""" + +import argparse +import json +import os +import resource +import time +from pathlib import Path + +import fitsio +import healpy as hp +import numpy as np +import treecorr +from cs_util import cfis +from scipy.sparse import coo_matrix +from scipy.sparse.csgraph import connected_components +from scipy.spatial import cKDTree + +from sp_validation.io import read_catalogue +from sp_validation.statistics import jackknife_patch_centers + +from .position_shear import measure + +GALAXY_COLUMNS = ["RA", "Dec", "e1", "e2", "w_des", "mag", "R_g11", "R_g22"] +STAR_COLUMNS = [ + "RA", + "DEC", + "MAG", + "HSM_G1_PSF", + "HSM_G2_PSF", + "HSM_G1_STAR", + "HSM_G2_STAR", + "HSM_FLAG_PSF", + "HSM_FLAG_STAR", +] +MEASUREMENTS = ("stars", "lambda", "tiles", "gaia", "alpha") +GAIA_G_EDGES = (3.0, 17.9, 19.2, 20.0) +TC = dict(min_sep=1.0, max_sep=250.0, nbins=20) + + +def log(message): + peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6 + print(f"[{time.strftime('%H:%M:%S')} peak {peak:.1f} GB] {message}", flush=True) + + +def in_region(ra, dec, region): + if region is None: + return np.ones(len(ra), dtype=bool) + ra_min, ra_max, dec_min, dec_max = region + return (ra >= ra_min) & (ra < ra_max) & (dec >= dec_min) & (dec < dec_max) + + +def tile_name(tile_id): + """``XXX.YYY`` from a ``TILE_ID`` written as a float string (``51.34``).""" + x, _, y = str(tile_id).partition(".") + return f"{int(x):03d}.{y.ljust(3, '0')}" + + +def tile_centres(tile_ids): + """RA, Dec (deg) of the CFIS tile centres for unique ``TILE_ID`` values.""" + names = sorted({tile_name(t) for t in tile_ids}) + nix, niy = zip(*(cfis.get_tile_number(n) for n in names)) + ra, dec = cfis.get_tile_coord_from_nixy(list(nix), list(niy)) + return names, np.asarray(ra.deg), np.asarray(dec.deg) + + +def merge_detections(ra, dec, radius_arcsec): + """Group detections closer than ``radius_arcsec`` (friends of friends). + + Returns the group label of each detection and the number of groups. + """ + xyz = hp.ang2vec(ra, dec, lonlat=True) + chord = 2 * np.sin(np.deg2rad(radius_arcsec / 3600) / 2) + pairs = cKDTree(xyz).query_pairs(chord, output_type="ndarray") + n = len(ra) + graph = coo_matrix( + (np.ones(len(pairs), dtype=bool), (pairs[:, 0], pairs[:, 1])), shape=(n, n) + ) + return connected_components(graph, directed=False)[::-1] + + +def group_mean(labels, n_groups, values): + counts = np.bincount(labels, minlength=n_groups) + return np.bincount(labels, weights=values, minlength=n_groups) / counts + + +def unique_stars(stars, radius_arcsec): + """One position and magnitude per star from its per-exposure detections.""" + labels, n = merge_detections(stars["RA"], stars["DEC"], radius_arcsec) + xyz = hp.ang2vec(stars["RA"], stars["DEC"], lonlat=True) + mean = np.column_stack([group_mean(labels, n, xyz[:, i]) for i in range(3)]) + ra, dec = hp.vec2ang(mean, lonlat=True) + return ra, dec, group_mean(labels, n, stars["MAG"].astype(float)) + + +def read_galaxies(path, region, chunk=5_000_000): + """Galaxy columns (float64) and the set of tile IDs, streamed in chunks.""" + parts, tiles = {k: [] for k in GALAXY_COLUMNS}, set() + with fitsio.FITS(path) as fits: + table = fits[1] + for start in range(0, table.get_nrows(), chunk): + rows = table[GALAXY_COLUMNS + ["TILE_ID"]][start : start + chunk] + keep = in_region(rows["RA"], rows["Dec"], region) + for key in GALAXY_COLUMNS: + parts[key].append(rows[key][keep].astype(np.float64)) + tiles.update(np.unique(rows["TILE_ID"][keep]).tolist()) + return {k: np.concatenate(v) for k, v in parts.items()}, tiles + + +def read_stars(path, region): + stars = read_catalogue(path, columns=STAR_COLUMNS) + keep = (stars["HSM_FLAG_PSF"] == 0) & (stars["HSM_FLAG_STAR"] == 0) + keep &= in_region(stars["RA"], stars["DEC"], region) + return stars[keep] + + +def footprint_pixels(path): + """Occupied pixels (RING) and nside of an ``occupied-mask-nside*.npz``.""" + mask = np.load(path) + assert str(mask["ordering"]) == "RING" + return int(mask["nside"]), np.asarray(mask["pixel"]) + + +def in_footprint(ra, dec, nside, pixels): + return np.isin(hp.ang2pix(nside, ra, dec, lonlat=True), pixels) + + +def footprint_randoms(n, nside, pixels, region, rng, depth=6): + """Points uniform over the footprint pixels, to 1/2**depth of a pixel.""" + nest = hp.ring2nest(nside, pixels) + fine = nest[rng.integers(len(nest), size=n)] * 4**depth + fine += rng.integers(4**depth, size=n) + ra, dec = hp.pix2ang(nside * 2**depth, fine, nest=True, lonlat=True) + keep = in_region(ra, dec, region) + return ra[keep], dec[keep] + + +def catalog(ra, dec, centres, **fields): + return treecorr.Catalog( + ra=ra, dec=dec, ra_units="deg", dec_units="deg", patch_centers=centres, **fields + ) + + +def save(out, name, version, results, provenance): + """Write one measurement: ``results`` maps a label to a ``measure`` dict.""" + arrays = {} + for label, result in results.items(): + for key, value in result.items(): + arrays[f"{label}__{key}"] = np.asarray(value) + path = Path(out) / f"{name}_{version}.npz" + np.savez_compressed( + path, + labels=np.array(list(results)), + provenance=json.dumps(provenance), + **arrays, + ) + log(f"wrote {path}") + + +def quantile_edges(values, n): + """Edges of ``n`` equal-count bins; the last edge just above the maximum.""" + edges = np.quantile(values, np.linspace(0, 1, n + 1)) + edges[-1] = np.nextafter(edges[-1], np.inf) + return edges + + +def alpha_measurement(gal, stars, centres, n_bins, threads, r_selection): + """τ₀ per magnitude bin, ρ₀, and α = τ₀/ρ₀ with a joint jackknife covariance.""" + config = dict( + TC, sep_units="arcmin", var_method="jackknife", cross_patch_weight="match" + ) + psf = catalog( + stars["RA"], + stars["DEC"], + centres, + g1=stars["HSM_G1_PSF"], + g2=stars["HSM_G2_PSF"], + ) + rho0 = treecorr.GGCorrelation(config) + rho0.process(psf, psf, num_threads=threads) + log("rho0 done") + edges = quantile_edges(gal["mag"], n_bins) + selections = [("all", np.ones(len(gal["mag"]), dtype=bool))] + [ + (f"mag{i}", (gal["mag"] >= lo) & (gal["mag"] < hi)) + for i, (lo, hi) in enumerate(zip(edges[:-1], edges[1:])) + ] + r_global = None + taus, responses = [], {} + for label, sel in selections: + w = gal["w_des"][sel] + r_shear = np.array( + [ + np.average(gal["R_g11"][sel], weights=w), + np.average(gal["R_g22"][sel], weights=w), + ] + ) + r_bin = r_shear + r_selection + if r_global is None: + r_global = r_bin + # Calibrated e is (e_uncal - c)/R_global; rescale to the bin's response. + scale = r_global / r_bin + g = [] + for i, key in enumerate(("e1", "e2")): + e = gal[key][sel] * scale[i] + g.append(e - np.average(e, weights=w)) + cat = catalog(gal["RA"][sel], gal["Dec"][sel], centres, g1=g[0], g2=g[1], w=w) + tau0 = treecorr.GGCorrelation(config) + tau0.process(cat, psf, num_threads=threads) + taus.append(tau0) + responses[label] = { + "n": int(sel.sum()), + "R_shear": r_shear.tolist(), + "R": r_bin.tolist(), + "rescale": scale.tolist(), + } + del cat + log(f"tau0 {label} done") + n = TC["nbins"] + + def vector(corrs): + rho = corrs[0].xip + return np.concatenate([c.xip / rho for c in corrs[1:]]) + + def raw(corrs): + return np.concatenate([c.xip for c in corrs]) + + corrs = [rho0, *taus] + alpha = vector(corrs) + cov = treecorr.estimate_multi_cov(corrs, "jackknife", func=vector) + raw_cov = treecorr.estimate_multi_cov(corrs, "jackknife", func=raw) + results = {} + for i, (label, _) in enumerate(selections): + block = slice(i * n, (i + 1) * n) + results[label] = dict( + theta=np.exp(rho0.meanlogr), + alpha=alpha[block], + alpha_error=np.sqrt(np.diag(cov)[block]), + tau0=taus[i].xip, + tau0_error=np.sqrt(np.diag(raw_cov)[(i + 1) * n : (i + 2) * n]), + npairs=taus[i].npairs, + ) + results["rho0"] = dict( + theta=np.exp(rho0.meanlogr), + rho0=rho0.xip, + rho0_error=np.sqrt(np.diag(raw_cov)[:n]), + npairs=rho0.npairs, + ) + results["joint"] = dict(alpha_covariance=cov, raw_covariance=raw_cov) + provenance = { + "galaxy_mag_edges": edges.tolist(), + "responses": responses, + "R_selection_global": r_selection.tolist(), + "shapes": "calibrated e1, e2 rescaled by R_global/R_bin, weighted mean " + "subtracted per bin (as rho_tau TauStat); w_des", + "stars": "PSF validation stars, HSM_FLAG_STAR == 0 & HSM_FLAG_PSF == 0, " + "every exposure detection (as the dashboard rho/tau)", + "rho0": "xi+ of HSM_G*_PSF x HSM_G*_PSF", + "tau0": "xi+ of galaxy e x HSM_G*_PSF", + "raw_covariance_order": ["rho0"] + [s for s, _ in selections], + } + return results, provenance + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--catalogue-dir", required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--gaia", help="staged Gaia DR3 FITS (stage_gaia.py)") + parser.add_argument("--only", nargs="+", choices=MEASUREMENTS, default=None) + parser.add_argument( + "--region", + type=float, + nargs=4, + default=None, + metavar=("RA_MIN", "RA_MAX", "DEC_MIN", "DEC_MAX"), + help="restrict every sample to a box (pilot runs)", + ) + parser.add_argument("--npatch", type=int, default=100) + parser.add_argument( + "--lens-fraction", + type=float, + default=0.1, + help="galaxy subsample used as lenses for lambda", + ) + parser.add_argument( + "--random-factor", type=float, default=1.0, help="randoms per lambda lens" + ) + parser.add_argument("--dedup-arcsec", type=float, default=0.5) + parser.add_argument("--mag-bins", type=int, default=3) + parser.add_argument("--threads", type=int, default=len(os.sched_getaffinity(0))) + parser.add_argument("--seed", type=int, default=425) + args = parser.parse_args(argv) + todo = args.only or [m for m in MEASUREMENTS if m != "gaia" or args.gaia] + cdir = Path(args.catalogue_dir) + out = Path(args.out_dir) + out.mkdir(parents=True, exist_ok=True) + rng = np.random.default_rng(args.seed) + base = { + "version": args.version, + "catalogue_dir": str(cdir.resolve()), + "region": args.region, + "npatch": args.npatch, + "treecorr": dict( + TC, sep_units="arcmin", var_method="jackknife", version=treecorr.__version__ + ), + "units": "theta in arcmin; shears dimensionless", + } + timings = {} + + t0 = time.time() + gal, tile_ids = read_galaxies(cdir / "shape_catalog_cut_ngmix.fits", args.region) + log(f"{len(gal['RA'])} galaxies, {len(tile_ids)} tiles") + stars = read_stars(cdir / "stars.hdf5", args.region) + log(f"{len(stars)} valid-HSM star detections") + timings["read"] = time.time() - t0 + + patch_path = out / f"patches_{args.version}.npz" + if patch_path.exists(): + centres = np.load(patch_path)["centres"] + assert len(centres) == args.npatch, patch_path + else: + every = max(1, len(gal["RA"]) // 2_000_000) + sub = treecorr.Catalog( + ra=gal["RA"][::every], + dec=gal["Dec"][::every], + ra_units="deg", + dec_units="deg", + ) + centres = jackknife_patch_centers(sub, args.npatch, seed=args.seed) + np.savez( + patch_path, + centres=centres, + npatch=args.npatch, + every_nth=every, + seed=args.seed, + region=str(args.region), + ) + log(f"patches -> {patch_path}") + + sources = None + if {"stars", "tiles", "gaia"} & set(todo): + sources = catalog( + gal["RA"], gal["Dec"], centres, g1=gal["e1"], g2=gal["e2"], w=gal["w_des"] + ) + nside, pixels = footprint_pixels(cdir / "occupied-mask-nside4096.npz") + + if "stars" in todo: + t = time.time() + ra, dec, mag = unique_stars(stars, args.dedup_arcsec) + log(f"{len(ra)} unique stars from {len(stars)} detections") + edges = quantile_edges(mag, args.mag_bins) + results = { + "all": measure( + catalog(ra, dec, centres), sources, num_threads=args.threads, **TC + ) + } + for i, (lo, hi) in enumerate(zip(edges[:-1], edges[1:])): + sel = (mag >= lo) & (mag < hi) + results[f"mag{i}"] = measure( + catalog(ra[sel], dec[sel], centres), + sources, + num_threads=args.threads, + **TC, + ) + timings["stars"] = time.time() - t + save( + out, + "gamma_stars", + args.version, + results, + dict( + base, + lenses="PSF validation stars (valid HSM), detections merged within " + f"{args.dedup_arcsec} arcsec", + n_lenses=len(ra), + n_detections=len(stars), + star_mag_edges=edges.tolist(), + sources="galaxies, calibrated e1/e2, w_des", + random_subtraction=False, + seconds=timings["stars"], + ), + ) + + if "tiles" in todo: + t = time.time() + names, ra, dec = tile_centres(tile_ids) + results = { + "all": measure( + catalog(ra, dec, centres), sources, num_threads=args.threads, **TC + ) + } + timings["tiles"] = time.time() - t + save( + out, + "gamma_tiles", + args.version, + results, + dict( + base, + lenses="CFIS tile centres of the tiles in the catalogue", + n_lenses=len(names), + sources="galaxies, calibrated e1/e2, w_des", + random_subtraction=False, + seconds=timings["tiles"], + ), + ) + + randoms = None + if {"lambda", "gaia"} & set(todo): + n_lens = int(args.lens_fraction * len(gal["RA"])) + rra, rdec = footprint_randoms( + int(args.random_factor * n_lens * 1.05), nside, pixels, args.region, rng + ) + randoms = catalog( + rra[: int(args.random_factor * n_lens)], + rdec[: int(args.random_factor * n_lens)], + centres, + ) + log(f"{randoms.ntot} randoms") + + if "gaia" in todo: + t = time.time() + gaia = fitsio.read(args.gaia, columns=["ra", "dec", "phot_g_mean_mag", "ruwe"]) + keep = (gaia["phot_g_mean_mag"] < 20) & (gaia["ruwe"] < 1.4) + keep &= in_region(gaia["ra"], gaia["dec"], args.region) + keep &= in_footprint(gaia["ra"], gaia["dec"], nside, pixels) + gaia = gaia[keep] + g = gaia["phot_g_mean_mag"] + results, counts = {}, {} + for i, (lo, hi) in enumerate(zip(GAIA_G_EDGES[:-1], GAIA_G_EDGES[1:])): + sel = (g >= lo) & (g < hi) + counts[f"G{i}"] = int(sel.sum()) + results[f"G{i}"] = measure( + catalog( + gaia["ra"][sel].astype(float), + gaia["dec"][sel].astype(float), + centres, + ), + sources, + randoms=randoms, + num_threads=args.threads, + **TC, + ) + log(f"gaia G{i} done") + timings["gaia"] = time.time() - t + save( + out, + "gamma_gaia", + args.version, + results, + dict( + base, + lenses="Gaia DR3, G < 20, RUWE < 1.4, inside the occupied " + "nside-4096 footprint", + gaia_file=str(Path(args.gaia).resolve()), + g_edges=list(GAIA_G_EDGES), + n_lenses=counts, + n_randoms=randoms.ntot, + sources="galaxies, calibrated e1/e2, w_des", + random_subtraction=True, + seconds=timings["gaia"], + ), + ) + + del sources + if "lambda" in todo: + t = time.time() + pick = rng.random(len(gal["RA"])) < args.lens_fraction + lenses = catalog(gal["RA"][pick], gal["Dec"][pick], centres) + results = {} + for label, g1, g2 in ( + ("psf", stars["HSM_G1_PSF"], stars["HSM_G2_PSF"]), + ( + "residual", + stars["HSM_G1_STAR"] - stars["HSM_G1_PSF"], + stars["HSM_G2_STAR"] - stars["HSM_G2_PSF"], + ), + ): + src = catalog(stars["RA"], stars["DEC"], centres, g1=g1, g2=g2) + results[label] = measure( + lenses, src, randoms=randoms, num_threads=args.threads, **TC + ) + log(f"lambda {label} done") + timings["lambda"] = time.time() - t + save( + out, + "lambda_stars", + args.version, + results, + dict( + base, + lenses=f"random {args.lens_fraction:g} of the galaxies", + n_lenses=lenses.ntot, + n_randoms=randoms.ntot, + sources="PSF validation star detections (valid HSM): psf = HSM_G*_PSF " + "(lambda_1), residual = HSM_G*_STAR - HSM_G*_PSF (lambda_3)", + n_sources=len(stars), + random_subtraction=True, + seconds=timings["lambda"], + ), + ) + del lenses + + if "alpha" in todo: + t = time.time() + calib = json.loads((cdir / "calibration.json").read_text()) + r_sel = np.diag(np.asarray(calib["R_selection"])) + results, prov = alpha_measurement( + gal, stars, centres, args.mag_bins, args.threads, r_sel + ) + timings["alpha"] = time.time() - t + save( + out, + "alpha_mag", + args.version, + results, + dict(base, **prov, seconds=timings["alpha"]), + ) + + log(f"timings (s): {json.dumps({k: round(v) for k, v in timings.items()})}") + + +if __name__ == "__main__": + main() diff --git a/src/sp_validation/tests/test_correlations.py b/src/sp_validation/tests/test_correlations.py new file mode 100644 index 000000000..3476cb6c1 --- /dev/null +++ b/src/sp_validation/tests/test_correlations.py @@ -0,0 +1,96 @@ +import healpy as hp +import numpy as np +import pytest + +from sp_validation.dashboard import correlations as C +from sp_validation.dashboard import products as P +from sp_validation.dashboard import tangential as TG + + +@pytest.mark.parametrize( + "tile_id, name", + [ + ("51.344", "051.344"), + ("51.34", "051.340"), + ("251.3", "251.300"), + ("7.312", "007.312"), + ], +) +def test_tile_name_restores_float_formatted_ids(tile_id, name): + assert C.tile_name(tile_id) == name + + +def test_tile_centres_follow_cfis_grid(): + names, ra, dec = C.tile_centres(["51.344", "51.344", "180.300"]) + assert names == ["051.344", "180.300"] + np.testing.assert_allclose(dec, [344 / 2 - 90, 300 / 2 - 90]) + np.testing.assert_allclose(ra[0], 51 / 2 / np.cos(np.deg2rad(82))) + + +def test_merge_detections_groups_repeat_exposures(): + rng = np.random.default_rng(1) + ra0 = rng.uniform(150, 151, 50) + dec0 = rng.uniform(40, 41, 50) + # Three detections per star, jittered by < 0.1 arcsec. + ra = np.repeat(ra0, 3) + rng.normal(0, 0.03 / 3600, 150) + dec = np.repeat(dec0, 3) + rng.normal(0, 0.03 / 3600, 150) + stars = np.zeros(150, dtype=[("RA", "f8"), ("DEC", "f8"), ("MAG", "f4")]) + stars["RA"], stars["DEC"], stars["MAG"] = ra, dec, np.repeat(np.arange(50), 3) + ura, udec, umag = C.unique_stars(stars, 0.5) + assert len(ura) == 50 + order = np.argsort(umag) + np.testing.assert_allclose(umag[order], np.arange(50)) + np.testing.assert_allclose(udec[order], dec0, atol=1e-4) + + +def test_quantile_edges_split_equal_counts(): + values = np.arange(300, dtype=float) + edges = C.quantile_edges(values, 3) + counts = np.histogram(values, edges)[0] + np.testing.assert_array_equal(counts, [100, 100, 100]) + + +def test_footprint_randoms_stay_in_footprint(): + nside = 64 + pixels = np.array([hp.ang2pix(nside, 200.0, 45.0, lonlat=True)]) + ra, dec = C.footprint_randoms(5000, nside, pixels, None, np.random.default_rng(0)) + assert len(ra) == 5000 + assert np.all(hp.ang2pix(nside, ra, dec, lonlat=True) == pixels[0]) + + +def test_chi2_null_hartlap_and_guard(): + cov = np.eye(4) + chi2, nu = TG.chi2_null(np.ones(4), cov, npatch=100) + assert nu == 4 + np.testing.assert_allclose(chi2, 4 * (100 - 6) / 99) + assert np.isnan(TG.chi2_null(np.ones(4), cov, npatch=5)[0]) + + +def _result(n=20, amplitude=1e-4): + theta = np.geomspace(1, 250, n) + return dict( + theta=theta, + xi_t=np.full(n, amplitude), + xi_x=np.zeros(n), + error_t=np.full(n, amplitude), + error_x=np.full(n, amplitude), + covariance=np.eye(2 * n) * amplitude**2, + npairs=np.ones(n), + weight=np.ones(n), + ) + + +def test_save_load_and_render_roundtrip(tmp_path): + prov = dict(npatch=100, n_lenses=10, star_mag_edges=[17.0, 19.0, 20.0, 22.0]) + C.save( + tmp_path, + "gamma_stars", + "v", + {k: _result() for k in ("all", "mag0", "mag1", "mag2")}, + prov, + ) + product = P.load_correlation(tmp_path / "gamma_stars_v.npz") + assert product["provenance"]["npatch"] == 100 + np.testing.assert_allclose(product["mag1"]["xi_t"], 1e-4) + out = TG.gamma_stars(product, tmp_path / "stars.png") + assert out.exists() From 2b548f1d5ec412c505d02aa4642d4dd9dc990222 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:29 -0400 Subject: [PATCH 31/53] fix(dashboard): use logarithmic PSF-systematic scale when positive --- src/sp_validation/dashboard/psf.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sp_validation/dashboard/psf.py b/src/sp_validation/dashboard/psf.py index 613eeef78..6887b7b65 100644 --- a/src/sp_validation/dashboard/psf.py +++ b/src/sp_validation/dashboard/psf.py @@ -291,9 +291,12 @@ def xi_sys_comparison(rt, out): ms=3, label=r"$\tau_{0,+}^2/\rho_{0,+}$", ) - S.zero_line(ax) S.theta_axis(ax) - ax.set_yscale("symlog", linthresh=1e-8) + if np.all(np.concatenate((lo, med, hi, simple)) > 0): + ax.set_yscale("log") + else: + S.zero_line(ax) + ax.set_yscale("symlog", linthresh=1e-8) ax.set_xlabel(r"$\theta$ [arcmin]") ax.set_ylabel(r"$\xi^{\rm sys}_+(\theta)$") ax.legend() From 3da891f6f9799f964d122de8371dec83ed037f9f Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:30 -0400 Subject: [PATCH 32/53] feat(dashboard): stamp selection histogram products --- src/sp_validation/dashboard/selection_hist.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/sp_validation/dashboard/selection_hist.py b/src/sp_validation/dashboard/selection_hist.py index 6dbe1b47c..da33b19fb 100644 --- a/src/sp_validation/dashboard/selection_hist.py +++ b/src/sp_validation/dashboard/selection_hist.py @@ -101,8 +101,15 @@ def main(): p.add_argument("--config", required=True) p.add_argument("--expected", type=int, required=True) p.add_argument("--out", required=True) + p.add_argument("--version", required=True) args = p.parse_args() - np.savez_compressed(args.out, **build(args.catalogue, args.config, args.expected)) + np.savez_compressed( + args.out, + version=args.version, + selection="cumulative config_mask cuts and noshear metacal cuts", + units="r-band magnitude, AB", + **build(args.catalogue, args.config, args.expected), + ) if __name__ == "__main__": From 460971e337d6df09e276fcea5533bc59ded20194 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:31 -0400 Subject: [PATCH 33/53] test: align schema and rho-tau fixture with dashboard inputs --- config/columns/shapepipe_v2.yaml | 10 ++++++++++ .../test_tomo_rho_tau_driver_external_checkout.py | 1 + src/sp_validation/tests/test_column_schema.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/config/columns/shapepipe_v2.yaml b/config/columns/shapepipe_v2.yaml index 7d9c74ecb..70d379da6 100644 --- a/config/columns/shapepipe_v2.yaml +++ b/config/columns/shapepipe_v2.yaml @@ -110,6 +110,16 @@ star: DEC: Declination [deg]. HSM_G1_PSF: PSF model ellipticity g1 (HSM). HSM_G2_PSF: PSF model ellipticity g2 (HSM). + HSM_G1_STAR: Stellar ellipticity g1 (HSM). + HSM_G2_STAR: Stellar ellipticity g2 (HSM). + HSM_T_PSF: PSF model size T (HSM). + HSM_T_STAR: Stellar size T (HSM). + HSM_FLAG_PSF: HSM PSF measurement flag. + HSM_FLAG_STAR: HSM stellar measurement flag. + HSM_M4_1_PSF: PSF model fourth-moment component 1. + HSM_M4_2_PSF: PSF model fourth-moment component 2. + HSM_M4_1_STAR: Stellar fourth-moment component 1. + HSM_M4_2_STAR: Stellar fourth-moment component 2. shear: about: >- diff --git a/src/sp_validation/tests/regression/test_tomo_rho_tau_driver_external_checkout.py b/src/sp_validation/tests/regression/test_tomo_rho_tau_driver_external_checkout.py index 8343238fe..fe7ee8434 100644 --- a/src/sp_validation/tests/regression/test_tomo_rho_tau_driver_external_checkout.py +++ b/src/sp_validation/tests/regression/test_tomo_rho_tau_driver_external_checkout.py @@ -53,6 +53,7 @@ def fake_cv(*args, **kwargs): "max_sep": "250.0", "nbins": "20", "npatch": "20", + "cov_estimate_method": "jackknife", "cat_config": str(checkout / "cosmo_val" / "cat_config.yaml"), "output_dir": str(checkout / "cosmo_val" / "output"), }, diff --git a/src/sp_validation/tests/test_column_schema.py b/src/sp_validation/tests/test_column_schema.py index da073b802..1fbabc48c 100644 --- a/src/sp_validation/tests/test_column_schema.py +++ b/src/sp_validation/tests/test_column_schema.py @@ -48,6 +48,10 @@ "TTYPE{}", # pseudo-Cl, COSEBIs and SACC products "BB", + "EE", + "BB_BB", + "EB_EB", + "EE_EE", "COSEBIS", "COVAR_BB_BB", "COVAR_EE_EE", From 89b6234e5d0cc5c57da6a46802b8b1ee24979e37 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:32 -0400 Subject: [PATCH 34/53] refactor(dashboard): simplify weight figure and phone layout --- src/sp_validation/dashboard/weights.py | 88 ++------------------------ 1 file changed, 5 insertions(+), 83 deletions(-) diff --git a/src/sp_validation/dashboard/weights.py b/src/sp_validation/dashboard/weights.py index 2f44b5c14..31006d911 100644 --- a/src/sp_validation/dashboard/weights.py +++ b/src/sp_validation/dashboard/weights.py @@ -1,11 +1,4 @@ -"""What the galaxy weights are. - -The shape weight is built per cell of a log-spaced SNR x size-ratio grid -(:func:`sp_validation.calibration.get_w_des`): every galaxy in a cell gets -that cell's R^2 / sigma_e^2. The weights therefore take at most one value per -occupied cell. The figure shows the weight over the grid, with each cell's -share of galaxies, and the resulting discrete distribution. -""" +"""SNR–size weight-cell diagnostics and shear-response distributions.""" import json @@ -30,9 +23,10 @@ def weights(cells, out, layout="wide"): (noise, r"$\sqrt{\langle e_1^2+e_2^2\rangle/2}$"), ] with S.figure_style(): - fig, axes = plt.subplots( - 2, 2, figsize=(7.2, 5.8) if layout == "wide" else (3.6, 4.5) - ) + if layout == "wide": + fig, axes = plt.subplots(2, 2, figsize=(7.2, 5.8)) + else: + fig, axes = plt.subplots(4, 1, figsize=(3.6, 9.5)) for ax, (values, label) in zip(axes.flat, panels): norm = mpl.colors.LogNorm() if label == "Number of galaxies" else None mesh = ax.pcolormesh( @@ -80,75 +74,3 @@ def summary(cells): "w_max": float(cells["w_values"].max()), "out_of_range": json.loads(str(cells["out_of_range"])), } - - -def _tt(text): - """Catalogue column names in typewriter mathtext.""" - return r"$\mathtt{" + text.replace("_", r"\_") + "}$" - - -def _legacy_weights(cells, out, layout="wide"): - """Galaxy weight over the SNR x size-ratio grid it is defined on. - - Colour is each cell's weight; the axis labels name the catalogue columns - binned and the colour-bar label defines the weight. - """ - snr, size = cells["snr_edges"], cells["size_ratio_edges"] - count = cells["count"] - w = np.where(count > 0, cells["w_min"], np.nan) - log_fmt = mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$") - with S.figure_style(): - wide = layout == "wide" - fig, g = plt.subplots(figsize=(7.2, 3.7) if wide else (3.6, 4.4)) - mesh = g.pcolormesh( - snr, size, w.T, cmap=SEQUENTIAL, edgecolors=S.RAG, linewidth=0.3 - ) - g.set_xscale("log") - g.set_yscale("log") - g.set_xlim(snr[0], snr[-1]) - g.set_ylim(size[0], size[-1]) - sep = ": " if wide else "\n" - g.set_xlabel( - "signal-to-noise ratio" - + sep - + _tt("snr = NGMIX_FLUX_NOSHEAR / NGMIX_FLUX_ERR_NOSHEAR"), - fontsize=10 if wide else 7.5, - ) - g.set_ylabel( - "size ratio" + sep + _tt("NGMIX_T_NOSHEAR / NGMIX_T_PSF_RECONV_NOSHEAR"), - fontsize=8.5 if wide else 7.5, - ) - g.xaxis.set_major_formatter(log_fmt) - g.xaxis.set_minor_formatter( - mpl.ticker.FuncFormatter(lambda v, _: f"${v:g}$" if v in (30, 300) else "") - ) - g.yaxis.set_major_formatter(log_fmt) - g.yaxis.set_minor_formatter( - mpl.ticker.FuncFormatter( - lambda v, _: f"${v:g}$" if v in (0.8, 1.5, 2, 3) else "" - ) - ) - S.frame_only(g) - S.header( - g, - rf"{_tt('w_des')}: one weight per cell" - + ( - rf", ${count.shape[0]} \times {count.shape[1]}$ log-spaced cells" - if wide - else "" - ), - ) - S.colorbar( - fig, - mesh, - g, - r"$w = \bar R^{2} / \sigma_e^{2}$" + "\n" - r"$\bar R = \frac{1}{2}\langle R_{11} + R_{22}\rangle$ (metacal)," - "\n" - r"$\sigma_e^{2} = \frac{1}{2}\langle e_1^{2} + e_2^{2}\rangle$" - " (uncalibrated), per cell", - aspect=22, - pad=0.02, - location="right" if wide else "bottom", - ) - return S.save(fig, out, "full" if wide else "half") From 2bb18c865fd22497c5ae021d41c4cd8d1a26bc06 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:39 -0400 Subject: [PATCH 35/53] feat(dashboard): use quantile leakage bins and standardized bias slopes --- src/sp_validation/dashboard/object_bias.py | 77 +++++++++++++++++----- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/src/sp_validation/dashboard/object_bias.py b/src/sp_validation/dashboard/object_bias.py index 8683b63bf..9d1bfbcd1 100644 --- a/src/sp_validation/dashboard/object_bias.py +++ b/src/sp_validation/dashboard/object_bias.py @@ -2,6 +2,9 @@ import matplotlib.pyplot as plt import numpy as np +from matplotlib.collections import PolyCollection +from matplotlib.patches import Rectangle +from matplotlib.ticker import MaxNLocator, ScalarFormatter from . import style as S from .sky import DIVERGING @@ -14,24 +17,57 @@ def slopes(sums): return (sxy - sx * sy / sw) / (sxx - sx * sx / sw) +def leakage_fit(sums): + """Component WLS slopes and conventional residual-based fit errors.""" + values = np.array([slopes(s) for s in sums]) + sw, sx, sy, sxx, sxy, syy, count = np.moveaxis(sums, 1, 0) + with np.errstate(divide="ignore", invalid="ignore"): + sxxc = sxx - sx * sx / sw + rss = syy - sy * sy / sw - values * (sxy - sx * sy / sw) + errors = np.sqrt(np.maximum(rss, 0) / (count - 2) / sxxc) + return values, errors + + def leakage(cells, out): """Weighted component-wise PSF-ellipticity regressions in each cell.""" - slopes_by_component = np.array([slopes(s) for s in cells["leakage_sums"]]) + values, errors = leakage_fit(cells["leakage_sums"]) + redges, sedges = cells["resolution_edges"], cells["leakage_snr_edges"] + limit = np.nanpercentile(np.abs(values), 95) with S.figure_style(): fig, axes = plt.subplots(1, 2, figsize=(7.2, 3)) - for ax, values, title in zip( - axes, slopes_by_component, (r"$\alpha_1$", r"$\alpha_2$") + for ax, val, err, title in zip( + axes, values, errors, (r"$\alpha_1$", r"$\alpha_2$") ): - limit = np.nanpercentile(np.abs(values), 98) - mesh = ax.pcolormesh( - cells["snr_edges"], - cells["resolution_edges"], - values.T, + polygons = [] + for i in range(len(redges) - 1): + for j in range(sedges.shape[1] - 1): + x0, x1 = sedges[i, j : j + 2] + y0, y1 = redges[i : i + 2] + polygons.append([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) + if not np.isfinite(err[i, j]) or abs(val[i, j]) < 2 * err[i, j]: + ax.add_patch( + Rectangle( + (x0, y0), + x1 - x0, + y1 - y0, + fill=False, + hatch="///", + edgecolor="#777777", + lw=0, + zorder=3, + ) + ) + mesh = PolyCollection( + polygons, + array=val.ravel(), cmap=DIVERGING, - vmin=-limit, - vmax=limit, + clim=(-limit, limit), + edgecolors="none", ) + ax.add_collection(mesh) ax.set_xscale("log") + ax.set_xlim(sedges.min(), sedges.max()) + ax.set_ylim(redges[0], redges[-1]) ax.set_xlabel("SNR") ax.set_ylabel(r"$r_{\rm PSF}/(r_{\rm PSF}+r_{\rm gal})$") S.colorbar(fig, mesh, ax, title) @@ -40,22 +76,29 @@ def leakage(cells, out): def bias_fit(s): """Fit weighted mean ellipticity versus property across populated bins.""" - sw, sx, sy, _, _, syy, sw2, count = s + sw, sx, sy, sxx, _, _, sw2, count, sw2y, sw2yy = s with np.errstate(divide="ignore", invalid="ignore"): x, y = sx / sw, sy / sw - var = np.maximum(syy / sw - y * y, 0) * sw2 / (sw * sw) + var = np.maximum(sw2yy - 2 * y * sw2y + y * y * sw2, 0) / (sw * sw) ok = (count > 1) & np.isfinite(x) & np.isfinite(y) & (var > 0) + if ok.sum() < 3: + return np.nan, np.nan + mean = np.sum(sx[ok]) / np.sum(sw[ok]) + std = np.sqrt(max(np.sum(sxx[ok]) / np.sum(sw[ok]) - mean**2, 0)) x, y, weight = x[ok], y[ok], 1 / var[ok] if len(x) < 3: return np.nan, np.nan x -= np.average(x, weights=weight) den = np.sum(weight * x * x) - return np.sum(weight * x * y) / den, np.sqrt(1 / den) + return std * np.sum(weight * x * y) / den, std * np.sqrt(1 / den) def bias(cells, out): """Slopes and fit significances for weighted binned means.""" - names = [r"galaxy $\sqrt{T}$" if name == "galaxy size" else name for name in cells["bias_names"]] + names = [ + r"galaxy $\sqrt{T}$" if name == "galaxy size" else name + for name in cells["bias_names"] + ] fits = np.array([[bias_fit(s) for s in row] for row in cells["bias_sums"]]) with S.figure_style(): fig, axes = plt.subplots(1, 2, figsize=(7.2, 4.5), sharey=True) @@ -77,7 +120,11 @@ def bias(cells, out): for ax in axes: ax.axvline(0, color=S.INK_MUTED, lw=0.7) axes[0].set_yticks(y, names) - axes[0].set_xlabel("Fitted slope") + axes[0].set_xlabel(r"$\Delta e$ per $1\sigma$ of property") + formatter = ScalarFormatter(useMathText=True) + formatter.set_powerlimits((-2, 2)) + axes[0].xaxis.set_major_formatter(formatter) + axes[0].xaxis.set_major_locator(MaxNLocator(5)) axes[1].set_xlabel("Slope / fit uncertainty") axes[0].legend() return S.save(fig, out, "full") From 9102eb7f2dbc7eb070c16eb3f2c751d1829f8db4 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:40 -0400 Subject: [PATCH 36/53] feat(dashboard): pair star-density and magnitude sky maps --- src/sp_validation/dashboard/sky.py | 54 +++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/sp_validation/dashboard/sky.py b/src/sp_validation/dashboard/sky.py index 41342d088..b67d39c40 100644 --- a/src/sp_validation/dashboard/sky.py +++ b/src/sp_validation/dashboard/sky.py @@ -380,6 +380,12 @@ def quantity(maps, name, nside_out, footprint=None): elif name in ("e1", "e2"): values = _ratio(sums[f"sum_w_{name}"], sums["sum_w"]) keep = sums["n_gal"] > 0 + elif name == "star_density": + values = sums["n_star"] / area + keep = sums["n_star"] >= 5 + elif name == "mag": + values = _ratio(sums["sum_mag"], sums["n_gal"]) + keep = sums["n_gal"] > 0 elif name == "fwhm_psf": values = _ratio(sums["sum_w_fwhm_psf"], sums["sum_w"]) keep = sums["n_gal"] > 0 @@ -390,7 +396,9 @@ def quantity(maps, name, nside_out, footprint=None): else: raise KeyError(name) # Drop sparsely filled pixels (footprint edges), whose means are noise. - n = sums["n_star" if name in ("de1", "de2", "dT_over_T") else "n_gal"] + n = sums[ + "n_star" if name in ("de1", "de2", "dT_over_T", "star_density") else "n_gal" + ] keep &= n >= 0.3 * np.median(n[n > 0]) return pix[keep], values[keep] @@ -476,6 +484,50 @@ def _aspect(parts): } +def paper_map_pair(maps, out, which): + """Draw the catalogue paper's two related sky-map quantities.""" + fields = { + "stars": [ + ("star_density", "Star detections [arcmin$^{-2}$]", False), + ("dT_over_T", r"$\langle(T_\star-T_{\rm PSF})/T_{\rm PSF}\rangle$", True), + ], + "magnitude": [ + ("fwhm_psf", "PSF FWHM [arcsec]", False), + ("mag", "Mean galaxy r magnitude", False), + ], + } + gal_pix = maps["pixels"][maps["n_gal"] > 0] + parts = make_parts(gal_pix, int(maps["nside"])) + nside = 256 + with S.figure_style(): + fig = plt.figure(figsize=(7.2, 3.6 * _aspect(parts) + 0.6)) + gs = fig.add_gridspec(1, 2, wspace=0.05) + for cell, (name, label, symmetric) in zip(gs, fields[which]): + pix, val = quantity(maps, name, nside) + inside = np.isin( + pix, np.unique(gal_pix // (int(maps["nside"]) // nside) ** 2) + ) + pix, val = pix[inside], val[inside] + lo, hi = _robust_limits(val, symmetric) + norm = ( + TwoSlopeNorm(0, lo, hi) if symmetric else mpl.colors.Normalize(lo, hi) + ) + axes = map_grid(fig, cell, parts) + for ax, part in zip(axes, parts): + im = draw_map( + ax, + part, + pix, + val, + nside, + DIVERGING if symmetric else SEQUENTIAL, + norm, + labels=False, + ) + _colorbar(fig, im, axes, label, symmetric) + return S.save(fig, out, "full") + + def field_maps(maps, out, which, footprint=None, layout="wide"): """2 x 2 small multiples of map quantities over the main footprint part. From d364a09aa47d1f19d45f61c399a040332c52f276 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:40 -0400 Subject: [PATCH 37/53] feat(dashboard): stream catalogue figure sufficient statistics --- src/sp_validation/dashboard/field_maps.py | 164 +++++++++++++++++- .../tests/test_dashboard_figures.py | 87 +++++++++- 2 files changed, 248 insertions(+), 3 deletions(-) diff --git a/src/sp_validation/dashboard/field_maps.py b/src/sp_validation/dashboard/field_maps.py index 3cfabe6af..e71beda27 100644 --- a/src/sp_validation/dashboard/field_maps.py +++ b/src/sp_validation/dashboard/field_maps.py @@ -40,13 +40,33 @@ "w_des", "e1", "e2", + "e1_uncal", + "e2_uncal", + "R_g11", + "R_g12", + "R_g21", + "R_g22", "e1_PSF", "e2_PSF", "fwhm_PSF", + "mag", "snr", "NGMIX_T_NOSHEAR", "NGMIX_T_PSF_RECONV_NOSHEAR", ] +BIAS_EDGES = { + "PSF e1": np.linspace(-0.2, 0.2, 21), + "PSF e2": np.linspace(-0.2, 0.2, 21), + "PSF FWHM": np.linspace(0.3, 2.0, 21), + "r magnitude": np.linspace(18, 27, 21), + "SNR": np.geomspace(10, 500, 21), + "size ratio": np.linspace(0.707, 3, 21), + "galaxy size": np.linspace(0, 5, 21), + "RA": np.linspace(0, 360, 21), + "Dec": np.linspace(-30, 90, 21), + "weight": np.linspace(0, 50, 21), +} + STAR_COLUMNS = [ "RA", "DEC", @@ -56,6 +76,10 @@ "HSM_G1_STAR", "HSM_G2_STAR", "HSM_T_STAR", + "HSM_M4_1_STAR", + "HSM_M4_2_STAR", + "HSM_M4_1_PSF", + "HSM_M4_2_PSF", "HSM_FLAG_PSF", "HSM_FLAG_STAR", ] @@ -69,6 +93,7 @@ "sum_w_e1_psf", "sum_w_e2_psf", "sum_w_fwhm_psf", + "sum_mag", ] STAR_SUMS = [ "n_star", @@ -103,6 +128,36 @@ def _accumulate(total, pixels, weights, npix): total += np.bincount(pixels, weights=weights, minlength=npix) +def leakage_cells(data, n_bins=8): + """Equal-count resolution bins, then equal-count SNR within each bin.""" + valid = np.all(np.isfinite(data), axis=1) & (data[:, 2] > 0) + data = data[valid] + resolution_edges = np.quantile(data[:, 1], np.linspace(0, 1, n_bins + 1)) + if np.any(np.diff(resolution_edges) <= 0): + raise ValueError("resolution quantiles are not distinct") + rbin = np.clip( + np.searchsorted(resolution_edges, data[:, 1], side="right") - 1, 0, n_bins - 1 + ) + snr_edges = np.zeros((n_bins, n_bins + 1)) + sums = np.zeros((2, 7, n_bins, n_bins)) + for r in range(n_bins): + d = data[rbin == r].astype(np.float64) + snr_edges[r] = np.quantile(d[:, 0], np.linspace(0, 1, n_bins + 1)) + if np.any(np.diff(snr_edges[r]) <= 0): + raise ValueError("SNR quantiles are not distinct") + idx = np.clip( + np.searchsorted(snr_edges[r], d[:, 0], side="right") - 1, 0, n_bins - 1 + ) + w = d[:, 2] + for component in range(2): + x, y = d[:, 3 + 2 * component], d[:, 4 + 2 * component] + for k, v in enumerate( + (w, w * x, w * y, w * x * x, w * x * y, w * y * y, np.ones_like(w)) + ): + sums[component, k, r] = np.bincount(idx, weights=v, minlength=n_bins) + return resolution_edges, snr_edges, sums + + def galaxy_products(path, nside, n_bins, snr_range, size_ratio_range, chunk): """Stream the galaxy catalogue once into map sums and weight-cell tallies.""" npix = hp.nside2npix(nside) @@ -113,10 +168,17 @@ def galaxy_products(path, nside, n_bins, snr_range, size_ratio_range, chunk): cell_w_max = np.full((n_bins, n_bins), -np.inf) out_of_range = {"snr_low": 0, "snr_high": 0, "size_low": 0, "size_high": 0} weights = [] + cell_response = np.zeros((n_bins, n_bins)) + cell_ee = np.zeros((n_bins, n_bins)) + response_edges = np.linspace(-5, 5, 501) + response_hist = np.zeros((4, 500), dtype=np.int64) + response_outside = np.zeros(4, dtype=np.int64) + bias_sums = np.zeros((len(BIAS_EDGES), 2, 10, 20)) with fitsio.FITS(path) as fits: table = fits[1] n_rows = table.get_nrows() + leakage_data = np.empty((n_rows, 7), dtype=np.float32) for start in range(0, n_rows, chunk): rows = table[GALAXY_COLUMNS][start : start + chunk] pix = hp.ang2pix(nside, rows["RA"], rows["Dec"], lonlat=True, nest=True) @@ -132,6 +194,7 @@ def galaxy_products(path, nside, n_bins, snr_range, size_ratio_range, chunk): ("sum_w_e1_psf", w * rows["e1_PSF"]), ("sum_w_e2_psf", w * rows["e2_PSF"]), ("sum_w_fwhm_psf", w * rows["fwhm_PSF"]), + ("sum_mag", rows["mag"]), ): _accumulate(sums[key], pix, value, npix) @@ -145,15 +208,81 @@ def galaxy_products(path, nside, n_bins, snr_range, size_ratio_range, chunk): j = weight_cell_index(size_ratio, size_edges) flat = i * n_bins + j cell_count += np.bincount(flat, minlength=n_bins**2).reshape(n_bins, -1) + response = 0.5 * (rows["R_g11"] + rows["R_g22"]) + ee = 0.5 * (rows["e1_uncal"] ** 2 + rows["e2_uncal"] ** 2) + cell_response += np.bincount( + flat, weights=response, minlength=n_bins**2 + ).reshape(n_bins, -1) + cell_ee += np.bincount(flat, weights=ee, minlength=n_bins**2).reshape( + n_bins, -1 + ) + for k, name in enumerate(("R_g11", "R_g22", "R_g12", "R_g21")): + hist, _ = np.histogram(rows[name], bins=response_edges) + response_hist[k] += hist + response_outside[k] += len(rows) - hist.sum() + with np.errstate(invalid="ignore", divide="ignore"): + resolution = 1 / (1 + np.sqrt(size_ratio)) + for column, data in enumerate( + (snr, resolution, w, rows["e1_PSF"], e1, rows["e2_PSF"], e2) + ): + leakage_data[start : start + len(rows), column] = data + props = ( + rows["e1_PSF"], + rows["e2_PSF"], + rows["fwhm_PSF"], + rows["mag"], + snr, + size_ratio, + np.sqrt(np.maximum(rows["NGMIX_T_NOSHEAR"], 0)), + rows["RA"], + rows["Dec"], + w, + ) + for p, (x, edges) in enumerate(zip(props, BIAS_EDGES.values())): + idx = np.searchsorted(edges, x, side="right") - 1 + ok = (idx >= 0) & (idx < 20) & np.isfinite(x) + b, xx, ww = idx[ok], x[ok].astype(float), w[ok] + for component in range(2): + y = rows[f"e{component + 1}"][ok].astype(float) + for k, v in enumerate( + ( + ww, + ww * xx, + ww * y, + ww * xx * xx, + ww * xx * y, + ww * y * y, + ww * ww, + np.ones_like(ww), + ww * ww * y, + ww * ww * y * y, + ) + ): + bias_sums[p, component, k] += np.bincount( + b, weights=v, minlength=20 + ) np.minimum.at(cell_w_min.ravel(), flat, w) np.maximum.at(cell_w_max.ravel(), flat, w) weights.append(w) + resolution_edges, leakage_snr_edges, leakage_sums = leakage_cells(leakage_data) + del leakage_data values, multiplicity = np.unique(np.concatenate(weights), return_counts=True) cells = { "snr_edges": snr_edges, "size_ratio_edges": size_edges, "count": cell_count, + "sum_response": cell_response, + "sum_uncal_ee": cell_ee, + "response_edges": response_edges, + "response_hist": response_hist, + "response_outside": response_outside, + "resolution_edges": resolution_edges, + "leakage_snr_edges": leakage_snr_edges, + "leakage_sums": leakage_sums, + "bias_names": np.array(list(BIAS_EDGES)), + "bias_edges": np.array(list(BIAS_EDGES.values())), + "bias_sums": bias_sums, "w_min": np.where(cell_count > 0, cell_w_min, np.nan), "w_max": np.where(cell_count > 0, cell_w_max, np.nan), "w_values": values, @@ -181,7 +310,27 @@ def star_sums(path, nside): ("sum_T_psf", t_psf), ): _accumulate(sums[key], pix, value, npix) - return sums, int(keep.sum()) + residuals = [ + stars["HSM_G1_STAR"] - stars["HSM_G1_PSF"], + stars["HSM_G2_STAR"] - stars["HSM_G2_PSF"], + stars["HSM_M4_1_STAR"] - stars["HSM_M4_1_PSF"], + stars["HSM_M4_2_STAR"] - stars["HSM_M4_2_PSF"], + np.sqrt(np.where((t_star > 0) & (t_psf > 0), t_star / t_psf, np.nan)) - 1, + ] + edges = ( + [np.linspace(-0.5, 0.5, 401)] * 2 + + [np.linspace(-0.5, 0.5, 401)] * 2 + + [np.linspace(-1, 1, 401)] + ) + histograms = {} + for i, (values, bins) in enumerate(zip(residuals, edges)): + values = values[np.isfinite(values)] + histograms[f"edges_{i}"] = bins + histograms[f"hist_{i}"] = np.histogram(values, bins=bins)[0] + histograms[f"mean_{i}"] = values.mean() + histograms[f"std_{i}"] = values.std() + histograms[f"outside_{i}"] = len(values) - histograms[f"hist_{i}"].sum() + return sums, int(keep.sum()), histograms def main(argv=None): @@ -206,19 +355,30 @@ def main(argv=None): args.size_ratio_range, args.chunk, ) - stars, n_stars = star_sums(args.stars, args.nside) + stars, n_stars, star_hist = star_sums(args.stars, args.nside) occupied = np.flatnonzero((gal["n_gal"] > 0) | (stars["n_star"] > 0)) np.savez_compressed( out_dir / f"field_maps_{args.version}.npz", + version=args.version, + selection="calibrated cut catalogue; stars with both HSM flags zero", nside=args.nside, nest=True, pixels=occupied, n_stars_used=n_stars, **{key: value[occupied] for key, value in {**gal, **stars}.items()}, ) + np.savez_compressed( + out_dir / f"star_residuals_{args.version}.npz", + version=args.version, + selection="PSF validation stars with both HSM flags zero", + units="dimensionless HSM ellipticity, fourth moments and radius ratio", + **star_hist, + ) np.savez_compressed( out_dir / f"weight_cells_{args.version}.npz", version=args.version, + selection="calibrated cut catalogue", + units="SNR dimensionless; sizes are ngmix T ratios", **cells, ) diff --git a/src/sp_validation/tests/test_dashboard_figures.py b/src/sp_validation/tests/test_dashboard_figures.py index 0f5426f24..5c4ee6256 100644 --- a/src/sp_validation/tests/test_dashboard_figures.py +++ b/src/sp_validation/tests/test_dashboard_figures.py @@ -5,10 +5,95 @@ import pandas as pd import pytest -from sp_validation.dashboard.field_maps import weight_cell_index, weight_grid_edges +from sp_validation.dashboard.field_maps import ( + leakage_cells, + weight_cell_index, + weight_grid_edges, +) +from sp_validation.dashboard.object_bias import bias_fit, leakage_fit, slopes +from sp_validation.dashboard.psf import xi_sys_comparison from sp_validation.dashboard.sky import Zenithal, coarsen, footprint_parts, quantity +def test_xi_sys_comparison_rejects_incompatible_grids(tmp_path): + rt = { + "theta": np.array([1.0, 2.0]), + "theta_tau": np.array([1.0, 3.0]), + "fit": {"theta": np.array([1.0, 2.0])}, + } + with pytest.raises(ValueError, match="angular grids differ"): + xi_sys_comparison(rt, tmp_path / "plot.png") + + +def test_leakage_quantile_bins(): + rng = np.random.default_rng(17) + data = rng.normal(size=(6400, 7)) + data[:, 0] = np.exp(data[:, 0]) + data[:, 2] = 1 + data[:, 4] = 0.2 * data[:, 3] + 0.4 + data[:, 6] = -0.1 * data[:, 5] - 0.2 + _, _, sums = leakage_cells(data) + np.testing.assert_array_equal(sums[0, 6], np.full((8, 8), 100)) + np.testing.assert_allclose(slopes(sums[0]), 0.2, atol=1e-12) + np.testing.assert_allclose(slopes(sums[1]), -0.1, atol=1e-12) + + +def test_weighted_leakage_regression_has_intercept(): + x = np.array([-2.0, -1.0, 1.0, 2.0]) + y = 0.3 * x + 0.7 + w = np.array([1.0, 2.0, 3.0, 4.0]) + sums = np.array( + [w.sum(), (w * x).sum(), (w * y).sum(), (w * x * x).sum(), (w * x * y).sum()] + ) + np.testing.assert_allclose(slopes(sums), 0.3) + + +def test_leakage_fit_matches_statsmodels(): + import statsmodels.api as sm + + rng = np.random.default_rng(42) + x = rng.normal(size=200) + y = 0.03 * x + 0.4 + rng.normal(size=200) + w = rng.uniform(0.1, 3, size=200) + s = np.array( + [ + w.sum(), + (w * x).sum(), + (w * y).sum(), + (w * x * x).sum(), + (w * x * y).sum(), + (w * y * y).sum(), + len(x), + ] + ) + value, error = leakage_fit(np.tile(s, (2, 1))) + fit = sm.WLS(y, sm.add_constant(x), weights=w).fit() + np.testing.assert_allclose(value, fit.params[1]) + np.testing.assert_allclose(error, fit.bse[1]) + + +def test_binned_bias_fit(): + x = np.array([-1.0, 0.0, 1.0]) + y = 0.4 * x + 0.2 + s = np.array( + [ + np.full(3, 10.0), + 10 * x, + 10 * y, + 10 * x * x, + 10 * x * y, + 10 * (y * y + 0.1), + np.full(3, 10.0), + np.full(3, 10.0), + 10 * y, + 10 * (y * y + 0.1), + ] + ) + slope, error = bias_fit(s) + np.testing.assert_allclose(slope, 0.4 * np.sqrt(2 / 3)) + assert error > 0 + + def test_size_residual_uses_model_normalised_sum(): maps = { "nside": 128, From d25e0758df5ede3557176cc9aa058e79ac89b098 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:14:41 -0400 Subject: [PATCH 38/53] feat(dashboard): register catalogue paper figures --- src/sp_validation/dashboard/render.py | 138 +++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 12 deletions(-) diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index ded20a983..00240dfe1 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -22,9 +22,9 @@ from . import covariance as CV from . import data_vectors as DV +from . import object_bias, ptes, selection_hist, sky, star_residuals from . import products as P from . import psf as PSF -from . import ptes, sky from . import scale_cuts as SC from . import versions as V from . import weights as W @@ -226,6 +226,23 @@ def render(run_dir, versions, out, maps_dir=None): } # PSF leakage + star_path = Path(maps_dir or ref.cv) / f"star_residuals_{versions[0]}.npz" + stars = dict(np.load(star_path, allow_pickle=False)) if star_path.exists() else {} + star_keys = { + f"{kind}_{i}" for i in range(5) for kind in ("edges", "hist", "mean", "std") + } + if star_keys <= stars.keys(): + star_residuals.plot(stars, figs.path("star_residuals")) + figs.add( + "star_residuals", + "leakage", + "PSF validation stars (valid HSM): " + "star-minus-model ellipticity and fourth moments, and half-light " + "radius residual from √(T★/T_PSF) − 1. Means and σ use all finite detections.", + "full", + ) + else: + print("Skipping star_residuals: product missing") rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit) PSF.tau_fit(rt, figs.path("psf_tau")) figs.add( @@ -274,25 +291,122 @@ def render(run_dir, versions, out, maps_dir=None): "half", ) + cell_path = ref.weight_cells + if cell_path is not None: + bias_cells = dict(np.load(cell_path, allow_pickle=False)) + if { + "leakage_sums", + "leakage_snr_edges", + "resolution_edges", + } <= bias_cells.keys(): + object_bias.leakage(bias_cells, figs.path("object_leakage_bins")) + figs.add( + "object_leakage_bins", + "leakage", + "Weighted regressions of calibrated e₁, e₂ against the corresponding " + "PSF model ellipticity at each galaxy, without an object-wise " + "leakage correction. Eight equal-count resolution bins each have " + "eight equal-count SNR bins. Hatching marks |α| < 2σ; σ is the " + "weighted least-squares fit uncertainty.", + "full", + ) + else: + print("Skipping object_leakage_bins: regression sums missing") + if {"bias_sums", "bias_names"} <= bias_cells.keys() and bias_cells[ + "bias_sums" + ].shape[2] == 10: + object_bias.bias(bias_cells, figs.path("additive_bias")) + figs.add( + "additive_bias", + "leakage", + "Linear fits to weighted binned " + "calibrated ellipticity versus each standardised property. " + "Error bars and significances use inverse-variance fits to binned " + "means with independent-galaxy shape-noise errors; they omit " + "spatially correlated noise, notably for RA and Dec. " + "Galaxy size is √T and the size ratio is T/T_PSF; only objects " + "inside the fixed property ranges are used.", + "full", + ) + else: + print("Skipping additive_bias: regression sums missing") + else: + print("Skipping object_leakage_bins and additive_bias: product missing") + # Weights, footprint and field maps, covariances - if ref.weight_cells is not None: - cells = dict(np.load(ref.weight_cells, allow_pickle=False)) - W.weights(cells, figs.path("weights")) - W.weights(cells, figs.path("weights_narrow"), layout="narrow") - s = W.summary(cells) + selection_path = Path(maps_dir or ref.cv) / f"selection_hist_{versions[0]}.npz" + if selection_path.exists(): + selection = dict(np.load(selection_path, allow_pickle=False)) + selection_hist.plot(selection, figs.path("selection_hist")) figs.add( - "weights", + "selection_hist", "survey", - f"Shape weight on its {int(np.sqrt(s['n_cells']))}×{int(np.sqrt(s['n_cells']))} " - f"signal-to-noise × size grid: every galaxy in a cell carries that cell's " - f"weight, so the catalogue has {s['n_values']} distinct weights " - f"({s['w_min']:.2f} to {s['w_max']:.1f}).", + "Cumulative v2 catalogue selections; histograms show magnitudes 15–30. " + "The paper's chip-defect/IMA_FLAGS, stellar-halo and pointing-count " + "stages have no corresponding cut in this run. The configured tile " + "overlap and r-band coverage cuts are included.", "full", - narrow=True, ) + else: + print("Skipping selection_hist: product missing") + if ref.weight_cells is not None: + cells = dict(np.load(ref.weight_cells, allow_pickle=False)) + if "sum_response" not in cells or "sum_uncal_ee" not in cells: + print("Skipping weights: weight cells lack response and noise sums") + else: + W.weights(cells, figs.path("weights")) + W.weights(cells, figs.path("weights_narrow"), layout="narrow") + figs.add( + "weights", + "survey", + "Counts, weights, mean diagonal galaxy response and per-component " + "uncalibrated ellipticity RMS on the weight grid.", + "full", + narrow=True, + ) + if { + "response_hist", + "response_edges", + "response_outside", + "n_rows", + } <= cells.keys(): + W.response_histogram(cells, figs.path("response_histogram")) + figs.add( + "response_histogram", + "survey", + "Galaxy metacal response components over −5 to 5; at most " + f"{100 * np.max(cells['response_outside']) / int(cells['n_rows']):.2f}% " + "of entries per component lie outside this range or are non-finite. " + "v2 has no metacal star shapes.", + "half", + ) + else: + print("Skipping response_histogram: response histogram missing") if ref.field_maps is not None: maps = dict(np.load(ref.field_maps, allow_pickle=False)) footprint = dict(np.load(ref.cv / f"footprint_{versions[0]}.npz")) + if "sum_dT_over_T_psf" in maps: + sky.paper_map_pair(maps, figs.path("sky_stars"), "stars") + figs.add( + "sky_stars", + "survey", + "PSF validation stars (valid HSM): star " + "detections per sky area, counted once per exposure, and " + "star-minus-model T divided by model T.", + "full", + ) + else: + print("Skipping sky_stars: model-normalised size residual missing") + if "sum_mag" in maps: + sky.paper_map_pair(maps, figs.path("sky_magnitude"), "magnitude") + figs.add( + "sky_magnitude", + "survey", + "PSF FWHM and unweighted mean galaxy r magnitude per pixel.", + "full", + ) + else: + print("Skipping sky_magnitude: magnitude sums missing") sky.footprint_map(maps, footprint, figs.path("sky_footprint")) figs.add( "sky_footprint", From a3bb0244450f3d4de3e020a3f173b3d9de2ad2a2 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:25:22 -0400 Subject: [PATCH 39/53] Render the tangential-shear tests and alpha(theta) by magnitude tangential.py draws galaxy shear around PSF stars, tile centres and Gaia stars, the PSF field around galaxies, and alpha = tau0/rho0 in galaxy magnitude tertiles, each panel quoting chi2/nu against zero from the Hartlap-corrected jackknife covariance. render.py registers them (new 'Tangential shear tests' section; alpha in PSF leakage) when --correlations-dir holds the products. Gaia staging caches chunks and splits queries that time out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- scripts/dashboard/build_correlations.sbatch | 42 ++++ scripts/dashboard/stage_gaia.py | 39 ++-- src/sp_validation/dashboard/products.py | 13 ++ src/sp_validation/dashboard/render.py | 73 ++++++- src/sp_validation/dashboard/tangential.py | 218 ++++++++++++++++++++ 5 files changed, 370 insertions(+), 15 deletions(-) create mode 100644 scripts/dashboard/build_correlations.sbatch create mode 100644 src/sp_validation/dashboard/tangential.py diff --git a/scripts/dashboard/build_correlations.sbatch b/scripts/dashboard/build_correlations.sbatch new file mode 100644 index 000000000..3e91f4e66 --- /dev/null +++ b/scripts/dashboard/build_correlations.sbatch @@ -0,0 +1,42 @@ +#!/bin/bash +#SBATCH --job-name=sp-correlations +#SBATCH --account=def-sfabbro +#SBATCH --time=03:00:00 +#SBATCH --cpus-per-task=8 +#SBATCH --mem=60G +#SBATCH --output=%x-%j.log +# +# Catalogue-paper position-shear and alpha(theta) products +# (sp_validation.dashboard.correlations), one measurement per step so each +# process holds only what it needs. +# +# Measured on the #425 snapshot (10,094 tiles, 73.0M galaxies, 5.76M valid-HSM +# star detections), 8 cores; see MEASURED below. Pass the variables with +# --export, e.g. +# sbatch --export=ALL,CATALOGUE_DIR=.../run-r425/catalogue,VERSION=SP_v2.0,\ +# OUT_DIR=.../catpaper/products,GAIA=.../gaia_dr3_G20_ruwe1p4_unions20k.fits \ +# scripts/dashboard/build_correlations.sbatch +# Stage Gaia beforehand on a login node (compute nodes may lack internet): +# uv run scripts/dashboard/stage_gaia.py --tiles --out +# +# MEASURED (filled in below from the #425 run) + +set -euo pipefail +: "${CATALOGUE_DIR:?}" "${VERSION:?}" "${OUT_DIR:?}" +SIF=${SIF:-/project/6001537/cdaley/containers/sp_validation-ddd58fe0.sif} +SRC=${SRC:-$SLURM_SUBMIT_DIR/src} # submit from the repository root +module load apptainer + +run() { + apptainer exec --bind /scratch,/project,/home --env PYTHONPATH="$SRC" "$SIF" \ + python -m sp_validation.dashboard.correlations \ + --catalogue-dir "$CATALOGUE_DIR" --version "$VERSION" --out-dir "$OUT_DIR" \ + --threads "$SLURM_CPUS_PER_TASK" "$@" +} + +run --only stars tiles +run --only lambda +run --only alpha +if [ -n "${GAIA:-}" ]; then + run --only gaia --gaia "$GAIA" +fi diff --git a/scripts/dashboard/stage_gaia.py b/scripts/dashboard/stage_gaia.py index 821b3b914..cad9fa510 100644 --- a/scripts/dashboard/stage_gaia.py +++ b/scripts/dashboard/stage_gaia.py @@ -10,7 +10,7 @@ ``XXX.YYY`` names or a JSON manifest whose ``tiles`` mapping is keyed by name. Gaia ``source_id`` encodes the level-12 nested pixel in its high bits, so each level-7 pixel is a contiguous ``source_id`` range and the query is a union of -indexed range scans, run synchronously in chunks. Writes ``.fits`` and ``.json`` (ADQL template, +indexed range scans, run synchronously in chunks. Each chunk is cached in ``.parts/`` so a rerun resumes. Writes ``.fits`` and ``.json`` (ADQL template, pixel list digest, row count). Usage:: @@ -28,7 +28,7 @@ import healpy as hp import numpy as np import pyvo -from astropy.table import vstack +from astropy.table import Table, vstack TAP = "https://dc.zah.uni-heidelberg.de/tap" TABLE = "gaia.dr3lite" @@ -82,6 +82,21 @@ def where(runs): ) +def fetch(service, pixels, attempts=4): + """Rows for ``pixels``; a chunk that keeps failing is fetched in halves.""" + query = ADQL.format(table=TABLE, ranges=where(ranges(pixels))) + for attempt in range(attempts): + try: + return service.run_sync(query, maxrec=16_000_000).to_table() + except (pyvo.dal.DALServiceError, pyvo.dal.DALQueryError) as error: + print(f"retry {attempt + 1} ({len(pixels)} pixels): {error}", flush=True) + time.sleep(30) + if len(pixels) == 1: + raise RuntimeError(f"pixel {pixels[0]} failed after {attempts} attempts") + half = len(pixels) // 2 + return vstack([fetch(service, pixels[:half]), fetch(service, pixels[half:])]) + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) parser.add_argument("--tiles", nargs="+", required=True) @@ -93,24 +108,22 @@ def main(argv=None): pixels = tile_pixels(names) print(f"{len(names)} tiles -> {len(pixels)} level-{LEVEL} pixels", flush=True) service = pyvo.dal.TAPService(TAP) + out = Path(args.out) + parts_dir = out.parent / f"{out.name}.parts" + parts_dir.mkdir(parents=True, exist_ok=True) parts = [] for start in range(0, len(pixels), args.pixels_per_query): chunk = pixels[start : start + args.pixels_per_query] - query = ADQL.format(table=TABLE, ranges=where(ranges(chunk))) - for attempt in range(5): - try: - table = service.run_sync(query, maxrec=16_000_000).to_table() - break - except pyvo.dal.DALServiceError as error: - print(f"retry {attempt + 1}: {error}", flush=True) - time.sleep(30) - else: - raise RuntimeError(f"pixels {start}+ failed after retries") + part = parts_dir / f"{chunk[0]:06d}-{chunk[-1]:06d}.fits" + if not part.exists(): + rows = fetch(service, chunk) + rows.meta.clear() + rows.write(part) + table = Table.read(part) print(f"pixels {start}-{start + len(chunk)}: {len(table)} rows", flush=True) parts.append(table) table = vstack(parts) table.meta.clear() - out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) table.write(out.with_suffix(".fits"), overwrite=True) record = { diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index dfea896b1..982d7decc 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -269,3 +269,16 @@ def load_rho_tau(sacc_path, tau_cov_path, psf_fit_path): def load_json(path): with open(path) as f: return json.load(f) + + +def load_correlation(path): + """A :mod:`sp_validation.dashboard.correlations` product as + ``{label: {quantity: array}}`` plus ``"provenance"``.""" + with np.load(path, allow_pickle=False) as data: + out = {"provenance": json.loads(str(data["provenance"]))} + for label in data["labels"]: + prefix = f"{label}__" + out[str(label)] = { + k[len(prefix) :]: data[k] for k in data.files if k.startswith(prefix) + } + return out diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index b83d6218c..5f046fb22 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -26,6 +26,7 @@ from . import psf as PSF from . import ptes, sky from . import scale_cuts as SC +from . import tangential as TG from . import versions as V from . import weights as W @@ -33,6 +34,7 @@ ("data_vectors", "Data vectors"), ("bmodes", "B modes"), ("leakage", "PSF leakage"), + ("tangential", "Tangential shear tests"), ("survey", "Weights, footprint and covariances"), ] GROUPS = { @@ -100,7 +102,68 @@ def load_version(run_dir, version, maps_dir): ) -def render(run_dir, versions, out, maps_dir=None): +CORRELATIONS = [ + # (product stem, renderer, figure id, section, caption, size) + ( + "alpha_mag", + TG.alpha_mag, + "psf_alpha_mag", + "leakage", + "Scale-dependent leakage α(θ) = τ₀/ρ₀ for all galaxies and in galaxy-magnitude " + "tertiles (calibrated e₁, e₂ rescaled to each bin's shear response, w_des; " + "jackknife errors). The paper's DES-Y3 comparison is omitted.", + "full", + ), + ( + "gamma_stars", + TG.gamma_stars, + "tan_psf_stars", + "tangential", + "Galaxy γt and γ× around PSF validation stars (valid HSM, one entry per star), " + "all and by star magnitude; calibrated e₁, e₂ with w_des, no random " + "subtraction; jackknife errors, χ²/ν against zero.", + "full", + ), + ( + "lambda_stars", + TG.lambda_stars, + "tan_lambda", + "tangential", + "PSF validation stars around galaxy positions: tangential and cross PSF-model " + "ellipticity (λ₁) and star-minus-model residual (λ₃), minus the same around " + "footprint randoms. Not a null test: it traces the PSF field.", + "full", + ), + ( + "gamma_tiles", + TG.gamma_tiles, + "tan_tiles", + "tangential", + "θγt and θγ× of galaxies around tile centres; jackknife errors.", + "full", + ), + ( + "gamma_gaia", + TG.gamma_gaia, + "tan_gaia", + "tangential", + "Galaxy γt and γ× around Gaia DR3 stars (RUWE < 1.4) in three G bins, minus " + "the same around footprint randoms; jackknife errors.", + "full", + ), +] + + +def render_correlations(figs, corr_dir, version): + """Figures from the correlation products present in ``corr_dir``.""" + for stem, renderer, fid, section, caption, size in CORRELATIONS: + path = Path(corr_dir) / f"{stem}_{version}.npz" + if path.exists(): + renderer(P.load_correlation(path), figs.path(fid)) + figs.add(fid, section, caption, size) + + +def render(run_dir, versions, out, maps_dir=None, corr_dir=None): cuts = run_config(run_dir) theta_cut, ell_cut, full_cut = cuts["theta"], cuts["ell"], cuts["theta_full"] loaded = [load_version(run_dir, v, maps_dir) for v in versions] @@ -261,6 +324,9 @@ def render(run_dir, versions, out, maps_dir=None): "full", ) + if corr_dir is not None: + render_correlations(figs, corr_dir, versions[0]) + # Weights, footprint and field maps, covariances if ref.weight_cells is not None: cells = dict(np.load(ref.weight_cells, allow_pickle=False)) @@ -389,9 +455,12 @@ def main(argv=None): help="catalogue version; repeat for a comparison run", ) parser.add_argument("--maps-dir", help="directory of field_maps/weight_cells NPZs") + parser.add_argument("--correlations-dir", help="directory of correlations NPZs") parser.add_argument("--out", required=True) args = parser.parse_args(argv) - manifest = render(args.run_dir, args.version, args.out, args.maps_dir) + manifest = render( + args.run_dir, args.version, args.out, args.maps_dir, args.correlations_dir + ) print(f"{len(manifest['figures'])} figures -> {args.out}") diff --git a/src/sp_validation/dashboard/tangential.py b/src/sp_validation/dashboard/tangential.py new file mode 100644 index 000000000..b0f6ea216 --- /dev/null +++ b/src/sp_validation/dashboard/tangential.py @@ -0,0 +1,218 @@ +"""Tangential-shear null tests and scale-dependent leakage in magnitude bins. + +Renderers for the products of :mod:`sp_validation.dashboard.correlations` +(loaded by :func:`sp_validation.dashboard.products.load_correlation`): galaxy +shear around PSF stars, tile centres and Gaia stars; the PSF field around +galaxies; and α(θ) = τ₀/ρ₀ by galaxy magnitude. Each panel quotes χ²/ν +against zero from the jackknife covariance (Hartlap-corrected). +""" + +import matplotlib.pyplot as plt +import numpy as np + +from . import style as S + +ALL = S.INK +BINS = ["#2a78d6", "#eb6834", "#1baf7a"] # validated categorical slots (psf.py) + + +def chi2_null(values, cov, npatch): + """χ² of ``values`` against zero, with the Hartlap factor for ``npatch``.""" + p = len(values) + if npatch <= p + 2: + return np.nan, p + hartlap = (npatch - p - 2) / (npatch - 1) + return float(values @ np.linalg.solve(cov, values) * hartlap), p + + +def _chi2_label(chi2, nu): + value = "n/a" if not np.isfinite(chi2) else f"{chi2 / nu:.2f}" + return rf"$\chi^2/\nu = {value}$" + + +def _blocks(result): + n = len(result["theta"]) + cov = result["covariance"] + return { + "t": (result["xi_t"], result["error_t"], cov[:n, :n]), + "x": (result["xi_x"], result["error_x"], cov[n:, n:]), + } + + +def _offsets(n, spread=0.035): + return 10 ** (spread * (np.arange(n) - (n - 1) / 2)) + + +def _theta_panel(ax): + S.zero_line(ax) + S.theta_axis(ax) + S.theta_ticks(ax) + ax.set_xlabel(r"$\theta$ [arcmin]") + + +def _gamma_panels( + series, npatch, out, scale=1e4, theta_weight=False, size="full", header=None +): + """γ_t and γ_× panels for ``series`` = [(label, colour, result)].""" + with S.figure_style(): + fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.7), sharey=True) + offsets = _offsets(len(series)) + ncol = 2 if len(series) > 2 else 1 + for ax, comp, sym in zip(axes, "tx", ("t", r"\times")): + _theta_panel(ax) + for (label, colour, result), off in zip(series, offsets): + theta = result["theta"] + value, error, cov = _blocks(result)[comp] + chi2, nu = chi2_null(value, cov, npatch) + factor = theta if theta_weight else 1.0 + ax.errorbar( + theta * off, + value * factor * scale, + yerr=error * factor * scale, + color=colour, + marker="o", + label=f"{label} {_chi2_label(chi2, nu)}", + **S.DATA_VECTOR_MARK, + ) + exponent = int(round(np.log10(scale))) + if theta_weight: + ylabel = ( + rf"$\theta\,\gamma_{{{sym}}}\ [10^{{-{exponent}}}\,\rm arcmin]$" + ) + else: + ylabel = rf"$\gamma_{{{sym}}}\ [10^{{-{exponent}}}]$" + ax.set_ylabel(ylabel) + S.label_box(ax, rf"$\gamma_{{{sym}}}$", loc="upper right") + ax.legend( + loc="lower center", + ncol=ncol, + fontsize=6.5, + handletextpad=0.3, + columnspacing=0.8, + borderaxespad=0.3, + frameon=False, + ) + # Room below the data for the legend (the y axis is shared). + lo, hi = axes[0].get_ylim() + rows = -(-len(series) // ncol) + axes[0].set_ylim(lo - 0.16 * rows * (hi - lo), hi) + axes[1].yaxis.set_tick_params(labelleft=True) + if header: + S.header(axes[0], header) + return S.save(fig, out, size) + + +def _bin_labels(edges, fmt, name): + return [ + rf"${lo:{fmt}} \leq {name} < {hi:{fmt}}$" + for lo, hi in zip(edges[:-1], edges[1:]) + ] + + +def gamma_stars(product, out): + """Galaxy γ_t, γ_× around PSF validation stars, all and by star magnitude.""" + prov = product["provenance"] + labels = _bin_labels(prov["star_mag_edges"], ".1f", "m") + series = [("all", ALL, product["all"])] + [ + (lab, c, product[f"mag{i}"]) for i, (lab, c) in enumerate(zip(labels, BINS)) + ] + return _gamma_panels( + series, + prov["npatch"], + out, + header=f"{prov['n_lenses']:,} PSF validation stars (valid HSM)", + ) + + +def gamma_tiles(product, out): + """θ γ_t and θ γ_× of galaxies around tile centres.""" + prov = product["provenance"] + series = [("tile centres", ALL, product["all"])] + return _gamma_panels( + series, + prov["npatch"], + out, + scale=1e3, + theta_weight=True, + header=f"{prov['n_lenses']:,} tiles", + ) + + +def gamma_gaia(product, out): + """Galaxy γ_t, γ_× around Gaia DR3 stars in three G bins, randoms subtracted.""" + prov = product["provenance"] + labels = _bin_labels(prov["g_edges"], "g", "G") + series = [ + (lab, c, product[f"G{i}"]) for i, (lab, c) in enumerate(zip(labels, BINS)) + ] + n = sum(prov["n_lenses"].values()) + return _gamma_panels( + series, prov["npatch"], out, header=f"{n:,} Gaia DR3 stars, randoms subtracted" + ) + + +def lambda_stars(product, out): + """PSF-model (λ₁) and residual (λ₃) ellipticity around galaxy positions.""" + prov = product["provenance"] + rows = [ + ("psf", r"\lambda_1", r"$e^{\rm PSF}$", 1e4), + ("residual", r"\lambda_3", r"$e^{\star} - e^{\rm PSF}$", 1e5), + ] + with S.figure_style(): + fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.8), sharex=True) + for (key, sym, what, scale), row in zip(rows, axes): + result = product[key] + exponent = int(round(np.log10(scale))) + for ax, comp, csym in zip(row, "tx", ("t", r"\times")): + _theta_panel(ax) + value, error, cov = _blocks(result)[comp] + chi2, nu = chi2_null(value, cov, prov["npatch"]) + ax.errorbar( + result["theta"], + value * scale, + yerr=error * scale, + color=ALL, + marker="o", + **S.DATA_VECTOR_MARK, + ) + ax.set_ylabel(rf"${sym},{{\,{csym}}}\ [10^{{-{exponent}}}]$") + S.label_box(ax, rf"{what}, ${csym}$", loc="upper right") + S.note(ax, _chi2_label(chi2, nu), loc="lower right") + if ax in axes[0]: + ax.set_xlabel("") + S.header(axes[0, 0], "PSF validation stars around galaxies, randoms subtracted") + return S.save(fig, out, "full") + + +def alpha_mag(product, out): + """α(θ) = τ₀/ρ₀ for all galaxies and in magnitude tertiles.""" + prov = product["provenance"] + labels = _bin_labels(prov["galaxy_mag_edges"], ".1f", "m") + series = [("all", ALL, product["all"])] + [ + (lab, c, product[f"mag{i}"]) for i, (lab, c) in enumerate(zip(labels, BINS)) + ] + with S.figure_style(): + fig, ax = plt.subplots(figsize=(7.2, 3.0)) + _theta_panel(ax) + for (label, colour, result), off in zip(series, _offsets(len(series))): + ax.errorbar( + result["theta"] * off, + result["alpha"], + yerr=result["alpha_error"], + color=colour, + marker="o", + label=label, + **S.DATA_VECTOR_MARK, + ) + ax.set_ylabel(r"$\alpha(\theta) = \tau_{0,+}/\rho_{0,+}$") + ax.legend( + loc="upper left", + fontsize=7.5, + ncol=4, + frameon=False, + handletextpad=0.3, + columnspacing=1.0, + ) + top = ax.get_ylim() + ax.set_ylim(top[0], top[1] + 0.25 * (top[1] - top[0])) + return S.save(fig, out, "full") From 1974518ca831a535f25b07848c41088105ae1d0e Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:28:29 -0400 Subject: [PATCH 40/53] Declare the star MAG column in the v2 schema Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- config/columns/shapepipe_v2.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/columns/shapepipe_v2.yaml b/config/columns/shapepipe_v2.yaml index 7d9c74ecb..7059bf682 100644 --- a/config/columns/shapepipe_v2.yaml +++ b/config/columns/shapepipe_v2.yaml @@ -110,6 +110,7 @@ star: DEC: Declination [deg]. HSM_G1_PSF: PSF model ellipticity g1 (HSM). HSM_G2_PSF: PSF model ellipticity g2 (HSM). + MAG: Star magnitude. shear: about: >- From 9dc97e34e94dafc4da5281ce3bc0b304cf7a547a Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:32:34 -0400 Subject: [PATCH 41/53] docs(dashboard): record diagnostic binning and map resolution --- src/sp_validation/dashboard/field_maps.py | 8 +++++++- src/sp_validation/dashboard/render.py | 9 +++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/sp_validation/dashboard/field_maps.py b/src/sp_validation/dashboard/field_maps.py index e71beda27..e801a0d09 100644 --- a/src/sp_validation/dashboard/field_maps.py +++ b/src/sp_validation/dashboard/field_maps.py @@ -15,7 +15,13 @@ The galaxy weight on the SNR x size-ratio grid it is built on (:func:`sp_validation.calibration.get_w_des`): per-cell counts and the weight range within each cell, the grid edges, and the distinct weight - values with their multiplicities. + values with their multiplicities. Also contains response histograms, + equal-count leakage-cell regression sums and binned additive-bias sums. + Quantile boundaries use a temporary float32 buffer of the seven leakage + inputs; only sufficient statistics are saved. + +``star_residuals_.npz`` + Histograms and full-sample moments of valid-HSM star residuals. Usage (inside the container):: diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index 00240dfe1..241e363cb 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -319,8 +319,8 @@ def render(run_dir, versions, out, maps_dir=None): figs.add( "additive_bias", "leakage", - "Linear fits to weighted binned " - "calibrated ellipticity versus each standardised property. " + "Linear fits to weighted means of calibrated ellipticity in " + "20 fixed bins of each standardised property. " "Error bars and significances use inverse-variance fits to binned " "means with independent-galaxy shape-noise errors; they omit " "spatially correlated noise, notably for RA and Dec. " @@ -392,7 +392,7 @@ def render(run_dir, versions, out, maps_dir=None): "survey", "PSF validation stars (valid HSM): star " "detections per sky area, counted once per exposure, and " - "star-minus-model T divided by model T.", + "star-minus-model T divided by model T; HEALPix nside 256.", "full", ) else: @@ -402,7 +402,8 @@ def render(run_dir, versions, out, maps_dir=None): figs.add( "sky_magnitude", "survey", - "PSF FWHM and unweighted mean galaxy r magnitude per pixel.", + "PSF FWHM and unweighted mean galaxy r magnitude per " + "HEALPix nside-256 pixel.", "full", ) else: From d5b5d46707a22b6bc95ff5fa320c1c08dacfe071 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:40:09 -0400 Subject: [PATCH 42/53] feat(dashboard): add catalogue-product batch builder --- scripts/dashboard/build_products.sbatch | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts/dashboard/build_products.sbatch diff --git a/scripts/dashboard/build_products.sbatch b/scripts/dashboard/build_products.sbatch new file mode 100644 index 000000000..58d4c91b1 --- /dev/null +++ b/scripts/dashboard/build_products.sbatch @@ -0,0 +1,38 @@ +#!/bin/bash +#SBATCH --job-name=dashboard-products +#SBATCH --cpus-per-task=4 +#SBATCH --mem=48G +#SBATCH --time=04:00:00 + +# Usage: sbatch build_products.sbatch CATALOGUE_DIR VERSION PRODUCT_DIR +# SPV_IMAGE must name the sp_validation Apptainer image. Run from the checkout. +# #425, 10,094 tiles: galaxy + star pass 3m42s, 7.45 GiB peak RSS +# (Slurm step 25103890.71, 4 CPUs). Linear 20,500-tile extrapolation: +# ~7m31s, ~15.1 GiB; quantile buffers scale with galaxy count. +# Comprehensive pass: 59m15s, 2.99 GiB peak RSS (step 25103890.23, +# 2 CPUs); linear 20,500-tile extrapolation ~2h00m20s, with streaming +# memory approximately constant. Wall time is sensitive to filesystem I/O. +set -euo pipefail +catdir=$(realpath "${1:?catalogue directory}") +version=${2:?version} +out=$(realpath -m "${3:?product directory}") +root=$(git rev-parse --show-toplevel) +: "${SPV_IMAGE:?set SPV_IMAGE to the container image}" +module load apptainer +mkdir -p "$out" +stage=$(mktemp -d "$out/.build.XXXXXXXX") +trap 'rm -rf "$stage"' EXIT +run() { + apptainer exec --bind /scratch,/project,/home \ + --env "PYTHONPATH=$root/src" "$SPV_IMAGE" python "$@" +} +galaxies="$catdir/shape_catalog_cut_ngmix.fits" +expected=$(run -c 'import fitsio,sys; print(fitsio.FITS(sys.argv[1])[1].get_nrows())' "$galaxies") +run -m sp_validation.dashboard.field_maps \ + --galaxies "$galaxies" --stars "$catdir/stars.hdf5" \ + --version "$version" --out-dir "$stage" +run -m sp_validation.dashboard.selection_hist \ + --catalogue "$catdir/shape_catalog_comprehensive_ngmix.hdf5" \ + --config "$catdir/config_mask.yaml" --expected "$expected" --version "$version" \ + --out "$stage/selection_hist_${version}.npz" +mv "$stage"/*.npz "$out/" From c8fc050bb808eb952bfd4889934167cbb56c0275 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:40:10 -0400 Subject: [PATCH 43/53] fix(dashboard): clarify selection histogram legend --- src/sp_validation/dashboard/selection_hist.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/sp_validation/dashboard/selection_hist.py b/src/sp_validation/dashboard/selection_hist.py index da33b19fb..9c584fd3c 100644 --- a/src/sp_validation/dashboard/selection_hist.py +++ b/src/sp_validation/dashboard/selection_hist.py @@ -80,14 +80,26 @@ def build(path, config, expected, chunk=500_000): def plot(data, out): """Plot cumulative histogram steps from the validated product.""" + import matplotlib as mpl import matplotlib.pyplot as plt from . import style as S with S.figure_style(): fig, ax = plt.subplots(figsize=(7.2, 4.0)) - for hist, label in zip(data["hist"], data["labels"]): - ax.stairs(hist, data["edges"], label=str(label)) + n = len(data["labels"]) + for i, (hist, label) in enumerate(zip(data["hist"], data["labels"])): + label = str(label) + if label.startswith('r"') and label.endswith('"'): + label = label[2:-1] + color = S.INK if i == n - 1 else mpl.colormaps["tab20"](i / max(n - 2, 1)) + ax.stairs( + hist, + data["edges"], + label=label, + color=color, + linewidth=1.5 if i == n - 1 else 0.8, + ) ax.set_yscale("log") ax.set_xlabel("r magnitude") ax.set_ylabel("Detections") From 58975131edee2465799d9f667a1ce669200729ea Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:47:10 -0400 Subject: [PATCH 44/53] Record measured cost in build_correlations.sbatch; drop Gaia chunk cache on success Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- scripts/dashboard/build_correlations.sbatch | 15 ++++++++++++--- scripts/dashboard/stage_gaia.py | 4 +++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/dashboard/build_correlations.sbatch b/scripts/dashboard/build_correlations.sbatch index 3e91f4e66..97cb4d877 100644 --- a/scripts/dashboard/build_correlations.sbatch +++ b/scripts/dashboard/build_correlations.sbatch @@ -1,9 +1,9 @@ #!/bin/bash #SBATCH --job-name=sp-correlations #SBATCH --account=def-sfabbro -#SBATCH --time=03:00:00 +#SBATCH --time=01:30:00 #SBATCH --cpus-per-task=8 -#SBATCH --mem=60G +#SBATCH --mem=96G #SBATCH --output=%x-%j.log # # Catalogue-paper position-shear and alpha(theta) products @@ -19,7 +19,16 @@ # Stage Gaia beforehand on a login node (compute nodes may lack internet): # uv run scripts/dashboard/stage_gaia.py --tiles --out # -# MEASURED (filled in below from the #425 run) +# Measured at 10k tiles (#425, 8 cores; wall time incl. a 45 s catalogue read +# per step; peak RSS of the Python process): +# stars + tiles 4 min 38 GB (4.77M unique stars as lenses; tiles 6 s) +# lambda 2.5 min 15 GB (7.3M galaxy lenses, 7.3M randoms) +# alpha 7 min 44 GB (rho0 + tau0 for all and 3 magnitude bins) +# gaia 6 min 39 GB (7.23M Gaia stars, 7.3M randoms) +# total ~20 min +# Extrapolated to 20.5k tiles (~2x galaxies, stars, Gaia stars and randoms; +# TreeCorr NG/GG time scales roughly with the lens and source counts): +# ~40-50 min and ~75-90 GB peak (alpha, stars), hence --mem=96G and 1.5 h. set -euo pipefail : "${CATALOGUE_DIR:?}" "${VERSION:?}" "${OUT_DIR:?}" diff --git a/scripts/dashboard/stage_gaia.py b/scripts/dashboard/stage_gaia.py index cad9fa510..6fcdc59ad 100644 --- a/scripts/dashboard/stage_gaia.py +++ b/scripts/dashboard/stage_gaia.py @@ -10,7 +10,7 @@ ``XXX.YYY`` names or a JSON manifest whose ``tiles`` mapping is keyed by name. Gaia ``source_id`` encodes the level-12 nested pixel in its high bits, so each level-7 pixel is a contiguous ``source_id`` range and the query is a union of -indexed range scans, run synchronously in chunks. Each chunk is cached in ``.parts/`` so a rerun resumes. Writes ``.fits`` and ``.json`` (ADQL template, +indexed range scans, run synchronously in chunks. Each chunk is cached in ``.parts/`` so a rerun resumes, and removed once the table is written. Writes ``.fits`` and ``.json`` (ADQL template, pixel list digest, row count). Usage:: @@ -22,6 +22,7 @@ import argparse import hashlib import json +import shutil import time from pathlib import Path @@ -140,6 +141,7 @@ def main(argv=None): "n_unique_source_id": int(len(np.unique(table["source_id"]))), } out.with_suffix(".json").write_text(json.dumps(record, indent=2) + "\n") + shutil.rmtree(parts_dir) print(json.dumps(record, indent=2)) From 1529e425dff9dc9fb651fc3affde926143b3865d Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:53:33 -0400 Subject: [PATCH 45/53] feat(dashboard): one submitter for products, correlations and render Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- scripts/dashboard/build_dashboard.sh | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 scripts/dashboard/build_dashboard.sh diff --git a/scripts/dashboard/build_dashboard.sh b/scripts/dashboard/build_dashboard.sh new file mode 100755 index 000000000..633ca813b --- /dev/null +++ b/scripts/dashboard/build_dashboard.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Build every cosmo_val dashboard figure for one catalogue snapshot through Slurm. +# +# Submits the catalogue-level products (build_products.sbatch) and the +# position-shear products (build_correlations.sbatch) in parallel, then a render +# job that runs once both succeed. With --after, all of it waits for that job +# (e.g. the catalogue extraction) to finish successfully. +# +# Usage, from the repository root on a login node: +# scripts/dashboard/build_dashboard.sh CATALOGUE_DIR RUN_DIR VERSION OUT_DIR \ +# [--gaia GAIA_FITS] [--after JOBID] +# Products go to OUT_DIR/products, figures and figures.json to OUT_DIR/figures. +# ACCOUNT (default def-sfabbro) and SPV_IMAGE (default the pinned SIF) are read +# from the environment. +set -euo pipefail +catdir=$(realpath -m "${1:?catalogue directory}") +rundir=$(realpath -m "${2:?cosmo_val run directory}") +version=${3:?version} +out=$(realpath -m "${4:?output directory}") +shift 4 +gaia="" +after="" +while [ $# -gt 0 ]; do + case $1 in + --gaia) gaia=$(realpath "$2"); shift 2 ;; + --after) after=$2; shift 2 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +root=$(git rev-parse --show-toplevel) +cd "$root" +account=${ACCOUNT:-def-sfabbro} +export SPV_IMAGE=${SPV_IMAGE:-/project/6001537/cdaley/containers/sp_validation-ddd58fe0.sif} +mkdir -p "$out/products" "$out/figures" "$out/logs" +dep=() +[ -n "$after" ] && dep=(--dependency="afterok:$after") + +products=$(sbatch --parsable --account="$account" "${dep[@]}" \ + --output="$out/logs/%x-%j.log" --export=ALL \ + scripts/dashboard/build_products.sbatch "$catdir" "$version" "$out/products") +correlations=$(sbatch --parsable --account="$account" "${dep[@]}" \ + --output="$out/logs/%x-%j.log" \ + --export=ALL,CATALOGUE_DIR="$catdir",VERSION="$version",OUT_DIR="$out/products",GAIA="$gaia",SIF="$SPV_IMAGE",SRC="$root/src" \ + scripts/dashboard/build_correlations.sbatch) +render=$(sbatch --parsable --account="$account" --job-name=dashboard-render \ + --dependency="afterok:$products:$correlations" \ + --time=00:30:00 --cpus-per-task=2 --mem=8G \ + --output="$out/logs/%x-%j.log" \ + --wrap="module load apptainer && apptainer exec --bind /scratch,/project,/home \ +--env PYTHONPATH=$root/src $SPV_IMAGE python -m sp_validation.dashboard.render \ +--run-dir $rundir --version $version --maps-dir $out/products \ +--correlations-dir $out/products --out $out/figures") +echo "products $products correlations $correlations render $render" From 3b267f4a387147e2f7c98855e5cb7e451d9098fd Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 06:54:02 -0400 Subject: [PATCH 46/53] docs: catalogue-paper figures for the PR on #425 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- .../catalogue-paper/additive_bias.png | Bin 0 -> 18878 bytes .../catalogue-paper/object_leakage_bins.png | Bin 0 -> 41612 bytes .../catalogue-paper/psf_alpha_mag.png | Bin 0 -> 18453 bytes .../catalogue-paper/psf_xi_sys_comparison.png | Bin 0 -> 12995 bytes .../catalogue-paper/response_histogram.png | Bin 0 -> 15062 bytes .../catalogue-paper/selection_hist.png | Bin 0 -> 35162 bytes .../catalogue-paper/sky_magnitude.png | Bin 0 -> 132796 bytes docs/validation/catalogue-paper/sky_stars.png | Bin 0 -> 147928 bytes .../catalogue-paper/star_residuals.png | Bin 0 -> 41918 bytes docs/validation/catalogue-paper/tan_gaia.png | Bin 0 -> 28994 bytes docs/validation/catalogue-paper/tan_lambda.png | Bin 0 -> 35522 bytes .../catalogue-paper/tan_psf_stars.png | Bin 0 -> 31114 bytes docs/validation/catalogue-paper/tan_tiles.png | Bin 0 -> 18770 bytes docs/validation/catalogue-paper/weights.png | Bin 0 -> 32446 bytes 14 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/validation/catalogue-paper/additive_bias.png create mode 100644 docs/validation/catalogue-paper/object_leakage_bins.png create mode 100644 docs/validation/catalogue-paper/psf_alpha_mag.png create mode 100644 docs/validation/catalogue-paper/psf_xi_sys_comparison.png create mode 100644 docs/validation/catalogue-paper/response_histogram.png create mode 100644 docs/validation/catalogue-paper/selection_hist.png create mode 100644 docs/validation/catalogue-paper/sky_magnitude.png create mode 100644 docs/validation/catalogue-paper/sky_stars.png create mode 100644 docs/validation/catalogue-paper/star_residuals.png create mode 100644 docs/validation/catalogue-paper/tan_gaia.png create mode 100644 docs/validation/catalogue-paper/tan_lambda.png create mode 100644 docs/validation/catalogue-paper/tan_psf_stars.png create mode 100644 docs/validation/catalogue-paper/tan_tiles.png create mode 100644 docs/validation/catalogue-paper/weights.png diff --git a/docs/validation/catalogue-paper/additive_bias.png b/docs/validation/catalogue-paper/additive_bias.png new file mode 100644 index 0000000000000000000000000000000000000000..d8433cd9f627918dfb8859d01dc01361188ca6d9 GIT binary patch literal 18878 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 literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/object_leakage_bins.png b/docs/validation/catalogue-paper/object_leakage_bins.png new file mode 100644 index 0000000000000000000000000000000000000000..3ad3d0fc028febe645e440192d7481a8af0eb40b GIT binary patch literal 41612 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 literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/response_histogram.png b/docs/validation/catalogue-paper/response_histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..cb8dbe426db53f9876fdc35f5846f287fafbab2a GIT binary patch literal 15062 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 literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/selection_hist.png b/docs/validation/catalogue-paper/selection_hist.png new file mode 100644 index 0000000000000000000000000000000000000000..f1884626407eb89b0499b2621bf0e4a05412abb1 GIT binary patch literal 35162 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�_8g5 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 literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/sky_magnitude.png b/docs/validation/catalogue-paper/sky_magnitude.png new file mode 100644 index 0000000000000000000000000000000000000000..237c5f14a7c9f444cdc249be201a4d62ab436cc8 GIT binary patch literal 132796 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

}9JmIY@UJ?=KS4MT z?Q}R3LA{otd7H~{b!G+Ti5$u1$m|Z2BRS+lH3EoeMW7>ak3%K{V=;>+t`c^v*yRqX zCK(PJ7zJl^N)0x9_3b!jYZ)A8FijSo>HtWKztKi{L|refB}qQloJ3Z>%B~F)Uy~<7 zFb7&(yxm)?ya|Izp0Q}M-d{gK4=hs|bkH5dK_rf&d>AHjJdG51Q5i07uM|L=`fr*7 z7PyfiZ3@*l^tPHJ!Xqh{NcX65H(caNcS_UJB7a&0oh8#b3(KASOPXw^ZXP|oq=qv!;?C8E-j2$Bm*-n_jYh@ zmH^0ggL)8pNMQ^d)B*wfksG0@*~bu#+o-dI`5qKTgDz_^2H`Pt9k>CQ=e=~pm(W~n8Io;J&d+pi=SZ8+W$o@|F-5Vs_`x;XEP-XD$!w@(4Jc#6^0n;x#1!Ojs1mn(3nR)M;M}8YCuXm zmJrpR6-Jp9L&owUh%Jv6omixr%5#>Iw54r31v`x6hl3HP`X;C(I)k1*jH`8~#+#9& zYcOFd5Hzgg<^Y<}_Zka+q`jL3Z=9r*p7r1nUwBe5*Aexg{EA?U3rE&XIb&zZ9n<6} z#J;jWRq`^(I1<4Sc!8&?uqchadP;Xd4p&-4w$>yJf?_QkP&Aa?!&KB5>OVMeIBPu; zBv%;OMs&fwnO^2n%&X<@t3$&?GiX~2i56pdGrv%T{0lBRDl;+!PaMu~(;|3fC)Y6T=(dO%eZfnys1s^oFp}TJ7EATytxpvU$6J>?Q(Z>Ga zG59=)?IEKck!Hq-ZrwIF6?%C!eKhtW^Bwu&l+SHkHSMV%>zygFq;crTJ|U%(YxNek zQE87Y%W1vzF3=_7xFt0fs*1UJvQT2oO&b5eUD4EQ1P+c$vCPHU9|T&Dj)RY^^B7CL zV6}i_J`%9RQY&(aZAM5NrVrI-f*FZp?sXNEUCmm@?2Gbd;Xj(F)2Kl4FO3eJ2P;W_ zOZKc)QTA5E@~mM_1^5xTOvbDwbWgY>{Fi$DMd4HqA(e7Tjcj0XlVJt%!fLpnNTI+THj`rrt z5;mAl9ruU1yLRMT^?ZX+z}~wo$Pnf_E})h#849yv|YvcQ5bQL8$q+jn=|R$dN*2%V=EnxbqYIazJWsHGP{ep zt`?ccr*tt#WTLn}`6kkztOJGUjrYIYu5kgTr?k>Drmeu7pd*S3xb!wEb|*ezyb;$g zFAxF#G3(TRMd-uuY(QGa5MW^cRIE|PIPw`1I!b*}F)^=>8m!9?srX+uqW;{0R=C4~ z!gPiap&0FZnoBfyXM9JZfw>Thf2Va6a^RG?i0u5sIUsTvL-!zN z&pU4+(E|(G%^J!&sFf-wqxSBi4Q}2HYEnF|KhCj(rZc;>nlZ;*mDfyfVh1{{>Y-Rj z&RP}*2qiG|tCR{^lpcS#*k!H~;?t~n+y@K5dZ=})F{cnSR0(VSs4Y?(5I)k~4?t6C zKY+Fs5p5J24?&JLpGEt!srduhI;kg#X z5jpnv?qN_%0(0ePJKGm|fEa)w85)t(l;4AERI@ByZM!B&cgFk|*VnG8qW9N^5x zX`@@W{nAC~$PWpc%`iMed)OikHYX@Z!@C!c(qbH}M>+|3MN1|+B9)^Dk|~ikW;vv4e_X%8d)MSzgj)QHDhF`D{_$Vq#?3Kp~>|Mr_SnUB@f=NFnUe3eQ? zH(PGO110-p`+QEHtK-LJ8QQ7`M%`G0zLvXD!>a#!N-Jk0Pbzpq!CM^~|J5{{v2};; zrSVsi^|~GUGZFf(>~zz$-vZ@fFv>%14#&)92!D8X9^25tK>`^x4}B-zBIv|$8ezb8 z@#10`60ny59k+fRFx_NKs*;Ca`g8hqK_muBXVxNm$bcn?MWk4y)*8!8n3CPq`)<`! zK7=>+l$!D~2}KSGokhKCw|#olfjlTeLYv9cDQm~b?$Ymb4)km+>R6Ylc{y;gu%+@D z+d9dH9K@V@W?i)}+nUe-3#gi}$x7O4(+lT>jiQFS;MgrATmc0(#qa{^ zD>on@5II+ivA7o%EP>=-aN2Vi;3|9e#{UuiX}VnK_@l`-fK+HVa>r3*AdmxtE@kl+ z&lVWYOoa%C<>JNY4ntl&P>3gWcz2wKA`I8uc6roe4)^gCUnY1b=B2GEn@+b>(6q?d z%0Z}$E1uyrFUd z3sE<;AyUU|kSc-%txCSenRb<&+qTtY1n%W#_~%-S&Th;V>^00BVK2f-=8{>GyODPG zH0p64eph}mpr{n%4cnfe*S)LMCuvh=O!0v8oS;TS@bO(o#RPh^fd=Rf9wnhMJpsl}Feq2kotzS$C9 z&t1}*3)gR3bZ#iL!l%--D(+3a7Z`5^G*L#VjO&AIq$gVRTEhM@r}LW4k@IbT&~;D) zY>%4OMk!kIe4`-v*?ZdYQ~it`RW$-S)mAG8-1`3co!7K4!zl9jO2jJ`%=FfxKAEnf z3*!~@LLrg2D>4=h1d+keOS$$&M{aS&qD%TSjsz=PjOFFp&7d=%4bTi_vF$nVH>``X z%toD#C?YvRMkFQA$VBjxcwW|~)W_s(eC^L*UE9ZO7y!Xs0A^?tFwyD#3v+3V8OQ;a zU+XgN+$=mTe0}RiO~m`XY8H5;7pR!DWbJ2x3mFz=mirOIJda&HDb(np(CSf+8%k2da4&L_m1`tpd)kM-2NbX}6N6=dBg(Y;V7YchSt-4|J08LCEVWyiL7Q|j+}H15Vvn7Iq#s%Ix)fsZ5)uiK9UGRvgc z>v)ZOhJf4i38%5=ny3&z4tACLx(3s9sm@EUqzl2&Y#F~AGwqo=dkz*VnJpQrR<&+5 z-*kT5dFABn7rzy;ZY5+9q+$^i(wgx?yH(Ao;!6W)^ELI_4y&MeOR_RqS)!)yQ;;`5 zspcA&tKlx4I#AZi4Y8+9!+P+4&89nf*IYz6u|n!FNwmK~3j^So@n|m4SbqRzTjvH% zff%jOwp|-lnROR5JGN+Os1Q2SG8ikw@-y|lZHmZ$te`G1zP>aGmX^&>p&I#rIY+b? z8HLRjUX2zz`5o$b6!jHO5dTZQo!@yea&$Kw4i4NeoAGb*)g1mZc$#-H?PML%NJ7v5 zLPpx0?z-Vr^(dKBKp)?O`r#wazwb^uP~^WO-^p%7`o|3!5C|dv z*zV;m3^?XmKnRm0_mYv>0rsx-M8fcrsC03*Z}`C=Mz{P+^1*$m2I4ZhxdX zRr_0iF~E4P<@nA@IVa&K5~2}IpXM7WuZ4yhV=8vXhbJ;~Vj&?In^Kml$u@76JESz; z{z;@poC}~W54I>NKs~mzjar|CO*nU~-Y+rmdhH@!8O7q7^T%kz**-Pi^qXqwBopbU z!-la!Oo>MW(PO=}CU2m#^hlXN99iakBW60@eD2vR>d0Tfg25Np(mMXi&`7ZbLu7b@ zDK|+~RhP*IZCh$RPLD%y0*YO-VnzNL$#*DpBJf4}pJa~nc5q68uGYv@pZuLr``0``B??I6DTGjoYNzUzf@ZIl2-B3@yVz=>T6jpo6 zoZQ_kSPl;9D3z zvq>E5o;rMk2c~%&{3-*yQWB@&JTR+}1G!nUFw8P>Ps+m(65ptpQ14UoP`0ISSTkos zZ`9>QP{p&Po=R@dDjFLjs7h&7gEHY7v|Bm!UwS0tcT?Bo=}9*?H*Q3uX~cL}(PD2x!~kak-WdPEGDtIws?up4JM zVnR-uv9F<#5d+SV;QYfGsWwf81yKj-(wBdGs!5Q*i>2n1iRAxtX2=qy;qJ~iUSy*) zA%`cYq=>x3E)1dctf-pEHN{GBh)egc&*`98jh~BRnOOUwInBnG`MTt*`zH|2>(0TN zF2(-WqC92=PWO^nCipU$dI60BcGptvDhNe8iQ4*#Woh8Yy~wm+v%IIhIN zq{+-qdj$=G_0$#QUaDD8LZj^1d)dE8WFP z)2pBGn!M{>0x3&IX$Y-;Bv3(Ed(bTG4rl`Aqa&_qC#f-*{|?t;kV62sc%E9UG$iH* z8DVURQ}jWbg3t7Ol;4A^X*MN^pudU*xNJQS7a#MZiVkl%K^}|Slu<62Mi@_Tzyb;- z^=Fhl*GaY#iF_^)ur^bTxbY*7};K>k2p5{MDEqp4=L2RPa5Y_(?oH6ruijc3q!DeOLTsdJs_r zoEh37Wr);cPII@NtDN5QWfWEJ$V2~^EiO|%eNWB5Mg>1&(eM7{_ntZP+6Hy^dAjrC zMMY`-o*|4nBZH`eD`ObFtGl7GVuvuPYOjQmi4)<56r3K1<1mlA4M-3cXJAg7J~bKb zuPp&>FcG7-){hiPHESF*TpOL*AYEdM2 z+J!1LatHD@2fymG?2bwaXVx!7M~xJQx66fUv&&mS>ALxz&3J7uwMd!is_j=Fa^fGe&c7@UlO6>y zD3n6%^PzOHw)O8*_$jp#6s+@^_^e(}EfNegxHK#F$;K@Y+I>>&Wde^}K+MxRv%rP} zi$c@49b5wuxDYt;QTm8K_(K#yD7W7=JdHe7n>ngai-vfxVA`k$HlwJgnrGq$^c~e0 zzBlXQROU&}R?WBH3n;cY7U2?T-wT{)pv9cIctDn<_(#XTg9iWA97{|$z>5Y^D=t~i zu>}!X;PvA-{fZk8Y$NEZGmH{8&8+@gDcil@&VvBUSM9r#3oU%Wp4`tTf=lmd z-U-A=a(Y|19GFOxXf zM|t`pd0rBLCluG8t)dx=G~L=Lw^NOR-kt6&C|!pT5EI=wo5q??}7N zgwH60E#us!p{`vLu}iV=Jo1$GmM)O+96QWkPXPp~a;ZAY+y<})a9RXJ<^^^e>om{>6xE!)8+rn&eyaf95d*lQ2un!tr0^$46EB!`VRlj^pdhSWlx zVh#9~5**e%)=_7fNq&L%ww~NodxaGG1K~RGRyd&LbFDkDoXY;d;5>og-J)AS`a){(4Pro9vrSUb?P)=YAgEK_q?lVrJ}!L;{p8?q@oW!Gh`by>?WX|wqE@_gfw zGu9^n!`VgwMV=i}^X=Q8En17OJc%E?e5790X9+~fwU$Bx=a1BD3HsD9CAhE(T zyD_=x%S=hAaoRpB7b{$V?9d9ony?V4{mzVNxQ{(>)8&dv(d_pJkK&o{|A6&7pX7VojyGnBiw)Fl6R1%0o?Dp%6u2(Qj#70?6tW zNjSw+Mwy6uIVF% zJp64+nfiQS*=8~32V^W02x4BMpS(tSUk3T5oZWOh6p5dw_Ko{NTy}d1sr>XzWjw6_*yHcY?q+nkX)pAw%7$dQ= zv*0o1A({TyLCl;<#K#ao6B|IpX3d#BxrJ9{h4*dPJq1c06E*3{;KRgY{8IQ_70v5 zJUstS6Ydbv{Q^ie529cQMZZR8H5A)*=;4mhs7drvTuGqRA3@65*N#Cx^t;P^ZI>*cJ_xYGL(Le~O!84vQ|iGGOumgk_7dq8OwA##OadE&$M{A{gNB3dEWQf*&T z?yoM%_MpF|`;g?#jvSR54JtiQOyhIR8n;R~gc$ak3kK3=II);ogjBPC4(Tv5OFr#; zA##6>QDDx8jX!Q&nI=hQQRNno4q0!~hrz(I{p&-OYr*mCV+(pn-|apMW+iFN=Ex=_ znQ-B&)b39Tw+K70{U--Ug(2q`?P3s)rGxxFiP=A82`|40j~}lg%1!iaZGq{a^40T6 ze28ouVZlf~sdppjtewHL;8(otCF=Zl66uSo6ndHK?EwIo_0!Me5!J)|QI9=2tgEDdI%M}25Sg;$&r3;mOD}FkL@7D2GvCnUDKA7 z0Y7#Xr0{XAQzh5{W^qujuncyfTHDPl=#4Qk2maoX zcsS}gQ6-B{(gJx(kq(t_5~B9leRXi&X=(c<2g{oFIgLKiT?S4np@W|I%&vltUn=}S zDnLKK3>akJt6#3qjIRCt!>b{js&m9=@_Uu?zw@+83XwzCKO^sj5)&lx1MUIi(%O>T zEUirm(Z&8Y);G^{e+CDNA6K>Ya+jY{HZm1UwS?2e`Bjl>f9Ei3;NSxZnrzi?X!ml@ z98~B}1n$1`P~~8X5#OwZkKns*rdangNwh_`p}bl zKNoEBwcN`e=72FnzdPAWnaIQO!qQ9kStd=&K$HZ6gyhSs;TPf2yK68rzG$;&(3gaY z*Y^7Z84d*7GtA%czyI}|kx zEotFwpjw?`H}n7tiRmDvwEjg8~0LDl^ie}VX8Gt^WOW3XunQt5VH8})(a+9B<^B4P$TW2NIFmQ0F{3i>quZl z`nl|wN5>+>;5kZh7EZ}L=m`DDEAlmd(++ckIi4S^r8q4_o((v|v#H`xD2V99m|ML?Gxf`X#2nwF?tT?kH_Rlds+I(lgk!=fV5I z5Fe?B3Kz&$J1x%VG3CKZ1i{e|voesZ3fBwevz>PAE>&NC5W!aDR+(x%%-q@<6TLsU zKN)}F`)N>sxMbJML|i=E8A)iMgC=jB-MsO_`R{oQnBTni-61 zt7V?Vg;r&OLcCS`0_5f(qC^-kv?0^3%bzoUhkq{5RQf1g%YMD+9%fQOeMcg3?Edw~ zd!#Ab40hIX^*qQGH`@koU9p@ejpTv?Khn3Emy2rQbdrnfM-=4WW#-xruY2Z5ffUhD zdb$wzwO(oZSn;nbC{MoW;6<0#)1bLJ=juUz;xIK@Rw~pxz%N!T+-2~%c`#1JKG$jv z!pye6-9zs7XoYxYLheKdo4Y<4$vf@to+Uxs$dAMr%M%v-M;hAr!lS=z$JS5Wx^oXZ zqmosh1clQuXK^=VIp=iPA@i$h!`~vZrxrM;oK#eroS_I-U2~iVQBm7}Swmn1dmK?S zYm(f-g_p%4J|p}n`Iw3&!P)X?oIuQFjgDq@8Z!SU3*FwrnbHPG?Q9}^+_sqC^Yrk( zR>*ii@tieuxa*BtF!|usywM$R);Lc?VI2mkdq-g)$cW8pUrlXCqPK^=tz|21jAGD` z;$jy3BX<1jwZ2^+Pao$A`xZRDY`^t(Or1V@2#GRvj=4CFP05Qj(xRl6x4Piv; zPEjn`Cr|cwkoK(B1lIlNQ#OoiAPN#6)w}8_CfX;*G}a%T8Fpj}?V>S*`-m(?_&V7- zexIG;K$+t9_cC-hj059=h7(0|%ErtG&2nSmbcv=b>n#_Jo{$~M*y54K@Mx{tg;df9 z1aJT3k0Y^0`IV`PAzx_}W@*{TI|R*wl%79K#zFQV8?t-7RHh%@=(6uDDmaUY8Y9t! zu(Nj_h~4}q>lbEr;1_+SembiUS~u=HWPaMQ37xC0d4iFVxNFf|dNX&(T3j*3_ei!E zr^P51EO`7Gf_l2__Gma{;Rtx1g;M;XEsuM}p}sxlrA2RO7QDIotQc9wQ?A5b=(CyX zT-p&wsFz6;9n2{uM3N{r3(41)X*xDxMH$HaM-GB`M1OVIq3+c0Cv=-XEhziYP|o2wVzJ2f|rLU93jfAS*?zdmF{Z)GNW#H6Z28Y51#-PrfK zozr^aPczfq%UjcVK$U10tl;vvBd!a^*hS}ivm>$sSVDQmWonIV=jc91kmUCTeKyn+ z@Sb+r{KnaKv0>ud0~PWMhM}!GQ*lJ*kt01_m)3pAZ+@n07}M*k9}%|FGpmwxV_~Ta zA{`xObIXasOhaseF}R_B=)!~z$$kP#$ETHC0?ag?3hutQD;Wty!*DJ1>1QI!=;z#{ zde8#%#ksc=xptJEFyXf7u7EV6MFd)WSggpS#!B(IZEmO(FJDg7u`a-6C1H z!6-SbyMi2o;qtm8Dx#j_NQ7hIf>l_5JE3{ZDy8)*D>A@!a;<);@S+BCl!n}hdkLb~rtVS@Z2|*iP@%HXMJN~J@ zcQz~>kI5)G%6%NBh*H7Qv)wH0N&BLQki^vt@_|&d1_}yyKmQ8cL^ee@x`#bEuk#t^ zXf)<4hE)cK@ibo#O4jYn&CS*9m4cH3eCEu-o*Y@(PxsslNwpzb)9FA1hVX4Z!8RWb^`SRUmfL8q^dCC-m1~lCB}?=7{{P!{ISKmV-d1PPoH3U$o7JJ?_g6B zsX-(w7{E643{bWASrjAx>v8>T9ivU+dAmH*-0y8PS{$yhl?TgXluG- zkzUeA?2MmtK)i86^upjUsdjv$X@?g)6zhGDG>T^0I{?+*KR@cc&&<8z67v^jjMQ}f zsP3opdt$X{G9nj);=z~`Y}Qyo=k-PaBW@Od=r$dr+6%X$GO#49cOEKlzka8YZM00) z%-@d?-pSMqV z%G4!)cnkt9s7s{vUj2q!)Hi&9GPyH8`?bshFL z+@1-3Q++Ds9!a6A)ZOQ^*S>6nB&NhfXeU;$-NdDPTt zxrp9vo_^Y>pd@st7Q+Z4!aM_YE#Vq!BUX748KdI@v2^$_-3R;Io$*yi)EfRDV}lMNYcve&Nna;WhGT9s%_{=_mm%;F4*<7RM@*e8FD|DL%Q~0m$+`XJv>;deB`M4#|}t?w)NAi zM5(8EQ_mR-`Klhl-`lau@5OU8Oc_4vIVy8Bn#xtHtW+zs=F8Ro%Oz(uRn-D%H=@n6 z;{5vC+~P>)Pe#BY-26+yBDincNDC<`iV?Nq^|?fQ?Ahc)R33D9{r+|!SE@DFLh!4j z64yGeCUH@Xu@kqe>;3tVG=v=#mR)gygMt=2MFh{lTK(`*0kxwxr7v?~2g{PdjzIQ$!N~8IwTGvBjaPy^3Myks_%)Z>&G1Q& z#>xHsN~64j0ytOgcBo!YbFX?I;SOMZ)7$$O+~&~fK4 z8WVD%;#|w<&MtoLth0|_-h&z&)(j|D60ESG5koMZ?9764eF3ETgB9PS342>l2NCt~rZzFl zC2MRIun-!`0~{63^&1q5w6}>2@ZKS!CrNmO8~7Sgpkgc=y4SU!ytS+n)YJ;2Io3l2 zTuSfi@n&00(TP`K4dGW!_-Sy{g8*C-nPl4JXe<9bw8-PtLwOj0A6{Cmm*WGwN)K?9 zlt}Nw4*f~Z;07)roV#|>xT#9T(dWwf_SB#iGEoQzIG7H0=vYoapU6QWA@v+qlT!8v zks<)ja%__IWe-EMn&f1fQV@aNtZ8J6uC9X5$5+x4k%0pq?OhI^{>=lzCu_hf0@B5! z(~s|r>ssHKpH~d}7aV)s^$>r?nnxac#T9<~!37p-yj?|FJiUJj-XhkJg#qZ309!S@ zTQ0`RPXli)FMS@A{6V>20p@UzyZXOD+kHk!HpHmv1~;LB2p;uQuSSnFZjY&R-^R;& zp>09h;yZLOLtJG_!8{+Qeb53nsVz0o{Z`hG|M7(mgqKFKV{AUo73%bY-uurk;Q@V< z^<-%dS4)n*Ge7V4LU&0C6|Xw3$NH|V|H|eo>)nfm!eo3q@0sDZ7vy>Au+nN&*FN=fxe_{G3w(5e}r${if1NANDHM; zqH|G5%zJpfH`w+|@(Pix9sbs)tP}I=k7`YBa1GCQki(^{VQ!OYR8_%^=?jEw^=L2+ zkF^lfe?0t(1UX(NV}E$ocHUZQdyFZ@$dY{D{PxgA)f@#1GsV#Pt0`K;L|^LBWX&fVv2$ z(6DL7@KJfT-&0d{9&MV~6x7q4GDRT!$VE7Iy5M-VrX>jUnd{&c@-|*yC%v#GCc3yW za_i_^0$+A4@iR!2KN;ogU-_oGT|DaEUKl)*-fmrR>EEgpdfP#_g3jd6ZFGtFsX97V zz&38Ur^NEEZgqRdBQHaUSh>U(?*n_JJKo)=)&Q>1E@J)qRFXA7N6&%tc}CIfVl1kg zWU>%H-uZ>m@cIA>ldItB_7KUT-*hOb#-rg@_G0$#*5HN=jJ&w^fq7}+sP8Y0e)^&S zod}v_@@Vp6J_h43FE3Yj@GfnS@OG?k%kkq~Vzjw>)aK~F3zJIJ46Fx&&U_yY93KdK z_iW4!9AAw?4JVCKTO8|TxsDv$2O{OF-~XT%EP8j|%E~jj_0-kOU(~cM3H<&)!}R~g z@zd{BVDNo-j59m^t&vqKcAa@HHSxVn_mUQ02LW}*YZ`8_4=cxPEY14n)qCixW%wfl zepS~?Ll0JWN=^6kWRWR<=bQ5W+MvE&^Y*ZGj7t8hF_61LVyNUQy(lG(kH`TR8BxMLGn z0XY1#iL(RG=LAuXW!%-)boz*9kEZy9Gjcn z%;kH}Si1n6)ICSAG)_~sS%ajGjGS5Ge#P!C^7ZvqZaeMU!iLVx$Xba{t&LQBR*=|tUS)rTmMjmrX zB-3stzQ9FF{y5o&oDlWbO6c{zm9 zzx5xtbN=!82nRNFDAo7*IJKUxRMb}cy)89EpuMQd74B!kwma4`pft(0%I zofn^wnoD$~05(e@TOUq4k8G6WmP+?nh{UC>SkDFNG_|DL5W&b5+Q>b)$iabA^SmBC z>5qgtYDjz5IHJ%B3Ci&g*f8KEBRs$Q`s?fA*R$p(8qFmwo!wMX)@aG!CEei#r^``X z1N(aic@`hC#ItgpWN2+eiTwY#1=^KyzfnIr`G-7~7)j#V8d69LPP@37>fr5H&2;q- znWR#?>sfm6T|6-AzM7;e{KeBtdD#AadpBs3L~XiUamzY_IV{Vp6pGqb=lddl?QU>< z3}2u%mRY2+|I53!Q_KGp!R^r^0Zds;1z*68m?h}RI4 zH_sk*EZ=)go~RP#3!o5diXV%DdDsRm21e#J=2gS#X3~F4+hVtbnuqSICRAT6Fs9Uo zIzDl)A*5Z=9(9(I^^8z1faZBsB7P$f>M0KsT}RpJ@%nkvunbJlb zi9Fr+>FC;b4k!8=DSn+w#Bx>v+Qs0MNJYr@xT88ONJiB?%2?aJJ>TT7S|1?4!877a zG;$I}0)&&8DokDZ|ILIjXO_-MF;G4^+QY7;43#!!mo9eOcaRE05VYKD_Yiq&Wp;%G z*yN{*YYqhno0QHDnN1Z!Gq-V@$SN=v$68ykPBttL1=%w(TxZ1V=>l&M6DZKS44K=e zrPQtn1nX2%OJ^Do6CCWDnlZoXl|6YLpHJ*tm0U+TmkT6{)I5{%B!;6n6<-lm%dR_u zBEScEpUM^B`8OjF$AE^VWfYy{%Hgwvpyqf^-Ih&0v*4@k6N(=FRscODLG&$fuH(|k*uhfU&{`p z$T(j7>&3JP+;!JB2~O#OC;i;H7tp|jTf!HU*r7ELOT>V5y?bwVhn_#E%AevJ0mVf` z3{Qv?7ID-n42|EdSBmnNINDwGydBeRev8-J!6Ulr--L@q@Mwi{2Z^@>V?K;~NSBCA zNPiMaLl9T_1QJCpq?Tg5^vOxEsw3!(61hMDNBJ6l3?}yTQTo0J6Z&g^iAK-$8jh93I`}#~9@?=AcKW$vz<+YRy;DD!a4@f1 zt_V=f%92Krp?1sMkW(>f!qUR4#wl?2*3^qQc(@>k!LH|}{Qc(O!{SR{XU(g08>pQ; zJrq~S-`PexJS2OWLz1hQldV&9%+bf>MH8NpGcg@2z(*8~66xAvi&C9Sa#=Hq-6mJ^ z+s~Z_w7Y#j_iSjT1C+4b)@=dt=z;k;ssXRKn4es4R4S!Acd5{|u$(=WnI{ebcW-Zg z9X!xJH*ucj@8}y>kw^Rukcp?my+)&h{3K24?MT@*vw|0;i5UQh{YJOq1Z}B7ig`S`$#!O-U<=SwI?g_M~>m>6qH3+5iUk#z@MALAY zPLdUI-9CF~jSgLnM?9O%jq!j{r#}40zqq=(L^@?L4ikKz0~KD8nX~qEW_|qF-2>|B zPGFIL-GE<#Wt6EM@$tRS4EkjhV(;)5&N=qt+fTRqMEZq`=M0Nb!p%|FU0V)Ms51an zgY>p2eb?!!Z+8PdGaX{ZH}(?z86B^fvINPp7Xg_i7pA$gT{TVNQGKlRn#+pD2Cf%6vCcvvX!s zncrOtPqlYp8)%g9lQ^WRjKE>I+ndcZvC?c#3K>t26t!3< z8sGl+vomjYf@o4Emg81G0g`B%TZHu4`(=LO=;GA2m*{wT)T!#DT#{;3F9h)OLH~3Q zzLle2!M}m+WfQ-&HO9&65&>gB*SN&(vfGAd<=dMA1J?GkoCVuOo4n(+*=O@KZydfa zm>eM<)S2MK4Ph8ShZfv;+wiyuh^G}-Y}ATgjB$A48?B~A&h!|TzFD`9+++UI63(Ny z_xaZmzf0Cb(=Oe$Dw4y{zQP-z>sjL@Nh8U^zYU5WrepuI3+LNJ{BLPb4&2$xje)a2kcob| zbG*_QPxz0+xR+3gCU6D`Hh&J=JXT6eioP)~v!o&6?isH|bi~IXPcUZ{R*Kf50A%nf z-6{KF`T6!Xhr2kK+a~&`u)ke_XFh9DKgu*bOy57KxCFpi5b~knBL|x{uYTdNO+5)@ zG)>~_OqR%jGoaCo0NCs3$`SPC!pOlCh`ZPT9vh0)7M;^T1g&2{{Qt0u-rB&3ZA9 zPdYgow2$t%IxLKRNHAs3T1QJ-LFBwu48&*HH}dl+Fy$OrZOHm&>93a~wDo|}HZ1ZP zr;_t~qT+743>=p;Z{rCj41Jz6YZg#?O~&Evz5G7w`JOnxw5a06DGa;F!0V%JB^nbI z{`%vvf$SM6NtxXlXHTz!zP;hJKF-mmX9QqENqEL*8yu!*YtZn;Gf+7EVT68N@~^F@ zU{_k8hDV(g6WOA=KWMVg#6m}i{CaX_K*AB#xV5v;z&)u#TTmFzpyF<(r6aGUiHyS!OC*6ScT!CzsSCWvI+*=ltMAF!u#Pa3)b$o$Na(|Gww-dJ) z3V>_(S-ETfn4iV$uRHGRB#sdmGHJi+{`KJ>>!e=%F+hk%-%QzIO8FPKz$4q&=n*O@8NA5HtVPTAVSa@XLER z8T%y%A+ld4Es^RtFf4#dp{;6Wb8&irTi>t$W|rQxb;_s<#7r!#!{9G6n9mG&hb}HD0At2e zD&Ey#GTRy*+%R5^d6;E0(TbtnL)YAKd3e|6Em55gK z2xU_^@*$-&PR!)v(sM-$e5*Y}c3iIoM8F(Xr>$h7!U&SvDW1g0G13N@Na!(-#vnot zA$~qm{M%28(5CI2DRCZTqAnrq9iwlS!Xro8+KC>7r&=5|-zGF*E`3ik8g8lA83Aze z&YDL&<<-WOxjwd>yis&IF*%EIYNQBm)5r5ZyiSJx-7N#jxq! zs+hf!W!9`38Gz$V87T8a28KLhdSBB_nK&~a@_*G?k}$0x`H7&A#eYZP!TkDl6q(_>+w<3pS7oa>@DyL-N5IKYO-X` zo%4V?i;h1{FTNtyeQMd&?f5>9Y{pWJpa-Bnla5E^M4_>=qOUQG!k?xxXPw_fPBWa^ zdqGR#Hd2tn7xlPpkU1|}Ybri3NoF!%94@!~Mf|B#S07Wf?&+OkcRRH1nO0z6fhq4D zy{H)07v%MQ1P=7e=xW=H>d`%Z;~YMOmtVotrW^F{sn3v+T>0~_p=;&sUWCU;OD3RQ zD6qS83?QD69Nsg?h#3jJ1zkP(=3(5s_t@CwtT9yM#w~a8;+3GHVson-e)$5aA}uET zGc@(q`VwL9Quox$f8ivh~{d^wpVYm}pS7<*&h zLNHz7@wmf|M{eZT{jde63Y|Vh_QX*J9>c`v%lP-Jqb-h*ug(F)gl|MO<*0Rjk%hyv zP3Pk_T(=AIYG`{{fgj<=9}d}nL_HxYo};#Z(yONRgr0 zvb#s%Qcj5V+Q})}`p);~fg%HBVk~lly2}6T@c*XLugOKmKR1fT`&QZR{25?X(vuw% z_Qvcg(~O!m%XdC9{u!<_HScwWTISE|E3VDotF>$^XpVt(q32P?Xads>W&8fsTgmcx T{|tTE$N&VMu6{1-oD!M<4VqoG literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/sky_stars.png b/docs/validation/catalogue-paper/sky_stars.png new file mode 100644 index 0000000000000000000000000000000000000000..da876812ae6c38479f19b11c2f529280adf7a95b GIT binary patch literal 147928 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�oEGu7X1RzW zILg;C>G-#YO1wpv+4_*sFN45DwsOM8Q?U+P&kmNo{^@B7WC|@?bG}xb&{w3Vcu(8P ztdt~QK?Ip^^G;~e;ehu&kY=!I-bhN%i@~L%YXl>@HnQn%dlP38-8kvDg^ckP}j!Lkqjk`ZnjZ8)0?1K zkWfzEXzzT=bQqOxT4IIM=|3HMX7B8bBeO(Cj_ny<_-2Gs)a4e-aw=zH5qUBmI362H zS}8xF!jtss_oOr)%ppwqQhZo-Is=x*w|YPVG%T2ME2B^<6ko`=wu-Up)BH$&T#xB2 z!vKh;V=A3($!7ms^yf?sNI|yJk$5EBh-^Ke`QU2SFDC^PY*WK#O|luT6lDeuDe-*Q z$b2P*l&fmzcasa{9e$8uc;b=1avHLDRVqdf{=Sk{&r!|tOT~?g_ECU`P(i8DHYvzS zMVWn=p0ucaW260n(=f;`eN1ziix^nvhVTnJe*LAtA^}oz#Ukpm(1eIxKj@|PY}(gO z;u6wXC$pkc#x92(EBoj~3|gi?zsTbZc+LUrEb);7HQy%}374-f~DnrR!{N!uedklAvxDK;W|F^Y6KGOg=2}PB)nU8$NinF$lJ)5 z^8(Ov_52rqSGwylMYj*mkvy5T&T{5pY0ksTy#tRC5u@D~AF;T7f0H@cLS0?EY8d<| zLJ?~jj1M+X{f`PMp;6!ZR6o-=y+Db{(2{0~)Olr_Uez4Ldf;(+c`35E$)hU%0D>~5 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 H9{N836d;1o literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/tan_lambda.png b/docs/validation/catalogue-paper/tan_lambda.png new file mode 100644 index 0000000000000000000000000000000000000000..22b3cc1fb0d95f7a169a8dfb2aa67777bd67d07c GIT binary patch literal 35522 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

)G>l+5&vW_1MC5|AA^_vYc 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 JT-rGJ{{hln0q6h# literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/tan_psf_stars.png b/docs/validation/catalogue-paper/tan_psf_stars.png new file mode 100644 index 0000000000000000000000000000000000000000..9e4c5f782956eae97e5f275761f964645bca2991 GIT binary patch literal 31114 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

16bNxJ8+ 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 VfVj?rN;Cif002ovPDHLkV1iJSDd7MB literal 0 HcmV?d00001 diff --git a/docs/validation/catalogue-paper/weights.png b/docs/validation/catalogue-paper/weights.png new file mode 100644 index 0000000000000000000000000000000000000000..047b0a85ffbdeb6a2df51597c35d5911f0d883f8 GIT binary patch literal 32446 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 IpBTUTANBmcGynhq literal 0 HcmV?d00001 From 6f0acea31b699e2a9848b84eaa25714e5ffd7549 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Tue, 27 Oct 2026 07:08:55 -0400 Subject: [PATCH 47/53] fix(dashboard): mean-subtract lambda sources; rebuild stale patches; correlations memory Subtracts the survey-mean PSF and residual ellipticity before the lambda correlations, as TauStat does for galaxies. Reused jackknife patches are rebuilt when the galaxy count changes. The correlations job requests 128G. The alpha-by-magnitude caption states the global selection response. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9EMizpnZRRK8cFCVBK --- scripts/dashboard/build_correlations.sbatch | 4 ++-- src/sp_validation/dashboard/correlations.py | 19 +++++++++++++++---- src/sp_validation/dashboard/render.py | 6 ++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/scripts/dashboard/build_correlations.sbatch b/scripts/dashboard/build_correlations.sbatch index 97cb4d877..4b6c805f9 100644 --- a/scripts/dashboard/build_correlations.sbatch +++ b/scripts/dashboard/build_correlations.sbatch @@ -3,7 +3,7 @@ #SBATCH --account=def-sfabbro #SBATCH --time=01:30:00 #SBATCH --cpus-per-task=8 -#SBATCH --mem=96G +#SBATCH --mem=128G #SBATCH --output=%x-%j.log # # Catalogue-paper position-shear and alpha(theta) products @@ -28,7 +28,7 @@ # total ~20 min # Extrapolated to 20.5k tiles (~2x galaxies, stars, Gaia stars and randoms; # TreeCorr NG/GG time scales roughly with the lens and source counts): -# ~40-50 min and ~75-90 GB peak (alpha, stars), hence --mem=96G and 1.5 h. +# ~40-50 min and ~75-90 GB peak (alpha, stars), hence --mem=128G for headroom and 1.5 h. set -euo pipefail : "${CATALOGUE_DIR:?}" "${VERSION:?}" "${OUT_DIR:?}" diff --git a/src/sp_validation/dashboard/correlations.py b/src/sp_validation/dashboard/correlations.py index 203e6bcd1..3dc22185c 100644 --- a/src/sp_validation/dashboard/correlations.py +++ b/src/sp_validation/dashboard/correlations.py @@ -350,9 +350,14 @@ def main(argv=None): timings["read"] = time.time() - t0 patch_path = out / f"patches_{args.version}.npz" - if patch_path.exists(): - centres = np.load(patch_path)["centres"] - assert len(centres) == args.npatch, patch_path + n_gal = len(gal["RA"]) + saved = dict(np.load(patch_path)) if patch_path.exists() else {} + if ( + saved + and len(saved["centres"]) == args.npatch + and int(saved.get("n_galaxies", -1)) == n_gal + ): + centres = saved["centres"] else: every = max(1, len(gal["RA"]) // 2_000_000) sub = treecorr.Catalog( @@ -366,6 +371,7 @@ def main(argv=None): patch_path, centres=centres, npatch=args.npatch, + n_galaxies=n_gal, every_nth=every, seed=args.seed, region=str(args.region), @@ -511,6 +517,10 @@ def main(argv=None): stars["HSM_G2_STAR"] - stars["HSM_G2_PSF"], ), ): + # The survey-mean ellipticity is an additive offset, not structure + # correlated with galaxy positions; subtract it as TauStat does. + g1 = g1 - np.mean(g1) + g2 = g2 - np.mean(g2) src = catalog(stars["RA"], stars["DEC"], centres, g1=g1, g2=g2) results[label] = measure( lenses, src, randoms=randoms, num_threads=args.threads, **TC @@ -528,7 +538,8 @@ def main(argv=None): n_lenses=lenses.ntot, n_randoms=randoms.ntot, sources="PSF validation star detections (valid HSM): psf = HSM_G*_PSF " - "(lambda_1), residual = HSM_G*_STAR - HSM_G*_PSF (lambda_3)", + "(lambda_1), residual = HSM_G*_STAR - HSM_G*_PSF (lambda_3), " + "each with its mean subtracted", n_sources=len(stars), random_subtraction=True, seconds=timings["lambda"], diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index 9787ea122..a23d80a96 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -110,8 +110,10 @@ def load_version(run_dir, version, maps_dir): "psf_alpha_mag", "leakage", "Scale-dependent leakage α(θ) = τ₀/ρ₀ for all galaxies and in galaxy-magnitude " - "tertiles (calibrated e₁, e₂ rescaled to each bin's shear response, w_des; " - "jackknife errors). The paper's DES-Y3 comparison is omitted.", + "tertiles (calibrated e₁, e₂ rescaled to each bin's shear response plus the " + "global selection response, w_des; jackknife errors). The selection " + "response is larger for the faint bin, so its |α| is likely underestimated " + "by ~10–20%. The paper's DES-Y3 comparison is omitted.", "full", ), ( From 6058ffd7abe791ef3e33f7fca2509ff88a63a319 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Wed, 28 Oct 2026 06:45:44 -0400 Subject: [PATCH 48/53] =?UTF-8?q?feat(dashboard):=20Paper=20II=20scale-cut?= =?UTF-8?q?=20figures;=20C=E2=84=93=20data=20vector=20shows=20EE=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config-space scale-cut figure now matches Paper II's: pure ξ+B, pure ξ−B and a COSEBIs B_n (n ≤ 6) heatmap over every reporting-bin cut, from a new cosebis_grid product computed off the run's integration ξ± and covariance (build_dashboard.sh submits it alongside the other products). The harmonic figure is the single Cℓ BB heatmap, as in the paper. The Cℓ data-vector figure draws EE alone; BB and EB are with the B modes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9GepCh7jeFUzLzH2ce --- scripts/dashboard/build_dashboard.sh | 24 ++- src/sp_validation/dashboard/cosebis_grid.py | 152 ++++++++++++++++++ src/sp_validation/dashboard/data_vectors.py | 33 ++-- src/sp_validation/dashboard/products.py | 16 ++ src/sp_validation/dashboard/render.py | 19 ++- src/sp_validation/dashboard/scale_cuts.py | 103 +++--------- .../tests/test_dashboard_figures.py | 30 ++++ 7 files changed, 266 insertions(+), 111 deletions(-) create mode 100644 src/sp_validation/dashboard/cosebis_grid.py diff --git a/scripts/dashboard/build_dashboard.sh b/scripts/dashboard/build_dashboard.sh index 633ca813b..bc60755e3 100755 --- a/scripts/dashboard/build_dashboard.sh +++ b/scripts/dashboard/build_dashboard.sh @@ -1,9 +1,11 @@ #!/bin/bash # Build every cosmo_val dashboard figure for one catalogue snapshot through Slurm. # -# Submits the catalogue-level products (build_products.sbatch) and the -# position-shear products (build_correlations.sbatch) in parallel, then a render -# job that runs once both succeed. With --after, all of it waits for that job +# Submits the catalogue-level products (build_products.sbatch), the +# position-shear products (build_correlations.sbatch) and the COSEBIs +# scale-cut PTE grid (from the run's ξ± and covariance) in parallel, then a +# render job that runs once all succeed. A grid already in OUT_DIR/products is +# kept. With --after, all of it waits for that job # (e.g. the catalogue extraction) to finish successfully. # # Usage, from the repository root on a login node: @@ -43,12 +45,24 @@ correlations=$(sbatch --parsable --account="$account" "${dep[@]}" \ --output="$out/logs/%x-%j.log" \ --export=ALL,CATALOGUE_DIR="$catdir",VERSION="$version",OUT_DIR="$out/products",GAIA="$gaia",SIF="$SPV_IMAGE",SRC="$root/src" \ scripts/dashboard/build_correlations.sbatch) +grid="$out/products/cosebis_grid_$version.npz" +after_grid="" +if [ ! -e "$grid" ]; then + # #425 full grid, 210 cuts: see the cosebis-grid log for timing. + cosebis=$(sbatch --parsable --account="$account" --job-name=dashboard-cosebis-grid \ + --time=01:30:00 --cpus-per-task=16 --mem=32G \ + --output="$out/logs/%x-%j.log" \ + --wrap="module load apptainer && apptainer exec --bind /scratch,/project,/home \ +--env PYTHONPATH=$root/src $SPV_IMAGE python -m sp_validation.dashboard.cosebis_grid \ +--run-dir $rundir --version $version --out $grid") + after_grid=":$cosebis" +fi render=$(sbatch --parsable --account="$account" --job-name=dashboard-render \ - --dependency="afterok:$products:$correlations" \ + --dependency="afterok:$products:$correlations$after_grid" \ --time=00:30:00 --cpus-per-task=2 --mem=8G \ --output="$out/logs/%x-%j.log" \ --wrap="module load apptainer && apptainer exec --bind /scratch,/project,/home \ --env PYTHONPATH=$root/src $SPV_IMAGE python -m sp_validation.dashboard.render \ --run-dir $rundir --version $version --maps-dir $out/products \ --correlations-dir $out/products --out $out/figures") -echo "products $products correlations $correlations render $render" +echo "products $products correlations $correlations cosebis-grid ${cosebis:-kept} render $render" diff --git a/src/sp_validation/dashboard/cosebis_grid.py b/src/sp_validation/dashboard/cosebis_grid.py new file mode 100644 index 000000000..19a9ad0ba --- /dev/null +++ b/src/sp_validation/dashboard/cosebis_grid.py @@ -0,0 +1,152 @@ +"""COSEBIs B-mode PTEs over every reporting-bin scale cut (Paper II Fig. 8). + +For each contiguous range of reporting bins ``[i, j]`` on the 20-bin +1′–250′ grid, the COSEBIs are recomputed on the integration-grid ξ± between +those reporting edges, and the B-mode χ² against zero is taken over the first +6 and over all 20 modes with the analytic covariance carried through the same +kernel. Cuts whose COSEBIs filters fail to converge are NaN, as in the paper. + +The product is a cache read by :func:`sp_validation.dashboard.scale_cuts.config_space`; +computing it needs only the run's integration-grid ξ± part and its OneCovariance +covariance, never the catalogue. Usage:: + + python -m sp_validation.dashboard.cosebis_grid --run-dir RUN --version V \\ + --out cosebis_grid_V.npz [--nproc N] +""" + +import argparse +import json +import os +from concurrent.futures import ProcessPoolExecutor +from pathlib import Path + +import numpy as np + +SUBSETS = (6, 20) + + +def _paths(run_dir, version): + run_dir = Path(run_dir) + (xi,) = (run_dir / "cosmo_val").glob(f"{version}_xi_minsep=*_npatch=1.sacc") + (cov,) = (run_dir / "cosmo_inference/data/covariance").glob( + f"covariance_{version}_g_minsep=*_masked/covariance_{version}_g_minsep=*_masked.dat" + ) + return xi, cov + + +def _grid(run_dir): + """Reporting edges and integration binning from the run's configuration.""" + import yaml + + cfg = yaml.safe_load( + json.loads((Path(run_dir) / "provenance.json").read_text())["config"] + ) + cv = cfg["cosmo_val"] + edges = np.geomspace(cv["theta_min"], cv["theta_max"], cv["nbins"] + 1) + integ = cv["integration"] + return ( + edges, + (integ["min_sep"], integ["max_sep"], integ["nbins"]), + cv["cosebis"]["nmodes"], + ) + + +_STATE = {} + + +def _init(xi_path, cov_path, blind, integ, nmodes): + from sp_validation import sacc_io + from sp_validation.b_modes import log_bin_edges + + part = sacc_io.load(xi_path, blind=blind) + theta, xip, xim = sacc_io.get_xi(part, (0, 0), grid="integration") + _STATE.update( + theta=theta, + xip=xip, + xim=xim, + cov=np.loadtxt(cov_path), + edges=log_bin_edges(*integ), + nmodes=nmodes, + ) + + +def _pair(cut): + from scipy import stats + + from sp_validation.b_modes import cosebis_scan_from_xi + + s = _STATE + try: + (r,) = cosebis_scan_from_xi( + s["theta"], + s["xip"], + s["xim"], + s["cov"], + *s["edges"], + nmodes=s["nmodes"], + scale_cuts=[cut], + ).values() + except Exception as e: # sympy NoConvergence in the filter roots + print(f"cut {cut}: {type(e).__name__}: {e}", flush=True) + return [np.nan] * len(SUBSETS) + n = s["nmodes"] + cov_B = r["cov"][n:, n:] + out = [] + for k in SUBSETS: + b = r["Bn"][:k] + out.append(float(stats.chi2.sf(b @ np.linalg.solve(cov_B[:k, :k], b), k))) + return out + + +def compute(run_dir, version, nproc=1): + from sp_validation.dashboard.products import blind_of + + xi_path, cov_path = _paths(run_dir, version) + blind = blind_of(xi_path) + edges, integ, nmodes = _grid(run_dir) + n = len(edges) - 1 + pairs = [(i, j) for i in range(n) for j in range(i, n)] + cuts = [(float(edges[i]), float(edges[j + 1])) for i, j in pairs] + init = (str(xi_path), str(cov_path), blind, integ, nmodes) + if nproc > 1: + with ProcessPoolExecutor(nproc, initializer=_init, initargs=init) as pool: + results = list(pool.map(_pair, cuts)) + else: + _init(*init) + results = [_pair(c) for c in cuts] + pte = {k: np.full((n, n), np.nan) for k in SUBSETS} + for (i, j), vals in zip(pairs, results): + for k, v in zip(SUBSETS, vals): + pte[k][i, j] = v + return { + "left_edges": edges[:-1], + "right_edges": edges[1:], + **{f"pte_B_n{k}": pte[k] for k in SUBSETS}, + "blind": np.array(blind), + } + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + ap.add_argument("--run-dir", required=True) + ap.add_argument("--version", required=True) + ap.add_argument("--out", required=True) + ap.add_argument( + "--nproc", type=int, default=int(os.environ.get("SLURM_CPUS_PER_TASK", 1)) + ) + a = ap.parse_args(argv) + # One numba thread per worker; the parallelism is over cuts. + os.environ.setdefault( + "NUMBA_NUM_THREADS", "1" if a.nproc > 1 else str(os.cpu_count()) + ) + data = compute(a.run_dir, a.version, a.nproc) + out = Path(a.out) + out.parent.mkdir(parents=True, exist_ok=True) + tmp = out.with_suffix(".tmp.npz") + np.savez(tmp, **data) + tmp.replace(out) + print(f"wrote {out}") + + +if __name__ == "__main__": + main() diff --git a/src/sp_validation/dashboard/data_vectors.py b/src/sp_validation/dashboard/data_vectors.py index 36db37f92..3a1e1654e 100644 --- a/src/sp_validation/dashboard/data_vectors.py +++ b/src/sp_validation/dashboard/data_vectors.py @@ -93,13 +93,8 @@ def _cl_shade(cl, ell_cut): def ell_cl(cl, out, ell_cut, layout="wide", blind=None): - """ℓ C_ℓ for EE, BB and EB on a linear ℓ axis (EE blinded).""" + """Blinded ℓ C_ℓ^EE on a linear ℓ axis; BB and EB are drawn with the B modes.""" ell = cl["ell"] - series = [ - ("EE", "o", True, S.COLOR_TOTAL, r"$EE$"), - ("BB", "o", True, S.COLOR_CL_BB, r"$BB$"), - ("EB", "D", False, S.COLOR_CL_EB, r"$EB$"), - ] top = np.max(ell * (cl["EE"] + cl["sigma_EE"])) p = int(np.floor(np.log10(top))) scale = 10.0**p @@ -107,26 +102,22 @@ def ell_cl(cl, out, ell_cut, layout="wide", blind=None): with S.figure_style(): fig, ax = plt.subplots(figsize=size) S.zero_line(ax) - bandwidth = cl["ell_hi"] - cl["ell_lo"] + 1 - for (key, marker, filled, color, _), off in zip(series, (-0.18, 0.0, 0.18)): - ax.errorbar( - ell + off * bandwidth, - ell * cl[key] / scale, - yerr=ell * cl[f"sigma_{key}"] / scale, - marker=marker, - color=color, - markerfacecolor=color if filled else "none", - markeredgecolor=color, - **S.DATA_VECTOR_MARK, - ) + ax.errorbar( + ell, + ell * cl["EE"] / scale, + yerr=ell * cl["sigma_EE"] / scale, + marker="o", + color=S.COLOR_TOTAL, + **{**S.DATA_VECTOR_MARK, "markersize": 3.8}, + ) ax.set_xlim(0, cl["ell_hi"][-1] + 30) S.shade_outside(ax, *_cl_shade(cl, ell_cut)) ax.set_xlabel(r"$\ell$") ax.set_ylabel(rf"$\ell\, C_\ell \times 10^{{{-p}}}$") - S.label_box(ax, r"$C_\ell$") - _legend(fig, [_marker(m, c, lab, f) for _, m, f, c, lab in series], 3) + S.label_box(ax, r"$C_\ell^{EE}$") if blind: - S.blind_stamp(fig, blind, where="bottom") + fig.suptitle(" ", fontsize=7) + S.blind_stamp(fig, blind) return S.save(fig, out, "full" if layout == "wide" else "half") diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index 982d7decc..6bf253f92 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -109,6 +109,13 @@ def field_maps(self): Path(self.extra_dir or self.cv) / f"field_maps_{self.version}.npz" ) + @property + def cosebis_grid(self): + """COSEBIs B PTEs over every reporting-bin cut (:mod:`.cosebis_grid`).""" + return _maybe( + Path(self.extra_dir or self.cv) / f"cosebis_grid_{self.version}.npz" + ) + @property def weight_cells(self): return _maybe( @@ -209,6 +216,15 @@ def powspace_edges(ell_eff, lmin=None, lmax=2048, power=0.5): return lo, hi +def load_cosebis_grid(npz_path, blind): + """The COSEBIs scale-cut PTE grid, checked against the expected blind.""" + with np.load(npz_path) as f: + d = {k: f[k] for k in f.files} + if str(d.pop("blind")) != blind: + raise ValueError(f"{npz_path} was computed under another blind") + return d + + def load_cl(sacc_path, cov_path, blind): """Band powers, their multipole edges, and the NaMaster covariance blocks.""" from astropy.io import fits diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index a23d80a96..f37c46d2f 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -190,7 +190,7 @@ def render(run_dir, versions, out, maps_dir=None, corr_dir=None): figs.add( "cl", "data_vectors", - f"Cℓ (NaMaster, mode-decoupled) as ℓCℓ: EE (blinded), BB and EB. Shaded: outside {lcut}.", + f"Blinded Cℓ EE (NaMaster, mode-decoupled) as ℓCℓ. Shaded: outside {lcut}.", "full", narrow=True, ) @@ -222,15 +222,19 @@ def render(run_dir, versions, out, maps_dir=None, corr_dir=None): f"Cℓ BB and EB in units of their uncertainty. Shaded: outside {lcut}.", "half", ) + cg = P.load_cosebis_grid(ref.cosebis_grid, blind) if ref.cosebis_grid else None for layout, suffix in (("wide", ""), ("narrow", "_narrow")): - SC.config_space(pe, figs.path("ptes_config" + suffix), theta_cut, layout=layout) - SC.harmonic_and_cosebis( - cl, co, figs.path("ptes_harmonic" + suffix), ell_cut, layout=layout + SC.config_space( + pe, cg, figs.path("ptes_config" + suffix), theta_cut, layout=layout ) + SC.harmonic(cl, figs.path("ptes_harmonic"), ell_cut) figs.add( "ptes_config", "bmodes", - "Pure-B PTE for every (θmin, θmax) cut; the fiducial cut is outlined.", + "B-mode PTE for every (θmin, θmax) cut on the 20-bin reporting grid: pure " + "ξ+B, pure ξ−B" + + (" and COSEBIs Bₙ (n ≤ 6)" if cg is not None else "") + + "; the fiducial cut is outlined.", "full", narrow=True, group="scale_cuts", @@ -238,9 +242,8 @@ def render(run_dir, versions, out, maps_dir=None, corr_dir=None): figs.add( "ptes_harmonic", "bmodes", - "Cℓ BB PTE for every band range, and COSEBIs B PTE (n ≤ 20) for each cached cut.", - "full", - narrow=True, + f"Cℓ BB PTE for every band range; the fiducial {lcut} is outlined.", + "half", group="scale_cuts", ) if len(versions) > 1: diff --git a/src/sp_validation/dashboard/scale_cuts.py b/src/sp_validation/dashboard/scale_cuts.py index 8a9a906df..678640c74 100644 --- a/src/sp_validation/dashboard/scale_cuts.py +++ b/src/sp_validation/dashboard/scale_cuts.py @@ -1,4 +1,4 @@ -"""B-mode PTEs as a function of the scale cut (Paper II Figs. 8-9 style). +"""B-mode PTEs as a function of the scale cut (Paper II Figs. 8-9). Each heatmap cell is the PTE of the B-mode null test over the bins between a lower cut (x) and an upper cut (y); the fiducial cut is outlined. The colour @@ -78,25 +78,31 @@ def _fmt_angle(v): return f"{v:.0f}" if v >= 10 else f"{v:.1f}".rstrip("0").rstrip(".") -def config_space(pe, out, scale_cut, layout="wide"): - """ξ+^B, ξ−^B and joint ξ±^B PTEs over all reporting-bin scale cuts.""" - start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], scale_cut) - fid = (start, stop - 1) +def config_space(pe, cg, out, scale_cut, layout="wide"): + """ξ+^B, ξ−^B and COSEBIs B_n (n ≤ 6) PTEs over all reporting-bin scale cuts. + + ``cg`` is the COSEBIs grid product (:mod:`.cosebis_grid`); without it the + COSEBIs panel is left out. + """ lo = [_fmt_angle(v) for v in pe["left_edges"]] hi = [_fmt_angle(v) for v in pe["right_edges"]] + start, stop = bins_from_scale_cut(pe["left_edges"], pe["right_edges"], scale_cut) panels = [ - ("xip_B", r"$\xi_+^B$"), - ("xim_B", r"$\xi_-^B$"), - ("combined", r"$\xi_\pm^B$ joint"), + (pe["pte_matrices"]["xip_B"], (start, stop - 1), r"$\xi_+^B$"), + (pe["pte_matrices"]["xim_B"], (start, stop - 1), r"$\xi_-^B$"), ] + if cg is not None: + c0, c1 = bins_from_scale_cut(cg["left_edges"], cg["right_edges"], scale_cut) + panels.append((cg["pte_B_n6"], (c0, c1 - 1), r"COSEBIs $B_n$")) cmap, norm = S.pte_cmap(), S.pte_norm() + n = len(panels) with S.figure_style(): if layout == "wide": - fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.75), sharey=True) + fig, axes = plt.subplots(1, n, figsize=(2.4 * n, 2.75), sharey=True) else: - fig, axes = plt.subplots(3, 1, figsize=(3.6, 9.6), sharex=True) - for ax, (key, label) in zip(axes, panels): - mesh = _heatmap(ax, pe["pte_matrices"][key], lo, hi, fid, label, cmap, norm) + fig, axes = plt.subplots(n, 1, figsize=(3.6, 3.2 * n), sharex=True) + for ax, (pte, fid, label) in zip(axes, panels): + mesh = _heatmap(ax, pte, lo, hi, fid, label, cmap, norm) for ax in axes if layout == "wide" else axes[-1:]: ax.set_xlabel(r"$\theta_{\min}$ [arcmin]") for ax in axes[:1] if layout == "wide" else axes: @@ -113,8 +119,8 @@ def config_space(pe, out, scale_cut, layout="wide"): return S.save(fig, out, "full" if layout == "wide" else "half") -def harmonic_and_cosebis(cl, co, out, ell_cut, layout="wide"): - """C_ℓ^BB PTEs over band ranges, beside COSEBIs B_n PTEs per cached cut.""" +def harmonic(cl, out, ell_cut): + """C_ℓ^BB PTEs over every contiguous band range (Paper II Fig. 9).""" pte = cl_pte_matrix(cl, "BB") inside = np.flatnonzero((cl["ell_lo"] >= ell_cut[0]) & (cl["ell_hi"] <= ell_cut[1])) fid = (inside[0], inside[-1]) @@ -122,66 +128,9 @@ def harmonic_and_cosebis(cl, co, out, ell_cut, layout="wide"): hi = [f"{v:.0f}" for v in cl["ell_hi"]] cmap, norm = S.pte_cmap(), S.pte_norm() with S.figure_style(): - if layout == "wide": - fig, (ax_cl, ax_co) = plt.subplots( - 1, 2, figsize=(7.2, 3.0), width_ratios=(1, 1.15) - ) - else: - fig, (ax_cl, ax_co) = plt.subplots( - 2, 1, figsize=(3.6, 6.6), height_ratios=(1.25, 1) - ) - mesh = _heatmap(ax_cl, pte, lo, hi, fid, r"$C_\ell^{BB}$", cmap, norm) - ax_cl.set_xlabel(r"$\ell_{\min}$") - ax_cl.set_ylabel(r"$\ell_{\max}$") - _cosebis_cuts(ax_co, co, cmap, norm) - S.pte_colorbar( - fig, - mesh, - [ax_cl, ax_co], - aspect=25, - pad=0.02, - shrink=0.9 if layout == "wide" else 0.6, - location="right" if layout == "wide" else "bottom", - ) - return S.save(fig, out, "full" if layout == "wide" else "half") - - -def _cosebis_cuts(ax, co, cmap, norm, threshold=S.PTE_THRESHOLD): - """One row per cached COSEBIs cut: the B_n PTE on a [0, 1] axis.""" - cuts = sorted(co["cuts"], key=lambda c: (c[1] - c[0], c[0]), reverse=True) - y = np.arange(len(cuts))[::-1] - ax.axvspan(0, threshold, color=cmap(0), alpha=0.18, lw=0) - ax.axvspan(1 - threshold, 1, color=cmap(cmap.N - 1), alpha=0.18, lw=0) - for yi, cut in zip(y, cuts): - p = co["cuts"][cut]["pte_B"] - is_fid = cut == co["fiducial"] - ax.hlines(yi, 0, 1, color=S.RULE, lw=0.6, zorder=1) - ax.scatter( - [p], - [yi], - s=46 if is_fid else 30, - color=cmap(norm(p)), - edgecolor=S.INK, - linewidth=1.4 if is_fid else 0.6, - zorder=3, - marker="s" if is_fid else "o", - ) - ax.text( - min(p + 0.035, 0.97), - yi + 0.18, - f"${S.fmt_pte(p)}$", - fontsize=7.5, - color=S.INK_MUTED, - va="bottom", - ha="left" if p < 0.85 else "right", - ) - ax.set_yticks(y) - ax.set_yticklabels([rf"${c[0]:g}'\!-\!{c[1]:g}'$" for c in cuts]) - ax.set_xlim(0, 1) - ax.set_ylim(-0.7, len(cuts) - 0.3) - ticks = (threshold, 0.5, 1 - threshold) - ax.set_xticks(ticks) - ax.set_xticklabels([f"${t:g}$" for t in ticks]) - ax.tick_params(which="both", top=False, right=False, left=False) - ax.set_xlabel(r"$B_n$ PTE ($n \leq %d$)" % co["nmodes"]) - S.label_box(ax, "COSEBIs", loc="lower right") + fig, ax = plt.subplots(figsize=(3.6, 3.2)) + mesh = _heatmap(ax, pte, lo, hi, fid, r"$C_\ell^{BB}$", cmap, norm) + ax.set_xlabel(r"$\ell_{\min}$") + ax.set_ylabel(r"$\ell_{\max}$") + S.pte_colorbar(fig, mesh, [ax], aspect=25, pad=0.02, shrink=0.9) + return S.save(fig, out, "half") diff --git a/src/sp_validation/tests/test_dashboard_figures.py b/src/sp_validation/tests/test_dashboard_figures.py index 5c4ee6256..09493bc34 100644 --- a/src/sp_validation/tests/test_dashboard_figures.py +++ b/src/sp_validation/tests/test_dashboard_figures.py @@ -160,3 +160,33 @@ def test_coarsen_sums_children(): out, sums = coarsen(pixels, {"n": np.ones(64)}, nside=4, nside_out=2) np.testing.assert_array_equal(out, np.arange(16)) np.testing.assert_array_equal(sums["n"], np.full(16, 4)) + + +def test_cosebis_grid_panel_and_blind_check(tmp_path): + from sp_validation.dashboard.products import load_cosebis_grid + from sp_validation.dashboard.scale_cuts import config_space + + edges = np.geomspace(1, 250, 21) + n = 20 + tri = np.where(np.triu(np.ones((n, n))) > 0, 0.5, np.nan) + path = tmp_path / "cosebis_grid_V.npz" + np.savez( + path, + left_edges=edges[:-1], + right_edges=edges[1:], + pte_B_n6=tri, + pte_B_n20=tri, + blind=np.array("unions"), + ) + with pytest.raises(ValueError, match="another blind"): + load_cosebis_grid(path, "other") + cg = load_cosebis_grid(path, "unions") + pe = { + "left_edges": edges[:-1], + "right_edges": edges[1:], + "pte_matrices": {"xip_B": tri, "xim_B": tri}, + } + for layout in ("wide", "narrow"): + out = tmp_path / f"ptes_{layout}.png" + config_space(pe, cg, out, (12, 83), layout=layout) + assert out.exists() From e93c07dd2032f61f077ba9066dbb19d9515f5047 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Wed, 28 Oct 2026 08:03:51 -0400 Subject: [PATCH 49/53] fix(dashboard): COSEBIs grid falls back to 6 modes where 20 fail to converge Log-COSEBIs filters of mode n do not depend on the requested mode count (checked bit-identical at 12'-83' and 1'-250'), so narrow cuts whose 20-mode roots fail still get their n <= 6 PTE. Fills the 18 holes on the full-area run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9GepCh7jeFUzLzH2ce --- src/sp_validation/dashboard/cosebis_grid.py | 38 +++++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/sp_validation/dashboard/cosebis_grid.py b/src/sp_validation/dashboard/cosebis_grid.py index 19a9ad0ba..bc68d7a66 100644 --- a/src/sp_validation/dashboard/cosebis_grid.py +++ b/src/sp_validation/dashboard/cosebis_grid.py @@ -4,7 +4,8 @@ 1′–250′ grid, the COSEBIs are recomputed on the integration-grid ξ± between those reporting edges, and the B-mode χ² against zero is taken over the first 6 and over all 20 modes with the analytic covariance carried through the same -kernel. Cuts whose COSEBIs filters fail to converge are NaN, as in the paper. +kernel. Where the 20-mode filters fail to converge (narrow cuts), the n ≤ 6 +PTE comes from a 6-mode computation and the n ≤ 20 PTE is NaN. The product is a cache read by :func:`sp_validation.dashboard.scale_cuts.config_space`; computing it needs only the run's integration-grid ξ± part and its OneCovariance @@ -70,9 +71,9 @@ def _init(xi_path, cov_path, blind, integ, nmodes): ) -def _pair(cut): - from scipy import stats - +def _modes(cut, nmodes): + """B-mode COSEBIs and their covariance for one cut, or None if the filter + roots fail to converge.""" from sp_validation.b_modes import cosebis_scan_from_xi s = _STATE @@ -83,18 +84,33 @@ def _pair(cut): s["xim"], s["cov"], *s["edges"], - nmodes=s["nmodes"], + nmodes=nmodes, scale_cuts=[cut], ).values() except Exception as e: # sympy NoConvergence in the filter roots - print(f"cut {cut}: {type(e).__name__}: {e}", flush=True) - return [np.nan] * len(SUBSETS) - n = s["nmodes"] - cov_B = r["cov"][n:, n:] + print(f"cut {cut}, {nmodes} modes: {type(e).__name__}: {e}", flush=True) + return None + return r["Bn"], r["cov"][nmodes:, nmodes:] + + +def _pair(cut): + """B PTEs over the first k modes for each k in SUBSETS. + + The log-COSEBIs filter of mode n does not depend on how many modes are + requested, so a cut whose full mode set fails to converge is retried + with only the modes a smaller subset needs. + """ + from scipy import stats + + full = _modes(cut, _STATE["nmodes"]) out = [] for k in SUBSETS: - b = r["Bn"][:k] - out.append(float(stats.chi2.sf(b @ np.linalg.solve(cov_B[:k, :k], b), k))) + r = full if full is not None or k >= _STATE["nmodes"] else _modes(cut, k) + if r is None: + out.append(np.nan) + continue + b, cov_B = r[0][:k], r[1][:k, :k] + out.append(float(stats.chi2.sf(b @ np.linalg.solve(cov_B, b), k))) return out From c8e1d39cc717f70d3f9f8d284bb3ef8116dc8f72 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Wed, 28 Oct 2026 15:14:46 -0400 Subject: [PATCH 50/53] Default cov_estimate_method to th in the rho/tau driver Matches the integration fix e4805219 so the launched-checkout regression test passes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9GhUCHD92bXVznvHP4 --- workflow/scripts/run_rho_tau.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/scripts/run_rho_tau.py b/workflow/scripts/run_rho_tau.py index 21712470d..995fbdc17 100644 --- a/workflow/scripts/run_rho_tau.py +++ b/workflow/scripts/run_rho_tau.py @@ -18,7 +18,7 @@ npatch=int(params["npatch"]), catalog_config=params["cat_config"], output_dir=params["output_dir"], - cov_estimate_method=params["cov_estimate_method"], + cov_estimate_method=params.get("cov_estimate_method", "th"), ) cv.calculate_rho_tau_stats(tomography=False) verify_outputs(snakemake) From 3a3520cefb7c4cb1d864392d83768517ae6cf005 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Thu, 29 Oct 2026 18:20:07 -0400 Subject: [PATCH 51/53] fix(dashboard): expect each version's declared blind, not the product's own stamp The dashboard read its expected blind from the assembled SACC's stamp, so a stale public SACC (and matching public caches) of a catalogue since declared under a blind was drawn raw. The renderer now takes --cat-config and asks blinding.blind_of for the version's declaration, resolving _leak_corr/_seed variants as the workflow does; every cosmological loader, rho/tau included, checks against it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9H9kWBd294QyzLNrxc --- src/sp_validation/dashboard/products.py | 32 +++++++++++++---- src/sp_validation/dashboard/render.py | 22 ++++++++---- .../tests/test_blind_consumers.py | 36 +++++++++++++++++++ 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/sp_validation/dashboard/products.py b/src/sp_validation/dashboard/products.py index 6bf253f92..20cfd5597 100644 --- a/src/sp_validation/dashboard/products.py +++ b/src/sp_validation/dashboard/products.py @@ -3,7 +3,8 @@ Every loader takes explicit paths and returns a dict; none measures, fits or transforms anything. Cosmological products are read through the blind-stamp checks of :mod:`sp_validation.sacc_io`, so a figure can only draw vectors -saved under the blind it was asked for. +saved under the blind its catalogue is declared under +(:func:`declared_blind`). :class:`RunProducts` resolves those paths inside a cosmo_val run directory for one catalogue version; the figure functions never see the directory layout. @@ -123,9 +124,28 @@ def weight_cells(self): ) -def blind_of(sacc_path): - """The blind a SACC was saved under.""" - return sacc_io.stamp(sacc_io.load(sacc_path)) +def catalogue_name(catalogues, version): + """The catalogue-config entry describing ``version``: its own, or the one + its ``_leak_corr`` / ``_seed`` variant derives from (as the workflow + resolves it).""" + if version in catalogues: + return version + return re.sub(r"_seed\d+$", "", re.sub(r"_leak_corr$", "", version)) + + +def declared_blind(cat_config, version): + """The blind ``version`` is declared under in the catalogue config. + + The expected blind comes from the declaration, never from a product's own + stamp, so a stale product of a catalogue that has since been put under a + blind is refused by the loaders rather than drawn. + """ + import yaml + + from sp_validation import blinding + + catalogues = yaml.safe_load(Path(cat_config).read_text()) + return blinding.blind_of(catalogues, catalogue_name(catalogues, version)) def load_xi(sacc_path, blind): @@ -253,9 +273,9 @@ def load_cl(sacc_path, cov_path, blind): } -def load_rho_tau(sacc_path, tau_cov_path, psf_fit_path): +def load_rho_tau(sacc_path, tau_cov_path, psf_fit_path, blind): """ρ/τ statistics with their covariance blocks, and the PSF-fit samples.""" - s = sacc_io.load(sacc_path) + s = sacc_io.load(sacc_path, blind=blind) cov = s.covariance.dense rho = {} for k in range(6): diff --git a/src/sp_validation/dashboard/render.py b/src/sp_validation/dashboard/render.py index f37c46d2f..ff4c9f670 100644 --- a/src/sp_validation/dashboard/render.py +++ b/src/sp_validation/dashboard/render.py @@ -88,9 +88,9 @@ def add(self, fid, section, caption, size, narrow=False, group=None): self.entries.append(entry) -def load_version(run_dir, version, maps_dir): +def load_version(run_dir, version, maps_dir, cat_config): r = P.RunProducts(run_dir, version, maps_dir) - blind = P.blind_of(r.assembled) + blind = P.declared_blind(cat_config, version) return ( r, blind, @@ -165,10 +165,10 @@ def render_correlations(figs, corr_dir, version): figs.add(fid, section, caption, size) -def render(run_dir, versions, out, maps_dir=None, corr_dir=None): +def render(run_dir, versions, out, cat_config, maps_dir=None, corr_dir=None): cuts = run_config(run_dir) theta_cut, ell_cut, full_cut = cuts["theta"], cuts["ell"], cuts["theta_full"] - loaded = [load_version(run_dir, v, maps_dir) for v in versions] + loaded = [load_version(run_dir, v, maps_dir, cat_config) for v in versions] ref, blind, d = loaded[0] pe, co, cl = d["pe"], d["co"], d["cl"] xi = P.load_xi(ref.assembled, blind) @@ -311,7 +311,7 @@ def render(run_dir, versions, out, maps_dir=None, corr_dir=None): ) else: print("Skipping star_residuals: product missing") - rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit) + rt = P.load_rho_tau(ref.assembled, ref.tau_cov, ref.psf_fit, blind) PSF.tau_fit(rt, figs.path("psf_tau")) figs.add( "psf_tau", @@ -594,12 +594,22 @@ def main(argv=None): required=True, help="catalogue version; repeat for a comparison run", ) + parser.add_argument( + "--cat-config", + required=True, + help="catalogue config declaring each version's blind (the run's cat_config.yaml)", + ) parser.add_argument("--maps-dir", help="directory of field_maps/weight_cells NPZs") parser.add_argument("--correlations-dir", help="directory of correlations NPZs") parser.add_argument("--out", required=True) args = parser.parse_args(argv) manifest = render( - args.run_dir, args.version, args.out, args.maps_dir, args.correlations_dir + args.run_dir, + args.version, + args.out, + args.cat_config, + args.maps_dir, + args.correlations_dir, ) print(f"{len(manifest['figures'])} figures -> {args.out}") diff --git a/src/sp_validation/tests/test_blind_consumers.py b/src/sp_validation/tests/test_blind_consumers.py index 2cb4fcb7f..06f5a8cdf 100644 --- a/src/sp_validation/tests/test_blind_consumers.py +++ b/src/sp_validation/tests/test_blind_consumers.py @@ -83,3 +83,39 @@ def test_load_checks_expected_blind(part_path, as_name, stamp): sacc_io.load(part_path, blind=expected) else: assert sacc_io.stamp(sacc_io.load(part_path, blind=expected)) == "toy" + + +def _cat_config(tmp_path, blind): + import yaml + + path = tmp_path / "cat_config.yaml" + path.write_text(yaml.safe_dump({"SP_v": {"blind": blind}, "paths": {}})) + return path + + +@pytest.mark.parametrize("version", ["SP_v", "SP_v_leak_corr"]) +def test_dashboard_takes_blind_from_declaration(tmp_path, version): + from sp_validation.dashboard import products + + assert products.declared_blind(_cat_config(tmp_path, "toy"), version) == "toy" + + +@pytest.mark.parametrize("declared", ["none", "toy"]) +def test_dashboard_refuses_stale_public_part( + part_path, tmp_path, monkeypatch, declared +): + """A product saved public, of a catalogue since declared under a blind, is + refused before its signal is read.""" + from sp_validation.dashboard import products + + class ReachedSignal(Exception): + pass + + def read_signal(*args, **kwargs): + raise ReachedSignal + + monkeypatch.setattr(sacc_io, "xi_correlation", read_signal) + blind = products.declared_blind(_cat_config(tmp_path, declared), "SP_v") + error = ReachedSignal if declared == "none" else ValueError + with pytest.raises(error): + products.load_xi(part_path, blind) From fe16e5369960b813ec698d3fec553768f9cf85c6 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Thu, 29 Oct 2026 18:22:01 -0400 Subject: [PATCH 52/53] fix(dashboard): the COSEBIs scale-cut grid expects the declared blind cosebis_grid took its blind from the xi SACC's own stamp and stamped the grid it derived with it, so a stale public xi of a catalogue since declared under a blind produced a public grid. It now takes --cat-config, like the renderer; build_dashboard.sh passes --cat-config (default: the run directory's cat_config.yaml, else the repository's) to both. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9H9kWBd294QyzLNrxc --- scripts/dashboard/build_dashboard.sh | 18 ++++++++++--- src/sp_validation/dashboard/cosebis_grid.py | 13 +++++++--- .../tests/test_blind_consumers.py | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/scripts/dashboard/build_dashboard.sh b/scripts/dashboard/build_dashboard.sh index bc60755e3..4c89209d2 100755 --- a/scripts/dashboard/build_dashboard.sh +++ b/scripts/dashboard/build_dashboard.sh @@ -8,9 +8,13 @@ # kept. With --after, all of it waits for that job # (e.g. the catalogue extraction) to finish successfully. # +# Every cosmological product is checked against the blind its version is +# declared under in the catalogue config: --cat-config, else RUN_DIR's +# cat_config.yaml, else the repository's cosmo_val/cat_config.yaml. +# # Usage, from the repository root on a login node: # scripts/dashboard/build_dashboard.sh CATALOGUE_DIR RUN_DIR VERSION OUT_DIR \ -# [--gaia GAIA_FITS] [--after JOBID] +# [--gaia GAIA_FITS] [--after JOBID] [--cat-config CAT_CONFIG] # Products go to OUT_DIR/products, figures and figures.json to OUT_DIR/figures. # ACCOUNT (default def-sfabbro) and SPV_IMAGE (default the pinned SIF) are read # from the environment. @@ -22,16 +26,24 @@ out=$(realpath -m "${4:?output directory}") shift 4 gaia="" after="" +catconfig="" while [ $# -gt 0 ]; do case $1 in --gaia) gaia=$(realpath "$2"); shift 2 ;; --after) after=$2; shift 2 ;; + --cat-config) catconfig=$(realpath "$2"); shift 2 ;; *) echo "unknown argument: $1" >&2; exit 2 ;; esac done root=$(git rev-parse --show-toplevel) cd "$root" +if [ -z "$catconfig" ]; then + catconfig="$rundir/cat_config.yaml" + [ -e "$catconfig" ] || catconfig="$root/cosmo_val/cat_config.yaml" +fi +[ -e "$catconfig" ] || { echo "no catalogue config: $catconfig" >&2; exit 2; } +echo "blinds declared by $catconfig" account=${ACCOUNT:-def-sfabbro} export SPV_IMAGE=${SPV_IMAGE:-/project/6001537/cdaley/containers/sp_validation-ddd58fe0.sif} mkdir -p "$out/products" "$out/figures" "$out/logs" @@ -54,7 +66,7 @@ if [ ! -e "$grid" ]; then --output="$out/logs/%x-%j.log" \ --wrap="module load apptainer && apptainer exec --bind /scratch,/project,/home \ --env PYTHONPATH=$root/src $SPV_IMAGE python -m sp_validation.dashboard.cosebis_grid \ ---run-dir $rundir --version $version --out $grid") +--run-dir $rundir --version $version --cat-config $catconfig --out $grid") after_grid=":$cosebis" fi render=$(sbatch --parsable --account="$account" --job-name=dashboard-render \ @@ -63,6 +75,6 @@ render=$(sbatch --parsable --account="$account" --job-name=dashboard-render \ --output="$out/logs/%x-%j.log" \ --wrap="module load apptainer && apptainer exec --bind /scratch,/project,/home \ --env PYTHONPATH=$root/src $SPV_IMAGE python -m sp_validation.dashboard.render \ ---run-dir $rundir --version $version --maps-dir $out/products \ +--run-dir $rundir --version $version --cat-config $catconfig --maps-dir $out/products \ --correlations-dir $out/products --out $out/figures") echo "products $products correlations $correlations cosebis-grid ${cosebis:-kept} render $render" diff --git a/src/sp_validation/dashboard/cosebis_grid.py b/src/sp_validation/dashboard/cosebis_grid.py index bc68d7a66..6179034a8 100644 --- a/src/sp_validation/dashboard/cosebis_grid.py +++ b/src/sp_validation/dashboard/cosebis_grid.py @@ -114,11 +114,11 @@ def _pair(cut): return out -def compute(run_dir, version, nproc=1): - from sp_validation.dashboard.products import blind_of +def compute(run_dir, version, cat_config, nproc=1): + from sp_validation.dashboard.products import declared_blind xi_path, cov_path = _paths(run_dir, version) - blind = blind_of(xi_path) + blind = declared_blind(cat_config, version) edges, integ, nmodes = _grid(run_dir) n = len(edges) - 1 pairs = [(i, j) for i in range(n) for j in range(i, n)] @@ -147,6 +147,11 @@ def main(argv=None): ap.add_argument("--run-dir", required=True) ap.add_argument("--version", required=True) ap.add_argument("--out", required=True) + ap.add_argument( + "--cat-config", + required=True, + help="catalogue config declaring the version's blind", + ) ap.add_argument( "--nproc", type=int, default=int(os.environ.get("SLURM_CPUS_PER_TASK", 1)) ) @@ -155,7 +160,7 @@ def main(argv=None): os.environ.setdefault( "NUMBA_NUM_THREADS", "1" if a.nproc > 1 else str(os.cpu_count()) ) - data = compute(a.run_dir, a.version, a.nproc) + data = compute(a.run_dir, a.version, a.cat_config, a.nproc) out = Path(a.out) out.parent.mkdir(parents=True, exist_ok=True) tmp = out.with_suffix(".tmp.npz") diff --git a/src/sp_validation/tests/test_blind_consumers.py b/src/sp_validation/tests/test_blind_consumers.py index 06f5a8cdf..d5ffbc8b4 100644 --- a/src/sp_validation/tests/test_blind_consumers.py +++ b/src/sp_validation/tests/test_blind_consumers.py @@ -119,3 +119,28 @@ def read_signal(*args, **kwargs): error = ReachedSignal if declared == "none" else ValueError with pytest.raises(error): products.load_xi(part_path, blind) + + +@pytest.mark.parametrize("declared", ["none", "toy"]) +def test_cosebis_grid_refuses_stale_public_xi( + part_path, tmp_path, monkeypatch, declared +): + """The scale-cut grid derives from ξ under the declared blind, not ξ's stamp.""" + import numpy as np + + from sp_validation.dashboard import cosebis_grid + + class ReachedSignal(Exception): + pass + + def read_signal(*args, **kwargs): + raise ReachedSignal + + monkeypatch.setattr(sacc_io, "get_xi", read_signal) + monkeypatch.setattr(cosebis_grid, "_paths", lambda r, v: (part_path, part_path)) + monkeypatch.setattr( + cosebis_grid, "_grid", lambda r: (np.geomspace(1, 100, 3), (1, 100, 10), 6) + ) + error = ReachedSignal if declared == "none" else ValueError + with pytest.raises(error): + cosebis_grid.compute(tmp_path, "SP_v", _cat_config(tmp_path, declared)) From 8a7b055b2703b3074e1398c7cf8ec10a0c4390db Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Thu, 29 Oct 2026 18:32:02 -0400 Subject: [PATCH 53/53] fix(dashboard): build_dashboard.sh refuses to guess the catalogue config Falling back to the repository's cosmo_val/cat_config.yaml checked a run's products against that file's declarations, which say none for shared names, so a stale public product of a version the run declared under a blind passed. The script now takes --cat-config or RUN_DIR/cat_config.yaml and exits 2 with neither. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JK7o9H9kWBd294QyzLNrxc --- scripts/dashboard/build_dashboard.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/dashboard/build_dashboard.sh b/scripts/dashboard/build_dashboard.sh index 4c89209d2..ee1430460 100755 --- a/scripts/dashboard/build_dashboard.sh +++ b/scripts/dashboard/build_dashboard.sh @@ -9,8 +9,8 @@ # (e.g. the catalogue extraction) to finish successfully. # # Every cosmological product is checked against the blind its version is -# declared under in the catalogue config: --cat-config, else RUN_DIR's -# cat_config.yaml, else the repository's cosmo_val/cat_config.yaml. +# declared under in the run's catalogue config: --cat-config, else RUN_DIR's +# cat_config.yaml. With neither, the script refuses to start. # # Usage, from the repository root on a login node: # scripts/dashboard/build_dashboard.sh CATALOGUE_DIR RUN_DIR VERSION OUT_DIR \ @@ -38,11 +38,11 @@ done root=$(git rev-parse --show-toplevel) cd "$root" -if [ -z "$catconfig" ]; then - catconfig="$rundir/cat_config.yaml" - [ -e "$catconfig" ] || catconfig="$root/cosmo_val/cat_config.yaml" -fi -[ -e "$catconfig" ] || { echo "no catalogue config: $catconfig" >&2; exit 2; } +catconfig=${catconfig:-$rundir/cat_config.yaml} +[ -e "$catconfig" ] || { + echo "no catalogue config at $catconfig: pass --cat-config with the config the run declared its blinds in" >&2 + exit 2 +} echo "blinds declared by $catconfig" account=${ACCOUNT:-def-sfabbro} export SPV_IMAGE=${SPV_IMAGE:-/project/6001537/cdaley/containers/sp_validation-ddd58fe0.sif}