From efb32b2082f39ae714ddcdda907069cbc92fb591 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Fri, 25 Sep 2026 14:41:17 +0200 Subject: [PATCH] chore: remove dead scripts and fix stale doc paths Deletes code with no live caller: - workflow/scripts/run_2pcf_highres.py and the parked, unrunnable xi_highres block in workflow/rules/twopoint.smk that was its only reference; the mpi4py comment in pyproject.toml no longer names it. - papers/bmodes/scripts/run_cov_sweep.sh, which calls workflow/scripts/run_cosmocov_chain.sh, absent from develop. Fixes docs that point at paths or tools that do not exist: - CLAUDE.md, CONTRIBUTING.md: single-test example uses test_cosmo_val.py. - CLAUDE.md: cosmo_inference runs through Snakemake (inference_fiducial), not a pipeline.sh driver. - papers/{bmodes,cosmo_val}/Snakefile: read cat_config through code/ rather than the deprecated pure_eb symlink. - ecut_spec.md, update_survey_stats.py: paths under papers/bmodes/. - README badge and installation docs: Python 3.12 floor, matching requires-python. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HeMhD6yCyrgBz5oz87bCtR --- CLAUDE.md | 14 +- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/source/installation.rst | 2 +- papers/bmodes/Snakefile | 2 +- papers/bmodes/config/ecut_spec.md | 10 +- papers/bmodes/scripts/run_cov_sweep.sh | 68 ---- papers/bmodes/scripts/update_survey_stats.py | 2 +- papers/cosmo_val/Snakefile | 2 +- pyproject.toml | 4 +- workflow/rules/twopoint.smk | 49 --- workflow/scripts/run_2pcf_highres.py | 394 ------------------- 12 files changed, 21 insertions(+), 530 deletions(-) delete mode 100755 papers/bmodes/scripts/run_cov_sweep.sh delete mode 100644 workflow/scripts/run_2pcf_highres.py diff --git a/CLAUDE.md b/CLAUDE.md index c35c20be..b4447758 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ Tests live in `src/sp_validation/tests/` and import the full scientific stack, so run them inside the container. - Run all tests: `pytest` (collects from `src/sp_validation/tests`; coverage on by default) - Skip the slow tests: `pytest -m "not slow"` -- Run a single test: `pytest src/sp_validation/tests/test_cosmology.py::test_function_name` +- Run a single test: `pytest src/sp_validation/tests/test_cosmo_val.py::test_function_name` CI runs this same suite inside the freshly-built image before publishing it (see `.github/workflows/deploy-image.yml`). @@ -57,11 +57,13 @@ is the container (full scientific stack pre-built). For a local dev environment: - **Healpy/HealSparse**: Sky map handling ### Cosmology Inference Pipeline (`cosmo_inference/`) -Run via `./pipeline.sh` with flags: -- `--pcf`: Calculate 2-point correlation functions -- `--covmat`: Calculate covariance matrix with CosmoCov -- `--inference`: Run CosmoSIS inference -- `--mcmc_process`: Analyze MCMC chains +Orchestrated through Snakemake, not a standalone driver; see +`cosmo_inference/README.md`. From the repository root: + +```bash +snakemake --profile workflow/profiles/candide -s workflow/Snakefile \ + inference_fiducial --configfile +``` ### Configuration Main configuration in `scripts/calibration/params.py` with parameters: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e385a386..0406741b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ toolchain (`autoconf`, `automake`, `libtool`, `pkg-config`) available. ```bash pytest # full suite pytest -m "not slow" # skip the slow tests -pytest src/sp_validation/tests/test_cosmology.py::test_name # a single test +pytest src/sp_validation/tests/test_cosmo_val.py::test_name # a single test ``` Tests live in `src/sp_validation/tests/`. The default options (configured in diff --git a/README.md b/README.md index 455ddd91..1d269a0c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Validation of weak-lensing catalogues (galaxy and star shapes and other paramete [![docs](https://img.shields.io/badge/docs-sphinx-blue)](https://cosmostat.github.io/sp_validation/) [![CI](https://github.com/CosmoStat/sp_validation/actions/workflows/deploy-image.yml/badge.svg)](https://github.com/CosmoStat/sp_validation/actions/workflows/deploy-image.yml) [![container](https://img.shields.io/badge/container-ghcr.io-2496ED?logo=docker&logoColor=white)](https://github.com/CosmoStat/sp_validation/pkgs/container/sp_validation) -[![python](https://img.shields.io/badge/python-3.11%2B-blue?logo=python&logoColor=white)](https://www.python.org/downloads/) +[![python](https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white)](https://www.python.org/downloads/) [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/CosmoStat/sp_validation/blob/develop/LICENCE.txt) [![code style: ruff](https://img.shields.io/badge/code%20style-ruff-261230?logo=ruff&logoColor=white)](https://github.com/astral-sh/ruff) [![contribute](https://img.shields.io/badge/contribute-read-lightgrey)](https://github.com/CosmoStat/sp_validation/blob/develop/CONTRIBUTING.md) diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 56de599f..22ea84cb 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -82,6 +82,6 @@ For a narrower install, ``.[test]`` adds only the test extras and ``.[docs]`` on .. note:: - ``sp_validation`` requires Python 3.11 or newer and pulls in a large scientific stack: ``treecorr``, ``pyccl``, ``healpy``, ``pymaster``, and others. + ``sp_validation`` requires Python 3.12 or newer and pulls in a large scientific stack: ``treecorr``, ``pyccl``, ``healpy``, ``pymaster``, and others. A bare development install builds these from source, which is slow and platform-sensitive. For most users the container is the more reliable path. diff --git a/papers/bmodes/Snakefile b/papers/bmodes/Snakefile index c58663d6..c9069c79 100644 --- a/papers/bmodes/Snakefile +++ b/papers/bmodes/Snakefile @@ -2,7 +2,7 @@ # compute workflow at ../../workflow/. configfile: "config/config.yaml" -configfile: "/n17data/cdaley/unions/pure_eb/code/sp_validation/cosmo_val/cat_config.yaml" +configfile: "/n17data/cdaley/unions/code/sp_validation/cosmo_val/cat_config.yaml" envvars: "PYTHONUNBUFFERED", diff --git a/papers/bmodes/config/ecut_spec.md b/papers/bmodes/config/ecut_spec.md index 49da3a92..53041659 100644 --- a/papers/bmodes/config/ecut_spec.md +++ b/papers/bmodes/config/ecut_spec.md @@ -32,7 +32,7 @@ DES-Y3 used e < 0.8 to remove stars. ### How versions flow through the pipeline -Everything is driven by `config["versions"]` in `workflow/config/config.yaml`. Adding a +Everything is driven by `config["versions"]` in `papers/bmodes/config/config.yaml`. Adding a version there (plus its `cat_config.yaml` entry) makes it flow through all existing rules: `xi`, `covariance`, `pure_eb_data_vector`, `cosebis_data_vector`, `cl_data_vector`. The 2PCF and covariance are independent and can run in parallel. @@ -43,7 +43,7 @@ Key resolution functions in `workflow/Snakefile`: - `resolve_covariance_version()` — identity function (each version gets its own covariance) - Wildcard constraint: `version=r"SP_v[\d.]+(_w_iv)?(_leak_corr)?"` — needs `_ecut\d+` -Version comparison rules in `workflow/rules/claims.smk` (lines 131, 271, 386) use +Version comparison rules in `papers/bmodes/rules/claims.smk` (lines 131, 271, 386) use `VERSIONS_LEAK_CORR` for inputs and derive version lists from config in the scripts. These should be parameterized to accept a version list via `snakemake.params`, so the same rules serve both paper and ecut comparisons. @@ -71,10 +71,10 @@ uncorrected columns can't be consistently filtered to guarantee the same rows. | What | Where | |------|-------| -| Workflow config | `workflow/config/config.yaml` (search `ecut`) | -| Catalog config | `code/sp_validation/cosmo_val/cat_config.yaml` (search `ecut07`) | +| Workflow config | `papers/bmodes/config/config.yaml` (search `ecut`) | +| Catalog config | `cosmo_val/cat_config.yaml` (search `ecut07`) | | Pipeline orchestration | `workflow/Snakefile` (wildcard constraints, version resolution functions) | -| Version comparison rules | `workflow/rules/claims.smk` lines 131, 271, 386 | +| Version comparison rules | `papers/bmodes/rules/claims.smk` lines 131, 271, 386 | | Version comparison scripts | `workflow/scripts/{pure_eb,cosebis,cl}_version_comparison.py` | | Covariance params | `workflow/rules/covariance.smk` line 4 (`get_cat_params`) | diff --git a/papers/bmodes/scripts/run_cov_sweep.sh b/papers/bmodes/scripts/run_cov_sweep.sh deleted file mode 100755 index f86f20dc..00000000 --- a/papers/bmodes/scripts/run_cov_sweep.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -# CosmoCov integration-grid covariance sweep over non-fiducial versions (Design B). -# -# Loops the non-fiducial version list (resolved via sweep_versions.py) and runs -# the same run_cosmocov_chain.sh chain the fiducial cov_integration_g recipe -# calls, once per version, on the 1000-bin integration grid (Gaussian-only, -# masked, blind A). Each version's processed matrix lands in a per-version subdir -# named canonically so cosebis_version_comparison (--cov-dir) and the pure E/B -# sweep can reconstruct it: -# -# /covariance__A_g_minsep=0.5_maxsep=300.0_nbins=1000_masked/ -# covariance__A_g_minsep=0.5_maxsep=300.0_nbins=1000_masked_processed.txt -# -# run_cosmocov_chain.sh writes covariance_processed.txt into --out; this driver -# renames it to the {base}_processed.txt the plotter/pure_eb sweep expect. -# -# Mask per version: v1.4.8 uses the star-halo footprint power spectrum, every -# other version uses the standard footprint (mirrors covariance.smk -# get_mask_cls_path). Covariance is recomputed per variant — it is NOT -# correction-invariant (masked v1.4.8 differs; uncorrected σ_e differs slightly). -# -# Usage: -# run_cov_sweep.sh --config --cat-config \ -# --planck18-json /planck18.json \ -# --mask-base --out \ -# [--blind A] [--versions "v1 v2 ..."] -set -euo pipefail - -. "$(dirname "${BASH_SOURCE[0]}")/container_env.sh" - -CONFIG=""; CATCONFIG=""; PLANCK18=""; MASKBASE=""; OUT=""; BLIND="A"; VERSIONS="" -MINSEP=0.5; MAXSEP=300.0; NBINS=1000 -while [ $# -gt 0 ]; do - case "$1" in - --config) CONFIG="$2"; shift 2;; - --cat-config) CATCONFIG="$2"; shift 2;; - --planck18-json) PLANCK18="$2"; shift 2;; - --mask-base) MASKBASE="$2"; shift 2;; - --out) OUT="$2"; shift 2;; - --blind) BLIND="$2"; shift 2;; - --versions) VERSIONS="$2"; shift 2;; - *) echo "unknown arg: $1" >&2; exit 2;; - esac -done - -mkdir -p "$OUT" - -VERSIONS=$(sweep_versions "$CONFIG") - -for ver in $VERSIONS; do - base="covariance_${ver}_${BLIND}_g_minsep=${MINSEP}_maxsep=${MAXSEP}_nbins=${NBINS}_masked" - # Version dir key: strip SP_, _leak_corr, _ecutNN (mirrors get_mask_cls_path). - vdir=$(echo "$ver" | sed -e 's/_leak_corr//' -e 's/^SP_//' -e 's/_ecut[0-9]*//') - if [ "$vdir" = "v1.4.8" ]; then - mask="$MASKBASE/mask_cls_footprint_starhalo_nside_4096_norm.txt" - else - mask="$MASKBASE/mask_cls_footprint_nside_4096_norm.txt" - fi - echo "[cov_sweep] $ver (mask: $(basename "$mask"))" - bash "$WSCRIPTS/run_cosmocov_chain.sh" \ - --version "$ver" --blind "$BLIND" \ - --min-sep "$MINSEP" --max-sep "$MAXSEP" --nbins "$NBINS" --gaussian g \ - --planck18-json "$PLANCK18" --cat-config "$CATCONFIG" \ - --mask-cls "$mask" --out "$OUT/$base" - mv "$OUT/$base/covariance_processed.txt" "$OUT/$base/${base}_processed.txt" - echo "[cov_sweep] $ver -> $OUT/$base/${base}_processed.txt" -done -echo "[cov_sweep] done -> $OUT" diff --git a/papers/bmodes/scripts/update_survey_stats.py b/papers/bmodes/scripts/update_survey_stats.py index 6713b3c0..a2dc386c 100644 --- a/papers/bmodes/scripts/update_survey_stats.py +++ b/papers/bmodes/scripts/update_survey_stats.py @@ -5,7 +5,7 @@ memory usage. Usage: - python workflow/scripts/update_survey_stats.py \ + python papers/bmodes/scripts/update_survey_stats.py \ --mask-standard --mask-starhalo """ diff --git a/papers/cosmo_val/Snakefile b/papers/cosmo_val/Snakefile index 716a3ac6..67bef0a7 100644 --- a/papers/cosmo_val/Snakefile +++ b/papers/cosmo_val/Snakefile @@ -7,7 +7,7 @@ # isolation or as the whole suite via the default `cosmo_val_all` target. configfile: "config/config.yaml" -configfile: "/n17data/cdaley/unions/pure_eb/code/sp_validation/cosmo_val/cat_config.yaml" +configfile: "/n17data/cdaley/unions/code/sp_validation/cosmo_val/cat_config.yaml" envvars: "PYTHONUNBUFFERED", diff --git a/pyproject.toml b/pyproject.toml index 5c45cfbf..4ad6fbf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,8 +159,8 @@ glass = [ # Snakemake workflow and cross-validation runners are available. workflow = [ "snakemake", - # run_2pcf_highres.py drives the MPI convergence run; the container ships - # OpenMPI under /opt/ompi so mpi4py builds against it. + # Optional MPI runners (the container ships OpenMPI at /opt/ompi, so mpi4py + # builds against it). "mpi4py", # CosmoSIS drives the cosmo_inference sampling step. Sdist-only, so this is a # source build (~2 min) against the base image's gfortran/GSL/cfitsio/OpenMPI — diff --git a/workflow/rules/twopoint.smk b/workflow/rules/twopoint.smk index 861c3b05..275afc3c 100644 --- a/workflow/rules/twopoint.smk +++ b/workflow/rules/twopoint.smk @@ -24,55 +24,6 @@ rule xi: "../scripts/run_2pcf.py" -# PARKED: xi_highres (high-resolution xi for COSEBIS integration). Not runnable -# as written -- the shell invokes run_2pcf_highres.py bare, but the script has -# required --cat-config and --out arguments. Revive it with those supplied. -# -# The MPI reasoning below is hard-won and must survive the revival: -# -# Exception to the profile-driven container model: this is multi-node MPI, one -# `apptainer exec` per rank. Snakemake's own container wrapping puts the -# *whole* shell command -- `mpiexec` included -- inside a single container -# instance, so only rank 0's node would run inside it; the other ranks, -# spawned by SLURM/PMI on their own nodes, would land bare on the host. -# `container: None` plus an explicit `mpiexec -n N apptainer exec ...` -# per-rank is therefore required. -# Snakemake's slurm-jobstep plugin deliberately does NOT prepend `srun` to a -# job carrying an `mpi` resource, which is what lets the rule's own launcher -# run on the host, outside the container. -# Because this rule builds its own apptainer call, reaching the source-cache -# copy of the script relies on our `--bind /home` rather than on Snakemake's -# automatic mount -- and on a concrete image file, since `apptainer exec` -# takes no `docker://` URI. Take that path from `resolve_image()[0]` rather -# than naming a second image path that can drift. -# -# rule xi_highres: -# container: None -# params: -# image=resolve_image()[0], -# input: -# script=workflow.source_path("../scripts/run_2pcf_highres.py"), -# output: -# txt=str(COSMO_VAL / f"{FIDUCIAL['version']}_xi_minsep={FIDUCIAL['min_sep_int']}_maxsep={FIDUCIAL['max_sep_int']}_nbins=10000_npatch=1.txt"), -# xi_plus=str(COSMO_VAL / f"xi_plus_{FIDUCIAL['version']}_minsep={FIDUCIAL['min_sep_int']}_maxsep={FIDUCIAL['max_sep_int']}_nbins=10000_npatch=1.fits"), -# xi_minus=str(COSMO_VAL / f"xi_minus_{FIDUCIAL['version']}_minsep={FIDUCIAL['min_sep_int']}_maxsep={FIDUCIAL['max_sep_int']}_nbins=10000_npatch=1.fits"), -# resources: -# tasks=30, -# cpus_per_task=12, -# nodes=6, -# mem_mb_per_cpu=2000, -# runtime=2880, -# slurm_extra="'--exclude=n17,n09,n36 --partition=pscomp'", -# mpi="/softs/openmpi/5.0.5-slurm-CentOS8/bin/mpiexec", -# shell: -# "{resources.mpi} -n {resources.tasks} " -# "apptainer exec " -# "--bind /home,/n09data,/n17data,/n23data1,/softs " -# "--env LD_LIBRARY_PATH=/softs/openmpi/5.0.5-slurm-CentOS8/lib " -# "{params.image} " -# "python {input.script} --cat-config <...> --out <...>" - - rule rho_tau_stats: output: rho_stats=str(COSMO_VAL / "rho_tau_stats/rho_stats_{version}_minsep={min_sep}_maxsep={max_sep}_nbins={nbins}_npatch={npatch}.fits"), diff --git a/workflow/scripts/run_2pcf_highres.py b/workflow/scripts/run_2pcf_highres.py deleted file mode 100644 index eb31d2c7..00000000 --- a/workflow/scripts/run_2pcf_highres.py +++ /dev/null @@ -1,394 +0,0 @@ -#!/usr/bin/env python -""" -High-resolution ξ± measurement for COSEBIS integration. - -Computes TreeCorr GGCorrelation with fine angular binning (10,000+ bins) -required for accurate COSEBIS mode integration. Uses MPI for patch-pair -distribution across nodes when available; falls back to multi-threaded -single-process otherwise. - -Reference: Asgari et al. 2017 — minimum 10,000 bins for E_7 at 0.5% accuracy. - -Usage: - # MPI (via Slurm submission script): - mpiexec --map-by ppr:1:node python run_2pcf_highres.py \ - --cat-config /path/to/cosmo_val/cat_config.yaml --out - - # Single-process fallback: - python run_2pcf_highres.py \ - --cat-config /path/to/cosmo_val/cat_config.yaml --out -""" - -import argparse -import os -import time - -import numpy as np -import treecorr -from astropy.io import fits - -try: - # In-container path: full sp_validation stack available. - from sp_validation.cosmo_val import CosmologyValidation - - _HAVE_COSMO_VAL = True -except ImportError: - # Bare-host path (host OpenMPI + host python for the 10k-bin MPI run): the - # full sp_validation stack (cs_util.plots -> healpy/healsparse) is not - # installed. This measurement only needs the shear catalog path + column - # names, which are a pure cat_config.yaml lookup — resolve them standalone. - CosmologyValidation = None - _HAVE_COSMO_VAL = False - -# --------------------------------------------------------------------------- -# MPI setup (graceful fallback) -# --------------------------------------------------------------------------- -try: - from mpi4py import MPI - - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - size = comm.Get_size() - USE_MPI = size > 1 -except ImportError: - comm = None - rank = 0 - size = 1 - USE_MPI = False - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -# Shear response (R=1 for all SP catalogs) -R = 1.0 - -# Detect threads from Slurm or fall back to OS count -NUM_THREADS = int(os.environ.get("SLURM_CPUS_PER_TASK", os.cpu_count() or 24)) - -# The catalog path, ellipticity/weight columns, TreeCorr grid, patch count and -# output directory are resolved from the CLI in main() (defaults reproduce the -# historical hardcoded values for a no-arg run). They are declared here as -# module globals so the rank-aware helpers below resolve them at call time; the -# catalog path + columns come from cat_config + version exactly as run_2pcf.py -# resolves them (via CosmologyValidation). -CAT_PATH = None -VERSION = None -E1_COL = None -E2_COL = None -W_COL = None -TMIN = None # arcmin -TMAX = None # arcmin -NBINS = None -NPATCH = None -OUTPUT_DIR = None -PATCH_FILE = None - - -def parse_args(argv=None): - """CLI mirroring run_xi_sweep's signature; defaults reproduce prior behavior.""" - ap = argparse.ArgumentParser( - description="High-resolution TreeCorr ξ± measurement for COSEBIS integration." - ) - ap.add_argument( - "--config", - default=None, - help="Path to bmodes config.yaml (accepted for signature parity with " - "run_xi_sweep; not read by this measurement).", - ) - ap.add_argument( - "--cat-config", required=True, help="Absolute path to cat_config.yaml" - ) - ap.add_argument( - "--version", - default="SP_v1.4.6.3_leak_corr", - help="Catalog version key in cat_config", - ) - ap.add_argument("--nbins", type=int, default=10000, help="Number of log bins") - ap.add_argument("--npatch", type=int, default=50, help="TreeCorr patch count") - ap.add_argument( - "--min-sep", type=float, default=0.5, help="Min separation [arcmin]" - ) - ap.add_argument( - "--max-sep", type=float, default=300.0, help="Max separation [arcmin]" - ) - ap.add_argument("--out", required=True, help="Output directory (lc {output})") - return ap.parse_args(argv) - - -def log(msg): - """Print with timestamp on rank 0 only.""" - if rank == 0: - print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) - - -def load_catalog(): - """Load shear catalog and apply mean subtraction.""" - log(f"Loading catalog: {CAT_PATH}") - hdul = fits.open(CAT_PATH, memmap=True) - data = hdul[1].data - - ra = np.array(data["ra"], dtype=np.float64) - dec = np.array(data["dec"], dtype=np.float64) - e1 = np.array(data[E1_COL], dtype=np.float64) - e2 = np.array(data[E2_COL], dtype=np.float64) - w = np.array(data[W_COL], dtype=np.float64) - hdul.close() - - log(f" {len(ra):,} galaxies loaded") - - # Additive bias: c = _w (R=1 for SP catalogs) - c1 = np.average(e1 / R, weights=w) - c2 = np.average(e2 / R, weights=w) - log(f" Additive bias: c1={c1:.6e}, c2={c2:.6e}") - - # Calibrated shear: g = (e - c) / R - g1 = (e1 - c1) / R - g2 = (e2 - c2) / R - - return ra, dec, g1, g2, w - - -def _wait_for_file(path, timeout=300, interval=1.0): - """Block until `path` is visible to this node, defeating NFS dir caching. - - On a multi-node run the rank that wrote `path` sees it immediately, but - peer nodes can carry a stale negative directory-cache entry past an MPI - Barrier. Re-listing the parent directory forces an NFS attribute refresh; - poll that until the entry appears (or raise after `timeout` seconds). - """ - parent = os.path.dirname(path) or "." - name = os.path.basename(path) - waited = 0.0 - while waited < timeout: - try: - if name in os.listdir(parent): - return - except FileNotFoundError: - pass - time.sleep(interval) - waited += interval - raise TimeoutError(f"patch-center file not visible after {timeout}s: {path}") - - -def compute_patch_centers(ra, dec): - """Compute patch centers from subsampled catalog (rank 0 only).""" - if os.path.exists(PATCH_FILE): - log(f"Using existing patch centers: {PATCH_FILE}") - return - - if rank != 0: - return - - log(f"Computing patch centers (npatch={NPATCH}) from 1% subsample...") - rng = np.random.default_rng(42) - n_sub = max(len(ra) // 100, NPATCH * 100) - idx = rng.choice(len(ra), size=n_sub, replace=False) - - cat_sub = treecorr.Catalog( - ra=ra[idx], - dec=dec[idx], - ra_units="degrees", - dec_units="degrees", - npatch=NPATCH, - ) - cat_sub.write_patch_centers(PATCH_FILE) - log(f" Wrote patch centers to {PATCH_FILE}") - del cat_sub - - -def write_xi_fits(gg, prefix, xi_data): - """Write ξ+ or ξ- to FITS matching CosmologyValidation format.""" - out_path = os.path.join( - OUTPUT_DIR, - f"{prefix}_{VERSION}_minsep={TMIN}_maxsep={TMAX}_nbins={NBINS}_npatch=1.fits", - ) - n = len(xi_data) - cols = [ - fits.Column(name="BIN1", format="K", array=np.ones(n, dtype=int)), - fits.Column(name="BIN2", format="K", array=np.ones(n, dtype=int)), - fits.Column(name="ANGBIN", format="K", array=np.arange(1, n + 1)), - fits.Column(name="VALUE", format="D", array=xi_data), - fits.Column(name="ANG", format="D", unit="arcmin", array=gg.meanr), - ] - ext_name = "XI_PLUS" if "plus" in prefix else "XI_MINUS" - hdu = fits.BinTableHDU.from_columns(cols, name=ext_name) - for key, val in { - "2PTDATA": "T", - "QUANT1": "G+R", - "QUANT2": "G+R", - "KERNEL_1": "NZ_SOURCE", - "KERNEL_2": "NZ_SOURCE", - "WINDOWS": "SAMPLE", - }.items(): - hdu.header[key] = val - hdu.writeto(out_path, overwrite=True) - log(f" Wrote {out_path}") - - -def resolve_shear_config(cat_config_path, version): - """Standalone shear-config resolver (bare-host fallback for CosmologyValidation). - - Reproduces exactly the ``cc[version]["shear"]`` fields this measurement reads - (path, e1_col, e2_col, w_col), replicating CosmologyValidation's two - transforms: (1) subdir-relative path resolution, and (2) the ``_leak_corr`` - virtual version — deep-copy the base version and swap - e1_col/e2_col -> e1_col_corrected/e2_col_corrected. See - sp_validation/cosmo_val/core.py. - """ - import copy - - import yaml - - with open(cat_config_path) as fh: - cc = yaml.load(fh, Loader=yaml.FullLoader) - - def resolve_paths(ver): - subdir = os.fspath(cc[ver]["subdir"]) - for section in cc[ver].values(): - if isinstance(section, dict) and "path" in section: - p = section["path"] - if not os.path.isabs(p): - section["path"] = os.path.join(subdir, p) - - leak_suffix = "_leak_corr" - if version in cc: - resolve_paths(version) - elif version.endswith(leak_suffix): - base = version[: -len(leak_suffix)] - if base not in cc: - raise ValueError(f"Base version '{base}' not in cat_config for '{version}'") - resolve_paths(base) - base_shear = cc[base]["shear"] - if "e1_col_corrected" not in base_shear or "e2_col_corrected" not in base_shear: - raise ValueError( - f"{base} lacks e1_col_corrected/e2_col_corrected; cannot form {version}" - ) - cc[version] = copy.deepcopy(cc[base]) - cc[version]["shear"]["e1_col"] = base_shear["e1_col_corrected"] - cc[version]["shear"]["e2_col"] = base_shear["e2_col_corrected"] - resolve_paths(version) - else: - raise ValueError(f"Version '{version}' not found in cat_config") - - return cc[version]["shear"] - - -def main(): - global CAT_PATH, VERSION, E1_COL, E2_COL, W_COL - global TMIN, TMAX, NBINS, NPATCH, OUTPUT_DIR, PATCH_FILE - - args = parse_args() - VERSION = args.version - NBINS = args.nbins - NPATCH = args.npatch - TMIN = args.min_sep - TMAX = args.max_sep - OUTPUT_DIR = args.out - - # Resolve catalog path + ellipticity/weight columns from cat_config + version - # exactly as run_2pcf.py does (applies the _leak_corr column swap and the - # subdir path resolution). In-container this uses CosmologyValidation; bare- - # host (10k-bin MPI run) it uses the standalone cat_config resolver, which is - # byte-identical for the shear-config fields this measurement reads. - if _HAVE_COSMO_VAL: - cv = CosmologyValidation( - versions=[VERSION], catalog_config=args.cat_config, output_dir=OUTPUT_DIR - ) - shear_cfg = cv.cc[VERSION]["shear"] - else: - shear_cfg = resolve_shear_config(args.cat_config, VERSION) - CAT_PATH = shear_cfg["path"] - E1_COL = shear_cfg["e1_col"] - E2_COL = shear_cfg["e2_col"] - W_COL = shear_cfg["w_col"] - - PATCH_FILE = os.path.join( - OUTPUT_DIR, - f"patch_centers_{VERSION}_{NPATCH}_{TMIN}_{TMAX}.dat", - ) - - t0 = time.time() - - log("=" * 60) - log("High-resolution ξ± measurement") - log(f" MPI: {'yes' if USE_MPI else 'no'} (ranks={size})") - log(f" Config: {NBINS:,} bins, [{TMIN}, {TMAX}] arcmin") - log(f" Patches: {NPATCH}, Threads/rank: {NUM_THREADS}") - log(f" Version: {VERSION}") - log("=" * 60) - - # All ranks load catalog (needed for TreeCorr patch assignment) - ra, dec, g1, g2, w = load_catalog() - - # Compute patch centers (rank 0 only; others wait) - compute_patch_centers(ra, dec) - if USE_MPI: - comm.Barrier() - # Cross-node visibility: rank 0 wrote PATCH_FILE on its node, but on a - # multi-node allocation the other ranks' nodes may not see it yet (NFS - # close-to-open + negative-dir caching persists past the Barrier). Poll - # with a forced directory refresh until it appears before reading it. - _wait_for_file(PATCH_FILE) - - # Create TreeCorr catalog with patch centers - log("Creating TreeCorr catalog with patches...") - cat = treecorr.Catalog( - ra=ra, - dec=dec, - g1=g1, - g2=g2, - w=w, - ra_units="degrees", - dec_units="degrees", - patch_centers=PATCH_FILE, - ) - cat.load() - cat.get_patches() - log(f" Catalog ready ({cat.nobj:,} objects, {cat.npatch} patches)") - - # Free raw arrays (TreeCorr holds its own copy) - del ra, dec, g1, g2, w - - # Compute GG correlation - log("Computing GGCorrelation...") - gg = treecorr.GGCorrelation( - min_sep=TMIN, - max_sep=TMAX, - nbins=NBINS, - sep_units="arcminutes", - verbose=2, - ) - - process_kwargs = {"num_threads": NUM_THREADS} - if USE_MPI: - process_kwargs["comm"] = comm - - gg.process(cat, **process_kwargs) - log(f" Correlation complete ({time.time() - t0:.0f}s elapsed)") - - # Write output (rank 0 only) - if rank == 0: - out_txt = os.path.join( - OUTPUT_DIR, - f"{VERSION}_xi_minsep={TMIN}_maxsep={TMAX}_nbins={NBINS}_npatch=1.txt", - ) - # Write only the main per-bin correlation. The convergence consumer - # (cosebis_binning_comparison.py) reads just the per-bin columns - # (np.loadtxt max_rows=nbins) and the 1000-bin covariance — the 10k - # jackknife cov is used nowhere. write_patch_results/write_cov=True - # serialised a 20000x20000 cov + 180 patch blocks (~10 GB) that nothing - # reads and also cost the estimate_cov compute; drop both. The patches - # still parallelise gg.process; gg.xip/gg.xim (values, FITS) are - # unaffected. - gg.write(out_txt, write_patch_results=False, write_cov=False) - log(f" Wrote {out_txt}") - - write_xi_fits(gg, "xi_plus", gg.xip) - write_xi_fits(gg, "xi_minus", gg.xim) - - elapsed = time.time() - t0 - log(f"Done! Total time: {elapsed / 3600:.1f}h ({elapsed:.0f}s)") - - -if __name__ == "__main__": - main()