Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
150 commits
Select commit Hold shift + click to select a range
681f156
feat(orchestration): the keep list, and the script that acts on it
Sep 1, 2026
de243a8
feat(orchestration): exp_persist, between exp_psf and clean_exposure
Sep 1, 2026
4cf639f
docs(orchestration): exp_persist in the rule list, and why the report…
Sep 1, 2026
1fa6455
feat(orchestration): exp_persist packs one tar per exposure, not loos…
cailmdaley Sep 3, 2026
a14a2f3
fix(orchestration): persist_exp never leaves a .tmp behind on failure
cailmdaley Sep 3, 2026
d91da36
docs(orchestration): drop references to the removed star-catalogue rules
cailmdaley Sep 9, 2026
626aaba
feat(orchestration): the two campaign-level merges
cailmdaley Sep 9, 2026
fc9aa3e
fix(orchestration): seven defects in the campaign-level merges
cailmdaley Sep 9, 2026
d774cc3
fix(create_final_cat): make the merged-catalogue writer reproducible
cailmdaley Sep 9, 2026
98532ce
perf(orchestration): size the two merges from the data, not from a guess
cailmdaley Sep 9, 2026
d0ecfdd
feat(orchestration): persist_exp keeps NAMED products, not globs
cailmdaley Sep 9, 2026
3b8c7d5
perf(merge_starcat): accumulate arrays, not python lists of floats
cailmdaley Sep 9, 2026
0ad6403
feat(orchestration): the star catalogue's inputs are not a user choice
cailmdaley Sep 9, 2026
90dfb00
fix(cfis): two stale columns in final_cat.param, and no mask column a…
cailmdaley Sep 9, 2026
a434c9a
perf(merge_starcat): two passes, so nothing is held twice
cailmdaley Sep 9, 2026
3c29825
feat(orchestration): final_cat_merge reconciles instead of rebuilding
cailmdaley Sep 9, 2026
78d2ce9
fix(orchestration): eight defects found reviewing the merge work
cailmdaley Sep 10, 2026
0fb514b
feat(orchestration): the star catalogue becomes hdf5, reconciled like…
cailmdaley Sep 10, 2026
4ed9fb0
fix(orchestration): nine findings from the third review
cailmdaley Sep 10, 2026
9fcf3f1
feat(cfis): wire the UNIONS external masks into the workflow
cailmdaley Sep 9, 2026
e86d8c8
test(unit): property-based state machines for reconcile and persist_exp
cailmdaley Sep 10, 2026
f5c76a4
feat(workflow): emit the instrument flags as a healsparse defect map …
cailmdaley Sep 10, 2026
6783f4d
docs(workflow): the defect map in the README, and how to join it to t…
cailmdaley Sep 10, 2026
958b562
fix(defect-map): a short split is an error, a bad chip is not an OOM
cailmdaley Sep 10, 2026
a9daa4f
fix(defect-map): a reclaimed exposure is a leaf, and the merge asks f…
cailmdaley Sep 10, 2026
1f6e832
docs(defect-map): the measurements, and three per exposure rather tha…
cailmdaley Sep 10, 2026
db4f426
fix(defect-map): reclamation never waits on a rasterization it cannot…
cailmdaley Sep 10, 2026
f760687
perf(defect-map): the merge allocates its accumulator once
cailmdaley Sep 10, 2026
1a343e6
fix(persist-exp): the PSF run dir is run_sp_exp_SxSePsf
cailmdaley Sep 10, 2026
5b88f17
workflow: image simulations as an input mode of the real-data workflow
Sep 11, 2026
4e17fee
workflow: image-sims merge column list, and the merge reads the workf…
Sep 11, 2026
716b160
workflow: campaign-unique node-local tile store for image sims; candi…
Sep 11, 2026
fdc9db4
workflow: port the MCCD exposure chain to the workflow grammar
Sep 11, 2026
836f91d
workflow: exp_psf reserves 2 cores, not 8, for the MCCD chain
Sep 12, 2026
ad80f23
mccd_interp: test for N_EPOCH among the column names
Sep 12, 2026
2082dcf
workflow: MCCD validated through the full chain; drop the warn-only f…
Sep 12, 2026
9085cd0
workflow: tile_store_root run-config key moves the tile store's bind,…
Sep 12, 2026
7637b39
completeness.py: atomic write for stage log/manifest JSON
Sep 15, 2026
7aababf
workflow/config.yaml: universal template, no cluster-specific paths
Sep 15, 2026
d64f88e
workflow: retrieve mode (symlink|vos) is a run-config key, not fixed …
Sep 15, 2026
c7bcfdd
config.yaml: restore real nibi defaults, keep the placeholder check a…
Sep 15, 2026
5be4c43
Snakefile: check machine: against SP_PROFILE at parse time
Sep 15, 2026
29442d3
config.yaml: machines: table drives per-machine (and per-input_type) …
Sep 15, 2026
7293e3b
workflow: one run-config resolver for Snakefile, bin/sp and container.py
Sep 15, 2026
084e305
run_config: `run:` name, expanded as $run in run-config paths
Sep 15, 2026
78cc0ca
added user run config template
Sep 17, 2026
d3b088a
bin/sp: -c/--config-file for the run config, instead of SP_RUN_CONFIG
Sep 17, 2026
746ca9c
bin/sp: document -c, the two-file merge and the environment in the he…
Sep 17, 2026
4e0bedc
improved commeents
Sep 17, 2026
3e269c4
final_cat_merge, star_cat_merge: record code provenance in the HDF5 a…
cailmdaley Sep 17, 2026
fbfdf3b
workflow smoothed; running until hdf5 file
Sep 23, 2026
0b5fae7
bin/sp: don't swallow -c/--config meant for the delegated command
cailmdaley Sep 25, 2026
d22c4a0
workflow/README: add products_dir to the image-sims run-config example
cailmdaley Sep 25, 2026
5ded2ca
run_template.yaml: make it resolve
cailmdaley Sep 25, 2026
dae9e1b
docs(astra): record the pipeline's scientific decisions in astra.yaml
cailmdaley Aug 31, 2026
e901cc9
test(astra): validate decision anchors and universe pins
cailmdaley Sep 26, 2026
8e40991
Merge remote-tracking branch 'origin/develop' into feat/persist-exp-p…
cailmdaley Sep 26, 2026
d84822f
merge: feat/workflow-image-sims (#894 @5ded2ca8) + develop into feat/…
cailmdaley Sep 26, 2026
4b1e868
workflow: final_cat_merge is the one merger, for data and image sims
cailmdaley Sep 26, 2026
d7ce47f
workflow: the campaign name is `run:`, and `run:` is required
cailmdaley Sep 26, 2026
3386347
workflow: no PSF persistence under psf_model: fake
cailmdaley Sep 26, 2026
89f00b5
test(grammar): check the image-sims final_cat.param too
cailmdaley Sep 26, 2026
1685d65
Merge remote-tracking branch 'origin/docs/astra-decision-record' into…
cailmdaley Sep 26, 2026
45a082f
feat(cfis): wire the UNIONS external masks into the workflow
cailmdaley Sep 9, 2026
4454d07
fix(file_io): recognize numpy bool arrays when picking the FITS colum…
cailmdaley Sep 10, 2026
09edd5e
docs(astra): record the pipeline's scientific decisions in astra.yaml
cailmdaley Aug 31, 2026
206b48d
test(astra): validate decision anchors and universe pins
cailmdaley Sep 26, 2026
9f9ec5e
test(astra): resolve Snakemake rule anchors; JSON report mode
cailmdaley Sep 26, 2026
5368d9d
docs(astra): rewrite the decision record against develop
cailmdaley Sep 26, 2026
a6eff5e
docs(claude): point the scientific-decisions section at the anchor test
cailmdaley Sep 26, 2026
f77959d
merge: feat/wire-external-masks-v2 (#886 on #879/#894) into feat/inst…
cailmdaley Sep 26, 2026
62a2699
feat(make_cat): fixed per-epoch slot count via N_EPOCH_SLOTS
cailmdaley Sep 26, 2026
512e7bf
feat(workflow): save fixed-slot per-epoch PSF data in tile make_cat
cailmdaley Sep 26, 2026
8d38292
workflow: no defect map for image simulations (MAPS_DEFECTS)
cailmdaley Sep 26, 2026
b2a4d19
fix(workflow): five review findings on the #894 merge
cailmdaley Sep 26, 2026
168d7fc
feat(cfis): wire the UNIONS external masks into the workflow
cailmdaley Sep 9, 2026
aee2ee9
fix(file_io): recognize numpy bool arrays when picking the FITS colum…
cailmdaley Sep 10, 2026
3bf2e12
Merge branch 'feat/wire-external-masks-v2' into feat/instrument-defec…
cailmdaley Sep 26, 2026
4958d66
workflow: scientific contracts for the campaign products
cailmdaley Sep 26, 2026
5fcc99e
test(workflow): the campaign name has one source, and products one root
cailmdaley Sep 26, 2026
e768908
test(final_cat_merge): columns, per-epoch slots, never-fit rows, miss…
cailmdaley Sep 26, 2026
e3f6f4a
docs(astra): correct seven rationale claims against the code
cailmdaley Sep 26, 2026
41be84a
docs(astra): the instrument flags become a conservative healsparse de…
cailmdaley Sep 26, 2026
27b59f7
Merge remote-tracking branch 'origin/docs/astra-decision-record' into…
cailmdaley Sep 26, 2026
eb05d3a
Merge remote-tracking branch 'origin/feat/make-cat-fixed-epoch-slots'…
cailmdaley Sep 26, 2026
d214966
Merge remote-tracking branch 'origin/docs/astra-decision-record' into…
cailmdaley Sep 26, 2026
3358105
config(cfis): final_cat.param carries the 12 per-epoch slots (EXP_ID,…
cailmdaley Sep 26, 2026
bac6eea
docs(astra): two lints this stack resolves — IMAFLAGS_ISO is not merg…
cailmdaley Sep 26, 2026
bc09a5a
test(defect-map): the fragment contains every flagged pixel
cailmdaley Sep 26, 2026
07038fd
feat(cfis): wire the UNIONS external masks into the workflow
cailmdaley Sep 9, 2026
c1fbe7c
fix(file_io): recognize numpy bool arrays when picking the FITS colum…
cailmdaley Sep 10, 2026
f84065d
docs(astra): the shear catalogue's default MASK_n* cut is the six r-m…
cailmdaley Sep 26, 2026
03fd684
test(masks): final_cat.param's MASK_n* names match MASK_EXT_PATHS; th…
cailmdaley Sep 26, 2026
d2b4de8
Merge feat/wire-external-masks-v2 (final) into feat/instrument-defect…
cailmdaley Sep 26, 2026
df20cf4
config(cfis): final_cat.param keeps NUMBER
cailmdaley Sep 26, 2026
05e7381
fix(workflow): the sims overlay resolves gauss_3.0_7x7.conv; `sp -- ……
cailmdaley Sep 26, 2026
45a9f1b
fix(workflow): refuse psf_model=mccd at parse time
cailmdaley Sep 26, 2026
16df7ae
fix(workflow): refuse reclamation when products_dir is run_dir
cailmdaley Sep 26, 2026
96abec8
fix(workflow): run_config expands to a fixed point and reports any le…
cailmdaley Sep 26, 2026
e3d7d4f
fix(defect-map): a changed fragment changes its manifest and the merg…
cailmdaley Sep 26, 2026
072b705
fix(persist_exp): the manifest records each member's sha256
cailmdaley Sep 26, 2026
b9882a2
fix(defect-map): a resolution change rebuilds instead of passing as a…
cailmdaley Sep 26, 2026
f524957
fix(defect-map): bind map and sidecar with a generation id
cailmdaley Sep 26, 2026
d179f2b
fix(hdf5_reconcile): one type per column across a campaign
cailmdaley Sep 26, 2026
187968e
test(hdf5_reconcile): isolate schema invalidation from source stamps
cailmdaley Sep 26, 2026
53d4b09
test(defect-map): drive the reconcile tests through main()
cailmdaley Sep 26, 2026
70d7a20
fix(build_index): readiness is the tiles row, not the retained edges
cailmdaley Sep 26, 2026
5a61aaf
fix(hdf5_reconcile): delete an abandoned tmp before the free-space check
cailmdaley Sep 26, 2026
47b2ec5
fix(hdf5_reconcile): each provenance record replaces the last
cailmdaley Sep 26, 2026
b15d442
fix(workflow): a reclaimed exposure's clean waits on its tar, not the…
cailmdaley Sep 26, 2026
5b56a68
test(conftest): the candide hostname test matches the whole bare name
cailmdaley Sep 26, 2026
33391a4
test(workflow): add an isolated Snakemake DAG driver
cailmdaley Sep 26, 2026
7f5ecf2
test(workflow): enforce campaign DAG scope and product custody
cailmdaley Sep 26, 2026
0e279d1
test(workflow): pin prologues and shells at campaign boundaries
cailmdaley Sep 26, 2026
a846e08
test(workflow): the DAG harness follows the parse-time guards
cailmdaley Sep 26, 2026
2c12bf5
feat(cfis): wire the UNIONS external masks into the workflow
cailmdaley Sep 9, 2026
3453e8f
fix(file_io): recognize numpy bool arrays when picking the FITS colum…
cailmdaley Sep 10, 2026
54c569a
docs(astra): the shear catalogue's default MASK_n* cut is the six r-m…
cailmdaley Sep 26, 2026
94e2dc1
test(masks): final_cat.param's MASK_n* names match MASK_EXT_PATHS; th…
cailmdaley Sep 26, 2026
0e4b4f5
Merge feat/wire-external-masks-v2 (on the reviewed #879) into feat/in…
cailmdaley Sep 26, 2026
eec9b03
test(workflow): the DAG harness knows the defect rules; the reconcile…
cailmdaley Sep 26, 2026
dc11dab
test(workflow): update the params pin — SP_INPUT_MASKS joins every da…
cailmdaley Sep 26, 2026
5f7332d
Merge branch 'feat/wire-external-masks-v2' into feat/instrument-defec…
cailmdaley Sep 26, 2026
a1cb926
Merge origin/develop (#879 squash, #875, #854, #905, #911, #894) into…
cailmdaley Sep 28, 2026
2d8f925
Merge feat/wire-external-masks (develop merged in) into feat/instrume…
cailmdaley Sep 28, 2026
be528c8
Write the defect map under <products_dir>/defect_map/
cailmdaley Sep 28, 2026
2b59d35
Coverage geometry becomes a library; retire the v1.x header-scrape chain
cailmdaley Sep 28, 2026
47b7e45
feat(workflow): the exposure-count map, beside the defect map
cailmdaley Sep 28, 2026
c29fd12
feat(workflow): the exposure-count map is on for every fitted-PSF run
cailmdaley Sep 29, 2026
e8ce5fe
fix(workflow): no exposure-count map job without a footprint record
cailmdaley Sep 29, 2026
60d9d67
workflow: refuse retired coverage:/defect_map: config keys at parse time
cailmdaley Sep 29, 2026
2e12ccd
tests: key the retired-key ordering check on either defaults call
cailmdaley Sep 29, 2026
0a65f63
Clarify HSC and Pan-STARRS mask bit labels
cailmdaley Sep 29, 2026
6fe0842
Merge develop into external-mask branch
cailmdaley Sep 30, 2026
d43c70d
State current star halo bit labels from mask producer schema
cailmdaley Sep 30, 2026
af641f8
Describe halo bit identities and guard per-map catalogue columns
cailmdaley Sep 30, 2026
f3f87db
Clarify flag values and current mask-column comments
cailmdaley Sep 30, 2026
a2e8068
Name the mask columns MASK_<flag value>_<name>
cailmdaley Oct 1, 2026
0a44b68
Merge feat/wire-external-masks (MASK_<flag value>_<name> columns, dev…
cailmdaley Oct 1, 2026
b617e39
config(cfis): the defect map's label carries no flag value
cailmdaley Oct 1, 2026
9423554
config(cfis): the defect-map recipe follows the multi-line MASK_EXT_P…
cailmdaley Oct 1, 2026
c2b5a7d
Merge develop into feat/wire-external-masks
cailmdaley Oct 5, 2026
6d2e9fc
Merge feat/wire-external-masks (with develop) into feat/instrument-de…
cailmdaley Oct 5, 2026
c28938b
Drop star halos from the default external-mask cut
cailmdaley Oct 5, 2026
d55be57
Merge feat/wire-external-masks into feat/instrument-defect-map
cailmdaley Oct 5, 2026
baa6430
Drop the mask_default_cut decision: ShapePipe makes no mask choice
cailmdaley Oct 5, 2026
9de93f7
refactor(exposure-maps): one fragment per exposure, summed into nexp …
cailmdaley Oct 5, 2026
9b9b2a2
Merge feat/wire-external-masks into feat/instrument-defect-map
cailmdaley Oct 5, 2026
8f351f0
docs(workflow): nflagged is a diagnostic, nexp is what randoms match
cailmdaley Oct 5, 2026
ac6523b
Merge origin/develop (#886 squash, #940) into feat/instrument-defect-map
cailmdaley Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions astra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,61 @@ analyses:
catalogue's MASK_<flag value>_<name> columns.
inputs: [exposure_flags, sky_masks]
decisions: [pixel_mask_source, psf_star_mask_veto, sky_mask_application]
- id: exposure_maps
type: data
format: hsp
description: >-
nexp_<run>.hsp and nflagged_<run>.hsp at the mask ladder's
resolution: per sky pixel, the number of exposures whose CCD with a
valid PSF model covers it, and the flagged CCD pixels of those
exposures that fall in it. Products; nothing in the workflow reads
them.
inputs: [exposure_flags]
decisions: [nexp_map_valid_psf_ccds, defect_map_from_flags]
decisions:
nexp_map_valid_psf_ccds:
label: The exposure count covers only CCDs with a valid PSF model
rationale: >-
exp_maps takes the CCDs whose validation_psf catalogue exp_persist
packed (psfex_interp writes it only when the fit succeeds) and marks
the sky pixels whose centres lie inside each CCD's corner polygon.
The CCDs of one exposure do not overlap, so summing fragments counts
exposures that can contribute a PSF-corrected epoch.
default: valid_psf_ccds
options:
valid_psf_ccds:
label: Count exposures whose CCD has a valid PSF model
all_ccds:
label: Count every exposure whose CCD covers the pixel
excluded: true
excluded_reason: >-
Counts epochs that cannot contribute a shape, overstating depth
exactly where the PSF fit failed.
defect_map_from_flags:
label: Instrument flags counted per sky pixel, inside the imaging area
rationale: >-
The per-CCD flag images otherwise never leave the pixel domain, so a
footprint built from CCD corners would include defective pixels.
exp_maps bins the centre of every nonzero-flag pixel inside DATASEC
into the sky pixel it falls in, and nflagged sums the counts over
exposures (~74 MegaCam pixels per sky pixel). The overscan border
(flag 3, 3.6% of a split) lies outside DATASEC and outside the
coverage polygon. On exposures 2603236/2603237 the remaining flags
are 0.28% of the imaging pixels; any-touch rasterization of them
masks 2.2% of the sky, because single-pixel columns and cosmics
widen to 1.6", so the map keeps the count and leaves the threshold
to the consumer (nflagged > 0 is the any-touch mask).
default: count_flagged_pixels
options:
count_flagged_pixels:
label: Count flagged CCD pixels per sky pixel, summed over exposures
any_touch_boolean:
label: Boolean mask of every sky pixel a flagged pixel touches
excluded: true
excluded_reason: >-
Widens one-pixel defects to the 1.6" sky pixel and loses how
much of each sky pixel is flagged; recoverable from the count as
nflagged > 0.
pixel_mask_source:
label: >-
Of the external masks, only the instrument flag image reaches pixels
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_campaign_lineage.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"prod_exp_dir": (("2605805",), False),
"prod_exp_manifest": (("2605805", "exp_persist"), False),
"prod_exp_tar": (("2605805",), False),
"prod_exp_maps": (("2605805",), False),
"nexp_map": ((), True),
"nflagged_map": ((), True),
}
PRODUCT_TEMPLATES = ("PROD_TILE_DIR", "PROD_EXP_DIR")

Expand Down
197 changes: 197 additions & 0 deletions tests/unit/test_exp_maps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""The per-exposure fragment and the campaign sum (``exp_maps.py``,
``merge_exposure_maps.py``).

The fixture is a synthetic store of two CCDs on a rotated TAN WCS at MegaCam's
0.187"/pixel, so a sky pixel covers ~74 CCD pixels as on the sky. Its flag
image carries a one-pixel bad column, a saturated blob, isolated pixels on the
CCD edges, a lattice of hot pixels, and a flagged overscan border outside
DATASEC, which must count for nothing.

Needs healsparse, hpgeom and astropy, so it runs inside the container and
skips outside.
"""

import importlib.util
import json
import sys
from pathlib import Path

import numpy as np
import pytest

pytestmark = [pytest.mark.unions,
pytest.mark.decision("masking.defect_map_from_flags")]

SCRIPTS = Path(__file__).resolve().parents[2] / "workflow" / "scripts"
EXP = "2079612"
NY, NX = 320, 240 # DATASEC
PAD_X, PAD_Y = 8, 6 # overscan columns either side, rows on top
SCALE_DEG = 0.187 / 3600.0
ROTATION_DEG = 23.0


def _load(name):
sys.path.insert(0, str(SCRIPTS))
try:
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
finally:
sys.path.remove(str(SCRIPTS))


@pytest.fixture(scope="module")
def maps():
for dep in ("healsparse", "hpgeom", "astropy"):
pytest.importorskip(dep)
return _load("exp_maps")


def _wcs(crval):
from astropy.wcs import WCS

theta = np.deg2rad(ROTATION_DEG)
wcs = WCS(naxis=2)
wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"]
wcs.wcs.crval = list(crval)
wcs.wcs.crpix = [PAD_X + NX / 2 + 0.5, NY / 2 + 0.5]
wcs.wcs.cd = SCALE_DEG * np.array([[-np.cos(theta), np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
return wcs


def _flags():
"""The DATASEC flags; ``_split`` adds the overscan border."""
flags = np.zeros((NY, NX), dtype=np.int16)
flags[:, 101] = 1 # bad column
yy, xx = np.mgrid[:NY, :NX]
flags[(yy - 210) ** 2 + (xx - 60) ** 2 <= 7 ** 2] = 2 # saturated blob
for row, col in [(0, 0), (NY - 1, NX - 1), (40, NX - 1), (77, 33)]:
flags[row, col] = 8 # isolated pixels
flags[5::11, 3::11] = 8 # sparse hot pixels
return flags


def _split(flags):
"""The full split: DATASEC flags inside a border flagged 3."""
full = np.full((NY + PAD_Y, NX + 2 * PAD_X), 3, dtype=np.int16)
full[:NY, PAD_X:PAD_X + NX] = flags
return full


def _store(tmp_path, ccds, crvals, valid):
"""A split dir with image (header only) and flag splits for ``ccds``, and
an exp_persist manifest naming the ``valid`` CCDs' validation_psf files."""
from astropy.io import fits

split = tmp_path / "store" / "output/run_sp_exp_Sp/split_exp_runner/output"
split.mkdir(parents=True)
for ccd, crval in zip(ccds, crvals):
header = _wcs(crval).to_header()
header["DATASEC"] = f"[{PAD_X + 1}:{PAD_X + NX},1:{NY}]"
fits.PrimaryHDU(header=header).writeto(split / f"image-{EXP}-{ccd}.fits")
fits.PrimaryHDU(data=_split(_flags())).writeto(
split / f"flag-{EXP}-{ccd}.fits")
manifest = tmp_path / "exp_persist.json"
manifest.write_text(json.dumps({"files": [
{"name": f"validation_psf-{EXP}-{c}.fits"} for c in valid]
+ [{"name": f"psf-{EXP}-0.psf"}]}))
return tmp_path / "store", manifest


def _run(maps, tmp_path, monkeypatch, **store):
import healsparse as hsp

exp_dir, persist = _store(tmp_path, **store)
fragment = tmp_path / "maps" / f"maps-{EXP}.hsp"
monkeypatch.setattr(sys, "argv", [
"exp_maps.py", "--exp-dir", str(exp_dir), "--exp", EXP,
"--persist-manifest", str(persist), "--fragment", str(fragment),
"--manifest", str(tmp_path / "exp_maps.json")])
maps.main()
return hsp.HealSparseMap.read(str(fragment)), json.loads(
(tmp_path / "exp_maps.json").read_text())


def _sky_pixels(maps, crval, x, y):
"""Sky pixels of DATASEC pixel positions (0-based within DATASEC)."""
import hpgeom as hpg

ra, dec = _wcs(crval).pixel_to_world_values(np.asarray(x) + PAD_X, y)
return hpg.angle_to_pixel(maps.NSIDE, ra, dec, nest=True)


def test_fragment_counts_every_flagged_pixel(maps, tmp_path, monkeypatch):
"""Each flagged DATASEC pixel is counted once, in the sky pixel holding
its centre; the overscan border counts for nothing."""
crval = (150.3, 31.7)
fragment, record = _run(maps, tmp_path, monkeypatch,
ccds=[0], crvals=[crval], valid=[0])
rows, cols = np.nonzero(_flags())
expected = dict(zip(*np.unique(_sky_pixels(maps, crval, cols, rows),
return_counts=True)))
pixels = fragment.valid_pixels
values = fragment.get_values_pix(pixels).astype(int)
got = {p: v - 1 for p, v in zip(pixels, values) if v > 1}
# Centres on the very edge of DATASEC can fall in a sky pixel whose own
# centre is outside the coverage polygon; those are dropped.
assert set(got) <= set(expected)
assert all(got[p] == expected[p] for p in got)
assert sum(got.values()) > 0.97 * rows.size
assert record["ccds"]["0"]["flagged_pixels"] == rows.size


def test_coverage_is_the_imaging_area_of_valid_psf_ccds(maps, tmp_path,
monkeypatch):
"""Interior pixel centres are covered, the overscan is not, and a CCD
without a PSF model contributes nothing."""
crvals = [(150.3, 31.7), (150.5, 31.7)]
fragment, record = _run(maps, tmp_path, monkeypatch,
ccds=[0, 1], crvals=crvals, valid=[0])
yy, xx = np.mgrid[13:NY - 13:7, 13:NX - 13:7] # > one sky pixel in
inside = _sky_pixels(maps, crvals[0], xx.ravel(), yy.ravel())
assert (fragment.get_values_pix(inside) > 0).all()
overscan = _sky_pixels(maps, crvals[0], np.full(50, -PAD_X - 4.0),
np.linspace(20, NY - 20, 50))
assert (fragment.get_values_pix(overscan) == 0).all()
other = _sky_pixels(maps, crvals[1], xx.ravel(), yy.ravel())
assert (fragment.get_values_pix(other) == 0).all()
assert list(record["ccds"]) == ["0"]


def test_merge_counts_exposures_and_flagged_pixels(maps, tmp_path,
monkeypatch):
"""Two fragments overlapping on half their pixels: nexp is 2 in the
overlap, nflagged sums the counts, and a fragment-less exposure is
skipped."""
import healsparse as hsp

merge = _load("merge_exposure_maps")
products = tmp_path / "products"
a = np.arange(1000, dtype=np.int64) + 10**9
for exp, pix in (("2000001", a), ("2000002", a + 500)):
frag = hsp.HealSparseMap.make_empty(maps.NSIDE_COVERAGE, maps.NSIDE,
np.uint8)
frag[pix] = np.ones(pix.size, np.uint8)
frag[pix[500:510]] = np.full(10, 1 + 7, np.uint8)
path = merge.fragment_path(products, exp)
path.parent.mkdir(parents=True)
frag.write(str(path))
monkeypatch.setattr(merge.build_index, "campaign_exposures",
lambda *_: ["2000001", "2000002", "2000003"])
out = {k: tmp_path / f"{k}.hsp" for k in ("nexp", "nflagged")}
monkeypatch.setattr(sys, "argv", [
"merge_exposure_maps.py", "--products-dir", str(products),
"--tile-list", "-", "--index-db", "-",
"--nexp", str(out["nexp"]), "--nflagged", str(out["nflagged"])])
merge.main()

nexp = hsp.HealSparseMap.read(str(out["nexp"]))
nflagged = hsp.HealSparseMap.read(str(out["nflagged"]))
assert nexp.get_values_pix(a[:500]).tolist() == [1] * 500
assert nexp.get_values_pix(a[500:]).tolist() == [2] * 500
assert nexp.get_values_pix(a[500:] + 500).tolist() == [1] * 500
assert nflagged.get_values_pix(a[500:510]).tolist() == [7] * 10
assert nflagged.get_values_pix(a[500:510] + 500).tolist() == [7] * 10
assert nflagged.valid_pixels.size == 20
6 changes: 3 additions & 3 deletions tests/workflow/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@ def final_cat(self, tile):
return (self.products_dir / "tiles" / tile[:2] / tile
/ f"final_cat-{tile}.fits")

def persist_manifest(self, exp):
"""Return the expected persistent manifest for one exposure."""
def persist_manifest(self, exp, stage="exp_persist"):
"""Return one exposure's manifest on the persistent root."""
return (self.products_dir / "exp" / exp[:2] / exp
/ "manifests" / "exp_persist.json")
/ "manifests" / f"{stage}.json")


@dataclass
Expand Down
4 changes: 3 additions & 1 deletion tests/workflow/params_pin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"clean_exposure": "22cb76b13a5205d20a02a9bd3b8c8bea5ea2801555e24dd2b84b11145f7e79d9",
"clean_tile": "a5c07b0461526ed407df36a291deb866d4181524b4c8e0fbc3dd047fd9d28479",
"exp_get_images": "a9e9eca0a99a348e43e0cd44d0c0aae0f890773d03e35007fb77f886b7d21f5f",
"exp_maps": "6114b2bb508217eb1a87c89a295485c4cc17f329391a27832548f525a3ef1d2e",
"exp_persist": "302e2837542bc1102430c27c81c600b7cda32e8bddcb5fd60d33950987609fff",
"exp_psf": "87b9fd799a54130eb3dd9c78bb5124838854c23f365df7a991a53f4c6ac4c3ff",
"exp_split": "8fc97dd48e76cd6c7fdf9ee388ba1c1a3dd63428cbe1961641fbebe76b05786e",
"exposure_maps": "15bc1023a759ad61b67cc27f1740a74bb3be4d268969073820766fb64d6aace1",
"final_cat_merge": "e7f46859c4503a2220713d7bb2507555515d0a9632d780b20f14c59e32210023",
"prepare_all_tiles": "b8f872a22adf014e25a7fa5198f49b71a6fe9e56042ed82b682bc8763970a844",
"star_cat_merge": "6277450958474af5270982fa35360f2f237a29f7533c526ee9265dfd5acc07a0",
Expand All @@ -24,7 +26,7 @@
"tile_vignets": "89d95bd9107c124e86dfb029d5ba2a2fc1871311a71a8fe18466302ef2213b14"
},
"schema": 1,
"sha256": "eab6930e0fd1044b9ce7a99172dbf9cbdcdec21291f8fa640d6e60c2efd3dd03",
"sha256": "23b5477740ce0ffbe6e86ee3427ed1fa9d42473519a0b8137767b8fd1ad356b9",
"unit_pre": {
"exp_get_images": "dac6685a207dae3ea81d296ce53e9ba75d4636cf1f81e6399f2dda452680c992",
"exp_psf": "87fc8ea1153a709ab7ba56542cfacc041f5f8b7f6cf2f38a9bc9d8004d1cecb4",
Expand Down
3 changes: 2 additions & 1 deletion tests/workflow/test_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"final_cat_merge",
}
TILE_SHAPE_STORE_RULES = ("tile_vignets", "tile_ngmix", "tile_make_cat")
PSF_RULES = {"exp_persist", "star_cat_merge"}
PSF_RULES = {"exp_persist", "star_cat_merge", "exp_maps", "exposure_maps"}
CATALOGUE_RULES = {"tile_get_catalogue"}


Expand Down Expand Up @@ -63,6 +63,7 @@ def test_clean_exposure_waits_on_persist_iff_psf(campaign, dag):
]
if campaign.psf_model != "fake":
expected.append(campaign.persist_manifest(exp))
expected.append(campaign.persist_manifest(exp, "exp_maps"))
assert Counter(map(str, job.input)) == Counter(map(str, expected)), (
"clean-exposure-waits-on-persist-iff-psf", exp, list(job.input)
)
Expand Down
2 changes: 2 additions & 0 deletions universes/committed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ analyses:
pixel_mask_source: instrument_flags_only
psf_star_mask_veto: instrument_flags_only
sky_mask_application: catalogue_columns
nexp_map_valid_psf_ccds: valid_psf_ccds
defect_map_from_flags: count_flagged_pixels
detection:
decisions:
tile_detection: unions_catalogue
Expand Down
3 changes: 2 additions & 1 deletion workflow/CONTRACTS
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ prose names it.
A campaign's name is the run config's `run:` and nothing else. The Snakefile
binds it once, `CAMPAIGN = config["run"]`, and every product that carries a
campaign name takes it from there: `final_cat_<run>.hdf5` and its
`patches/<run>` group, `full_starcat_<run>.hdf5`, and the `$run` in the
`patches/<run>` group, `full_starcat_<run>.hdf5`, `nexp_<run>.hsp`,
`nflagged_<run>.hsp`, and the `$run` in the
machine defaults' `products_dir`/`index_db`. No rule or script reads a
`campaign` key or derives a name from a directory (`PRODUCTS_DIR.name`); two
sources that can disagree would file one campaign's merge under another's name.
Expand Down
23 changes: 20 additions & 3 deletions workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ each overlay file to its `cfis/` original confined to input naming.
`psf_model: fake` is the simulations' true PSF: the exposure stage runs only
SExtractor (for the background maps the vignets read), and `tile_vignets` runs
`fake_interp_runner`, which writes the `galaxy_psf` product from `psf_dict`.
With no PSF model there is nothing to persist per exposure, so `exp_persist` and
`star_cat_merge` do not run and `clean_exposure` does not wait on them.
With no PSF model there is nothing to persist per exposure, so `exp_persist`,
`exp_maps` and the two merges after them do not run and `clean_exposure` does
not wait on them.
Simulations that contain stars can run `psfex` as the data do. `mccd` is
refused until `persist_exp.py` and `merge_star_cat.py` read MCCD products.

Expand Down Expand Up @@ -239,7 +240,7 @@ workflow/
bin/sp committed launcher (module load + /project venv + launch code snapshot + run/report/container/cancel)
rules/
prepare.smk tile get_images/uncompress/find_exposures
exposure.smk per-exposure: get_images, split, psf, persist (no temp()); campaign star_cat_merge
exposure.smk per-exposure: get_images, split, psf, persist, maps (no temp()); campaign star_cat_merge, exposure_maps
tile.smk per-tile: exp forest, merge_headers, detect (SExtractor, joined to the UNIONS catalogue on data), vignets, ngmix, merge, make_cat; campaign final_cat_merge
scripts/
build_index.py prepare-phase run_index.sqlite builder (plain script)
Expand All @@ -252,6 +253,8 @@ workflow/
merge_star_cat.py ALL exposures' validation_psf, out of the tars -> full_starcat_<run>.hdf5
merge_final_cat.py ALL tiles' final_cat -> final_cat_<run>.hdf5 (the final_cat_merge rule)
clean_exposure.py ONE exposure's store + manifests + logs -> tombstone (the clean_exposure rule)
exp_maps.py ONE exposure's valid-PSF CCD footprints + flagged-pixel counts -> healsparse fragment (the exp_maps rule)
merge_exposure_maps.py ALL fragments -> nexp_<run>.hsp, nflagged_<run>.hsp (the exposure_maps rule)
profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; keep-going
```

Expand Down Expand Up @@ -302,6 +305,20 @@ profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; kee
catalogue server, staged, or rasterized, which is why the old
`star_catalogue` / `exp_star_cat` / `exp_mask` rules and their cache root are
gone.
- **Two HealSparse maps come out of the exposures.** At the mask ladder's
resolution (nside 131072), `nexp_<run>.hsp` counts per sky pixel the
exposures whose CCD with a valid PSF model covers it, and
`nflagged_<run>.hsp` counts the flagged CCD pixels (bad columns, saturated
pixels, bleed trails, cosmics) of those exposures that fall in it, ~74 CCD
pixels filling one sky pixel. The flag image is the one mask that otherwise
never leaves the pixel domain, so a footprint built from CCD corners could not
subtract it (#878). `exp_maps` writes one fragment per exposure beside its PSF
tar, from the split's `DATASEC` (the overscan border is neither coverage nor
defect) and the CCDs `exp_persist` packed a PSF for; `exposure_maps` sums the
campaign's fragments. Nothing in the workflow reads either map. `nexp` is
what randoms need to match an `N_EPOCH` cut; `nflagged` is a diagnostic of
where the detector is bad, not a cut (it does not count exposures lost to
the defect veto).
- **External masks are wired, on the tile side only (data runs).** `inputs.masks`
is a third input root beside tiles and exposures, set per machine in the
`machines:` table, exported as `$SP_INPUT_MASKS` and
Expand Down
Loading
Loading