Restructuring: top-level layout + workflow/papers/scratch division#197
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold Sacha's pending foundation (PR #192 head, sachaguer:develop @ c22f075) onto current develop so the restructuring builds on his foundation without racing his merge gesture (Cail's direction, 2026-06-05). .gitignore conflict resolved in favour of develop: kept the .felt tracking block, rejected sacha's broad cluster bans (*.png *.sh *.fits *.out *.err) — those get narrowed during the restructuring gitignore pass, not adopted wholesale. cosmo_val.py / cat_config.yaml auto-merged cleanly (origin's docstring-RST polish + sacha's functional changes did not collide). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cosmology.py get_cosmo read planck_defaults["mnu"] but the dict never defined the key, so every bare get_cosmo() call (no ccl_params, no mnu arg) raised KeyError: 'mnu'. Add "mnu": PLANCK18["m_nu"] (0.06 eV). Verified: test_cosmology.py 26/26 pass (was immediate KeyError before). This is the one blocker that kept Sacha's foundation from running clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/source/sp_validation.*.rst are regenerated on every docs build by sphinx-apidoc (deploy-docs.yml: `sphinx-apidoc -feTMo docs/source src/sp_validation`), matching the already-ignored fortuna.*/scripts.* stubs — they should never be committed. uv.lock: the container is the canonical runtime (CLAUDE.md), the lockfile has never been tracked, so ignore it rather than make an unowned pinned-dep commitment. One-line flip to track if we decide to pin. Establishes a clean base for the restructuring branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sacha's branch removed the cosmosis_pipeline_glass_mock_0*.ini and _v0*.ini ignore patterns, which un-ignored ~700 generated glass-mock pipeline configs in cosmo_inference/cosmosis_config/. Restore the two specific patterns (not broad bans) so the tree returns to develop's clean state. These are generated artifacts, never tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate demo, catalogue-reduction, and plotting scripts (jupytext/percent format) plus the params.py config template and des_y3 example from notebooks/ to scripts/examples/. notebooks/ is now removed entirely. - Group all demos and reduction scripts under scripts/examples/ to keep the doc-scanned top-level scripts/ limited to clean CLI tools. - Fix two paste-corruption syntax errors in demo_calibrate_minimal_cat.py so the file parses (matched unpacking/signature to calibrate_comprehensive_cat). - Apply ruff safe autofixes (whitespace, unused imports, import sorting, f-strings) to the moved files.
Background agents create transient isolated worktrees under .claude/worktrees/; they must never be tracked. https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
Repoint params.py config references (CLAUDE.md, quickstart.rst, run_validation.md, post_processing.md, prepare_patch_for_spval.sh) at scripts/examples/params.py, and the extract_information.* reference at the moved scripts/examples/extract_info.py. Register notebooks/params.py -> scripts/examples/params.py in the dangling-move-references guard.
All three parallel reorg threads integrated onto cleanup/restructuring. https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
Convert the user-facing tutorial_UNIONS_SP_v1.0 notebook (removed in the notebooks/ cleanup) into a Sphinx User Guide page rather than deleting it. Updated for the HDF5 catalogue format (>= v1.4.1) and cross-referenced to sp_validation.calibration.get_calibrated_m_c / get_calibrate_e_from_cat, which now automate the hand-rolled metacalibration steps. https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
The cosmo_val/ module was promoted from notebooks/cosmo_val/ during phase-2 with its notebooks riding along untouched. Resolve them per "reasonably reusable code -> library, the rest -> scratch scripts": - Lift three generic helpers into src/sp_validation/basic.py: chi2_and_pte, corr_from_cov, cov_from_one_covariance. - Convert the five notebooks to jupytext percent-light .py under scratch/guerrini/ (magics guarded like run_cosmo_val.py, hardcoded paths preserved verbatim). compute_pte_cell and one_covariance now import the lifted helpers and SquareRootScale from sp_validation.rho_tau. - Move the investigation namaster/ bundle (utils.py -> namaster_utils.py, exploration.ipynb -> exploration.py) and repair a latent broken import (sp_validation.utils_cosmo_val -> rho_tau) the phase-2 move missed. cosmo_val/ is now notebook-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
Conversion shipped in e3147b5; record outcome, closed status, and handoff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
Four .tldr/status + .tldrignore files (auto-generated by the TLDR tool, status 'stopped') were committed by accident during the restructuring. Remove them and add the patterns to .gitignore so they don't return. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
Capture the cosmo_inference notebook reorg (make it notebook-free like cosmo_val) as a TODO fiber, to be done carefully on Candide where the inference stack runs. Also gitignore felt's SQLite WAL/shm sidecars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QuJMSPvnpsBkr7PBZVcPQ
|
Can we cluster scripts that are used for calibration? These would be (at the moment, this might change for v2.0)
|
|
Please move to scratch:
This can be deleted:
|
There was a problem hiding this comment.
The PR looks fine to me.
Regarding the namaster_utils script being redundant with other scripts, I think I will handle it when I will upgrade cosmo_val harmonic space scripts to tomography.
One objective would be to move the namaster_utils to the source so that one can use it directly and use cosmo_val to iterate on the basic functions. I will use it to refactor the script computing the Gaussian simulations with less duplication of code. I would keep it for the next PR.
Some paper scripts from configuration space paper remain to be moved to the papers directory. The folder 2D_consistency_check can be moved there as well. It contains figures regarding the consistency between harmonic and configuration space.
I am sure that we will find bugs when we will try to run some of the scripts but, as for me, the objective of this PR to refactor the global structure of the repository has been met.
…cv_init
These per-version rules called cv_params(version_list=["{version}"]) — a Python
call evaluated at parse time, so the literal string "{version}" reached
CosmologyValidation and raised KeyError('Version string {version} not found').
xi/rho_tau worked (string-param substitution); cv_pseudo_cl worked (iterates the
real CV_VERSIONS). Now cv_init is a lazy 'lambda w: cv_init_params(config,
[w.version])' so snakemake passes the resolved wildcard. Surfaced by the first
real cosmo_val run (job 791399/791400).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er, consistency move
Martin's review:
- Cluster the calibration pipeline into scripts/calibration/: extract_info →
create_joint_comprehensive_cat → demo_apply_hsp_masks → calibrate_comprehensive_cat
(+ the shared params.py template + a README documenting the 4-step order).
- Delete dead example scripts: leakage_minimal, create_shear_mb_empty,
demo_add_bands{,_to_empty}, des_y3_cat, star_response (tests_bump already gone).
- Move to scratch/kilbinger/: demo_binned_mask, plot_binned_quantities, and
analyse_matched_stars_UNIONS_HSC (recovered from history, converted .ipynb→.py).
Sasha's review (procedural):
- Relocate the config-vs-harmonic consistency folder
cosmo_inference/notebooks/2D_cosmic_shear_consistency → papers/consistency.
Docs + guards: post_processing/run_validation/quickstart/CLAUDE.md/prepare_patch
updated to the new scripts/calibration/ paths; MOVE_MAP gains the consistency
retirement and the params.py target; gitignore the cosmo_val SLURM run logs.
Structural guards green (10/10 package-free); test_basic + test_calibration
value-drift pins green in-container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scratch/guerrini/ and the namaster_utils→source / Gaussian-sims work he reserved for his next PR in the #197 review. So future workers don't touch it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The image rebuild pulled scipy 1.18.0 (py3.12+), which changed interpolators to return arrays for scalar input. That breaks camb 1.6.x's BBN Y_He predictor inside CAMBparams.set_cosmology — `self.YHe = Y_He(...)` raises "TypeError: only 0-dimensional arrays can be converted to Python scalars". Every get_cosmo-backed test (test_cosmology, test_cosmo_val, test_glass_mock — 27 failures) funnelled through it; none are code regressions. Confirmed by isolated repro: scipy 1.18.0 + camb 1.6.6 fails the exact call, scipy 1.17.x passes. Cap until camb ships a scipy-1.18-compatible BBN predictor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scratch/guerrini/ and the namaster_utils→source / Gaussian-sims work he reserved for his next PR in the #197 review. So future workers don't touch it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the comments both of you! @martinkilbinger I have started addressing your requests in #199. Will merge this now so we can keep moving and do targeted bugfix/improvement PRs directly to develop. |
) * refactor(src): dissolve basic.py into calibration + statistics The module named "basic" was a grab-bag: the 546-line `metacal` response class (the heart of shear calibration) plus galaxy-selection masks and a handful of cosmology-independent statistics helpers — none of which "basic" described. Its symbols now live where they belong, and basic.py is deleted. - `metacal` class + `mask_gal_size`/`mask_gal_SNR` (galaxy selection) → calibration.py, joining the m/c routines that already consumed a `gal_metacal` instance. One subsystem, one module. - `jackknif_weighted_average2`, `corr_from_cov`, `chi2_and_pte`, `cov_from_one_covariance` → new statistics.py (a clean leaf: numpy/scipy only; calibration imports the jackknife from it). - Every importer repointed (papers, scripts, the two scratch/guerrini import lines — path-only, his logic untouched); dead `from sp_validation import basic` lines removed from calibration.py and cat.py; `__all__` and the architecture docs updated. - Tests split: metacal + mask pins → test_calibration.py, jackknife pin → test_statistics.py; test_basic.py removed. All moved code is byte-identical to the original (md5-verified); value-drift pins (metacal R-matrix rtol 1e-12) and the full suite pass in-container, except the pre-existing galaxy/cs_util.size old-sandbox gap. No circular imports. Verified by an adversarial multi-agent pass (byte-identity, no-stale-refs, value-pins, no-cycles). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * felt: reserved-sasha — document Sasha's hands-off zones scratch/guerrini/ and the namaster_utils→source / Gaussian-sims work he reserved for his next PR in the #197 review. So future workers don't touch it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(statistics): pin the extracted helpers; drop dead code in calibration Follow-up polish on the basic.py dissolution (#199). Tests — characterization (value-drift) coverage for the three statistics.py helpers that had none, with literals generated by running the real functions in-container and teeth on each: - corr_from_cov: unit diagonal + reconstruction from cov/outer(std,std) - chi2_and_pte: diagonal reduces to sum((d/sigma)^2) with matching scipy PTE, plus a non-diagonal case exercising the full d^T C^-1 d path - cov_from_one_covariance: gaussian(col 10) vs non-gaussian(col 9) selection and a row-major-layout check (a transpose would be caught) Calibration — strictly behavior-preserving dead-code removal: - 3 unused module imports (util, io, get_footprint — verified unreferenced) - an unused local (col_noshear) in metacal._read_data - the uncallable metacal._return method (defined without self, references self.* in its body — would NameError if ever invoked; referenced nowhere) Value pins (metacal R-matrix, m/c bias) stay green; conservatively skipped any change that would reorder float ops or restructure an estimator. Verified by an adversarial behavior-preservation review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(src): delete dead info.py, fix cat.py version import info.py had zero importers; its only content was a redundant __name__ = 'sp_validation'. cat.py imported __version__ and __name__ from the package root but used only __version__ (line 607); the software name at line 606 is already hardcoded. Repoint to the canonical home: from sp_validation.version import __version__. Register the retired import path in the dangling-move guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(src): make package __all__ honest The old __all__ listed modules nobody imports through the package (io, plot_style, cosmo_val) and omitted the two genuinely public diagnostic modules rho_tau and b_modes. Replace it with the real public surface, alphabetised, and drop the stale commented-out explicit-import block. Nothing does `from sp_validation import *`, so this is purely a documentation fix. (util and run_joint_cat are renamed in the Tier-2 commits.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cosmo_val): hoist b_modes imports to module top level Five methods each imported from .b_modes inside their bodies. b_modes is import-time side-effect-light (it pulls only .cosmology) and has no back-edge to cosmo_val, so the locals were defensive, not necessary. Consolidate the union of imported names into one top-level block next to the existing cosmology/rho_tau imports and drop the five inline imports. test_cosmo_val: 11 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(run_joint_cat): drop shadowed confusion_matrix def Two module-level defs named confusion_matrix existed; the first (mask, confidence_level=0.9) was a near-duplicate of correlation_matrix and was unconditionally shadowed by the second (prediction, observation) ~40 lines later. Every caller — in scripts/calibration and scripts/examples — uses the (prediction, observation) signature. Remove the dead first def. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: remove dead cluster/convergence-map helpers Five functions had zero callers anywhere in the repo (src, scripts, papers, scratch, notebooks), including across the star-imports of plots: cosmology.py: get_clusters, stack_mm3, gamma_T_tc, xi_gal_gal_tc plots.py: plot_map_stacked Removing the cosmology block also orphaned the imports that existed solely for it (treecorr, fits, canfar, radec2xy, cKDTree, tqdm, get_footprint); drop those too. test_cosmology + test_plots: 29 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(src): rename util -> format; drop dead transform_nan import util.py held only millify / print_millified — number formatting, not a grab-bag. Rename it to format.py and sweep all importers: internal: cat.py, run_joint_cat.py (util.millify -> format.millify) scripts: apply_alpha.py, examples/demo_calibrate_minimal_cat.py, calibration/extract_info.py (star-import x2) papers: catalog/hist_mag.py Register the rename in the dangling-move guard; update package __all__. B1: scripts/plot_leakage.py imported transform_nan from the old util module — a symbol removed from the library long ago and never used in the script. Drop the dead import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(src): move SquareRootScale to plots, re-export from rho_tau SquareRootScale is a matplotlib ScaleBase subclass — plotting infrastructure, not rho/tau logic. Move the class and its register_scale call into plots.py (which now carries the matplotlib.scale/ticker/transforms imports it needs). rho_tau.py keeps a compat re-export so existing `from sp_validation.rho_tau import SquareRootScale` callers — in cosmo_inference, scratch/guerrini, papers/harmonic — still resolve. workflow/scripts/plotting_utils.py holds a near-duplicate that diverges (ScalarFormatter(useMathText=True); inverted transform method named transform_non_affine not transform), so it is left in place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(papers/harmonic): repoint SquareRootScale off dead utils_cosmo_val Six harmonic-space scripts imported SquareRootScale from sp_validation.utils_cosmo_val, a module that no longer exists. Repoint to sp_validation.rho_tau (the compat re-export), matching the sibling 2026_03_17 script. get_params_rho_tau was already correctly imported from rho_tau. No other utils_cosmo_val imports remain (the two scratch/guerrini mentions are prose noting the module's removal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(src): rename run_joint_cat -> catalog_builders The module holds the catalogue-builder runner classes (JointCat, ApplyHspMasks, CalibrateCat) and their run_* entry-point functions; catalog_builders names that role. Sweep all importers (the `as sp_joint` alias is preserved, only the module name changes): scripts/apply_hsp_masks.py scripts/examples/{demo_check_footprint, create_binned_mask_comprehensive, demo_comprehensive_to_minimal_cat, demo_create_footprint_mask, demo_calibrate_minimal_cat}.py scripts/calibration/{create_joint_comprehensive_cat (direct symbol), demo_apply_hsp_masks, calibrate_comprehensive_cat}.py scratch/kilbinger/demo_binned_mask.py papers/catalog/hist_mag.py Also update the two prose references in docs and update __all__ and the dangling-move guard. The OPTIONAL masks.py extraction (Mask + mask-algebra fns) is deferred: those symbols are reached externally through the sp_joint.* module alias (Mask, get_masks_from_config, print_mask_stats across 4 scripts + papers/catalog), so splitting them out would require either re-exports or sweeping the public call surface — beyond a behavior-preserving move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extract masks.py from catalog_builders.py Move the healsparse-backed spatial-masking cluster out of catalog_builders.py into a dedicated masks.py: the Mask class plus get_masks_from_config, print_mask_stats, correlation_matrix, and confusion_matrix. Bodies are byte-identical; the move carries the numexpr/scipy.stats imports those helpers need (now removed from catalog_builders, which no longer references them). catalog_builders.py re-exports the five symbols from sp_validation.masks so external code using `from sp_validation import catalog_builders as sp_joint` keeps resolving sp_joint.Mask, sp_joint.get_masks_from_config, etc. The *Cat runner classes (ApplyHspMasks, ReadCat, run_* entry points) stay; ApplyHspMasks uses healsparse directly, not the Mask class. masks added to __init__.__all__. No MOVE_MAP entry: this is an extraction-in-place (catalog_builders survives), not a retired path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Rename cat.py -> catalog.py: catalogue data layer The catalogue module pair now reads by role: catalog.py is the data layer (read/write/column-access/matching free functions), catalog_builders.py is the construction pipeline (runner classes built on it). Module docstrings state this hierarchy explicitly. Behaviour-preserving. Every importer of the local sp_validation.cat module is swept to sp_validation.catalog, each preserving its local binding (bare `import cat` forms gain `as cat` so function bodies are unchanged). The cs_util `cat` import is a different module and is left untouched throughout. The dangling-reference guard registers the retired flat-import form `sp_validation.cat import` rather than the bare `sp_validation.cat` token, which would false-positive on the live `sp_validation.catalog` / `sp_validation.catalog_builders` modules (same prefix trap as glass_mock). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Deleted obsolete script cosmo_val/match_LF_SP.py * Deleted old scripts/create_joint_shape_cat.py --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Martin Kilbinger <martin.kilbinger@cea.fr>
Restructuring: top-level layout + workflow / papers / scratch split
Reorganizes
sp_validationaround one principle — the things you run live at the top — with a clean split between analysis, paper figures, and personal scratch, plus a modular Snakemake workflow built for more than one person. Base branch:develop(untouched).Layout
Previously
cosmo_valwas buried insidenotebooks/whilecosmo_inference/sat at the top, so you had to hunt for where each piece lived. Now the things you actually run sit side by side, sharingsrc/underneath.Division of labor
The boundary is the inputs to a paper figure: everything up to that point is analysis, the figure itself is presentation.
workflow/— all analysis. Generic, reusable, modular. Produces analysis products and diagnostic plots intoresults/. The bulk of the work lives here.papers/{name}/— final-figure assembly only. Figure PDFs, colour, layout. Tied to one paper; may never touch Snakemake.scratch/{person}/— personal, ad hoc, tracked. One-off experiments and custom workflows; tracked because seeing each other's scratch is useful.Modular workflow
Nothing here is computed once — the catalog changed ~20× in the first release, and every paper varies the data vector, covariance, and inference. So the workflow is parameterized: the rules are shared, the config changes per run. Snakemake's
moduledirective imports the rules under your own config and an outputprefix, with per-rule override:Each run namespaces under
results/{run}/; a--dry-runon every composition guards against silent breakage as the structure grows.Notebooks
The analysis tree is now notebook-free — the top-level
notebooks/directory is gone, and every notebook was moved to a proper home, converted, or deleted:src/sp_validation/; runnable workflows inscripts/examples/(e.g.extract_info.py,calibrate_comprehensive_cat.py,leakage_minimal.py).tutorial_UNIONS_SP_v1.0is now the live Sphinx page "Using the weak-lensing catalogues".papers/. The harmonic plot set and the catalogcheck_gaiaplot moved underpapers/{harmonic,catalog}/(kept as notebooks — final-figure assembly).cosmo_val/resolved. Generic helpers lifted intosrc/sp_validation/basic.py; the five working notebooks converted to jupytext percent-light scripts underscratch/guerrini/.cosmo_val/now holds only code + config + README.glass_mock/validate_glass_mock, anddefunct/(quarantined since 2024) — all recoverable fromdevelophistory.nbstripoutstrips notebook outputs on commit, plus a large-file pre-commit hook (pre-commit install; seeCONTRIBUTING).Full per-notebook ledger (every
.ipynbmoved or deleted vsdevelop)Moved →
papers/harmonic/(fromcosmo_inference/notebooks/2D_harmonic_space_cosmic_shear_plots/, preserved as notebooks):2025_09_26_plot_contours+ its variants (_NL_modelling,_blind,_cl_vs_xi,_covariance,_glass_mock,_iNKA_vs_OneCov,_leakage,_scale_cut,_small_vs_large_scales,_weak_lensing),2025_10_28_plot_whisker,S8_whiskerMoved →
papers/catalog/(fromnotebooks/cosmo_val/catalog_paper_plot/):check_gaiaPromoted → docs (from
notebooks/analyse_shear_cat/):tutorial_UNIONS_SP_v1.0→docs/source/using_the_catalogues.mdConverted → scripts in
scratch/guerrini/(fromnotebooks/cosmo_val/; reusable helperschi2_and_pte,corr_from_cov,cov_from_one_covariancelifted tosrc/sp_validation/basic.py):compute_pte_cell.py,one_covariance.py,plot_comparison.py,get_prior_leakage.py,exploration.py(+ itsnamaster_utils.pyhelper)Deleted (recoverable from
develophistory):notebooks/:main_set_up,metacal_global,metacal_local,psf_leakage,maps,maps_local,match_stats,correlation,cosmology,write_cat,frac_error_local_calib,analyse_matched_stars_UNIONS_HSC,analyse_shear_cat/m2_SP_LF_alphaglass_mock/:validate_glass_mockdefunct/:TD_WL_cycle2_2021,validation_local_calglass_mock
The generation core folded into
src/sp_validation/glass_mock.py; the runner scripts moved toscripts/glass_mock/. The top-levelglass_mock/directory is gone.Still open
Intentionally left for follow-up passes:
cosmo_inference/notebooks still need the same notebook-free treatment ascosmo_val/— the 2D cosmic-shear paper plots belong inpapers/(parallel topapers/harmonic/), the working notebooks inscratch//scripts. Deferred to a careful pass on Candide where the inference stack runs, so each migration can be executed and verified rather than transformed blind.scratch/promotion candidates — some scratch material may still deserve a home insrc/orworkflow/, most notablyscratch/guerrini/namaster_utils.py, a NaMaster covariance toolkit that overlapscosmo_val/harmonic_covariance_gaussian_sims.py. Consolidating the two (with tests) is a covariance-API decision best left to Sacha.Safety net
A back-pressure guard suite holds structural invariants as files move — imports + standalone-
scripts/resolution,snakemake -ndry-runs, config-path existence, symlink integrity, and a dangling-reference / move-map guard. Sacha'sdevelopfoundation (paper plots, harmonic configs, library changes) is folded in, with thecosmology.pymnuKeyErrorfixed.— Claude on behalf of Cail