Skip to content

Metacal flags are nonzero whenever a type has no measured shear, using ngmix's own bits - #854

Merged
cailmdaley merged 16 commits into
developfrom
ngmix-flag-wholesale-failure
Sep 28, 2026
Merged

cailmdaley merged 16 commits into
developfrom
ngmix-flag-wholesale-failure

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #853. Closes #889.

sp_validation keeps a galaxy only if NGMIX_MCAL_FLAGS == 0 and NGMIX_MCAL_TYPES_FAIL == 0, i.e. metacal succeeded. ShapePipe writes 0 in two cases where it did not:

  • Objects ngmix never fit (no usable epoch, or the fit raised). make_cat fills their flag columns with 0 and their shapes with −10. In the 64-tile smk-g7 catalogue that is 18,983 of 1,851,100 objects (1.03%); only a separate -10 sentinel check keeps them out of the shear sample.
  • A metacal type that reports flags == 0 with a non-finite or missing shear. It counts as a success.

Design. One function, get_type_flags in ngmix.py, decides whether a metacal type succeeded. NGMIX_FLAGS_<SHEAR>, NGMIX_MCAL_FLAGS (their OR) and NGMIX_MCAL_TYPES_FAIL (the count of nonzero types) all derive from it, including the values make_cat writes for never-fit objects, so the three columns cannot disagree. Every failure carries one of ngmix's own flag bits; ShapePipe adds none, so every nonzero value decodes with ngmix.flags.

Case Flag written
Fitter reported failure its own flags, unchanged
flags == 0 but g non-finite or missing ngmix.flags.LM_FUNC_NOTFINITE (2**12)
No flags key, or metacal type absent LM_FUNC_NOTFINITE
Never-fit object (every type treated as absent) LM_FUNC_NOTFINITE in all five FLAGS_<SHEAR> and MCAL_FLAGS; MCAL_TYPES_FAIL = 5

A genuine LM fit can also set LM_FUNC_NOTFINITE; NGMIX_N_EPOCH == 0 identifies never-fit rows. The consumer cut is == 0, so it rejects both either way.

Also in this PR

  • ngmix raises before fitting when centroid_source = "wcs" (the default) and the vignettes lack OFFSET (cut by an older vignetmaker). Otherwise every fit raises and the tile silently yields an empty catalogue.
  • ngmix logs an error when a tile fits no object, or every fitted object is flagged. It logs rather than raises, so one empty edge tile cannot abort a campaign.
  • ngmix_runner's early exits for tiles whose PSF or galaxy stamps are all empty now log the same error and write the empty catalogue the workflow's completeness check expects.
  • astra.yaml: the catalogue_assembly.failure_sentinels rationale and option label describe the new flag values; the pinned option is unchanged.

Tests. Synthetic results for each failure path, plus two never-fit objects, go through ngmix's writer and make_cat. The test asserts that the galaxy cut keeps exactly the two clean objects, checks each object's exact MCAL_FLAGS, that MCAL_FLAGS and MCAL_TYPES_FAIL equal the OR and count of the per-type columns, and that no bit outside ngmix.flags appears. A parametrised test checks that NaN or ±inf in either shear component of any type yields LM_FUNC_NOTFINITE. An end-to-end shapepipe_run on an all-empty tile must write one empty catalogue and log the error once. Full suite: 689 passed, 1 skipped (plus the 2 known errors in tests/cluster/test_star_shear_response.py).

For reviewers

— Claude (Opus) on behalf of Cail

🤖 Generated with Claude Code

cailmdaley and others added 10 commits September 26, 2026 02:50
mcal_flags (NGMIX_MCAL_FLAGS) faithfully recorded failures ngmix
*reported*, but defaulted an absent metacal-type result or missing
'flags' key to 0 -- success -- so a fitter failing silently (sentinel
outputs, flags never set) produced a fully-populated catalogue with an
all-zero flag column. Close the hole three ways:

* absent result / absent 'flags' now contributes FLAG_NO_RESULT (2^30)
  to mcal_flags and to mcal_types_fail instead of counting as success;
* a nominally clean fit (mcal_flags == 0) whose noshear shear is
  non-finite is flagged FLAG_NO_RESULT -- that contradiction is always
  a silent failure;
* end of loop: RuntimeError when objects were processed but none
  fitted, and a w_log.error line when 100% of fitted objects carry
  nonzero mcal_flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dA9sas4vvNBcNKRmi973L
…one empty edge tile)

The end-of-run 0-fitted guard from #854 raised RuntimeError, which would
abort an entire multi-tile campaign job over a single tile with no valid
objects. Demote to the same w_log.error used for the 100%-flagged case:
loud enough to catch in review, not fatal to the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_save_ngmix_data pre-fills every NGMIX_* column with sentinels and only
overwrites objects present in the ngmix output. An object ngmix never fit
(0 epoch to process, or an exception caught and skipped) was absent from
that output and so kept the pre-fill default of MCAL_FLAGS = 0 -- read by
sp_validation as "fit succeeded" (shapepipe#889).

Default NGMIX_MCAL_FLAGS and the per-shear-type NGMIX_FLAGS_<SHEAR> to
FLAG_NO_RESULT (imported from ngmix_package.ngmix, not duplicated), the
same bit an absent per-type result gets in ngmix.get_mcal_flags. Default
NGMIX_MCAL_TYPES_FAIL to len(METACAL_TYPES) (all types failed) rather than
0 failures, since sp_validation's classification_galaxy_ngmix also cuts on
MCAL_TYPES_FAIL == 0. NGMIX_N_EPOCH is left at its existing 0 default,
which is already the correct "never fit" value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process()'s end-of-run 0-fitted / 100%-flagged checks were inline and only
reachable through the full per-tile pipeline (Tile_cat, vignet catalogues,
...), with no unit coverage. Pull them into a module-level function taking
just (w_log, count, n_fitted, n_flagged) so the 0-fitted-logs-not-raises
contract can be tested directly. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- get_mcal_flags: an entirely absent metacal type, a result present but
  missing 'flags', and the mix of an absent type alongside a real failure
  all set FLAG_NO_RESULT rather than silently reading as success
  (shapepipe#853).
- log_run_health: 0-fitted logs (not raises), 100%-flagged logs, and a
  healthy run logs nothing (shapepipe#854).
- make_cat: an object absent from the ngmix output gets FLAG_NO_RESULT for
  MCAL_FLAGS and every FLAGS_<SHEAR> column, and len(METACAL_TYPES) for
  MCAL_TYPES_FAIL, while a present/clean object still reads 0
  (shapepipe#889).
- test_psf_averaging_properties.py's _SENTINELS contract updated to match
  (it previously pinned the pre-fix 0-default as the expected absent-object
  sentinel); its measured-row placeholder for mcal_types_fail moved off 5
  now that 5 (== len(METACAL_TYPES)) is the absent sentinel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_type_flags is the single definition of a metacal type's success: its
fit ran, reported flags == 0 and returned a finite shear. An absent
result, a missing flags key, or flags == 0 with non-finite or missing g
all give FLAG_NO_RESULT.

compile_results now derives FLAGS_<SHEAR>, MCAL_FLAGS (OR) and
MCAL_TYPES_FAIL (count) from it, so the three columns agree by
construction. This extends the non-finite guard from noshear to all five
types (a NaN g in 1p/1m/2p/2m also poisons the response), counts such
types in MCAL_TYPES_FAIL, tolerates an absent type instead of raising
KeyError at save time, and drops compile_results' own
.get("mcal_flags", 0) absent-as-clean default.

make_cat's never-fit defaults are what ngmix derives for an empty result
(get_*({})), so the two sides cannot drift apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mcal-flags-zero-means-measured (get_type_flags): 0 means the fit ran,
  succeeded and returned a finite shear; every NGMIX flag column derives
  from this, and sp_validation's galaxy cut trusts it.
- never-fit-is-not-clean (_save_ngmix_data): rows ngmix never fit take
  the empty-result flags, never 0; couples to sp_validation's
  MCAL_FLAGS == 0 && MCAL_TYPES_FAIL == 0 cut.
- run-health-logs-not-raises (log_run_health): wholesale failure is
  logged, never raised, so one tile cannot abort a campaign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tements

Add test_galaxy_cut_admits_only_measured_objects (contracts
mcal-flags-zero-means-measured, never-fit-is-not-clean): synthetic metacal
results covering every way a fit can fail to be a measurement, plus
never-fit objects, go through compile_results/save_results and
_save_ngmix_data; sp_validation's MCAL_FLAGS == 0 & MCAL_TYPES_FAIL == 0
cut must admit exactly the clean objects, each with N_EPOCH > 0 and a
finite, non-sentinel shape in all five metacal types, and MCAL_FLAGS /
MCAL_TYPES_FAIL must equal the OR / count of FLAGS_<SHEAR> on every row.

Mutation-checked: it fails for each injected regression in
get_type_flags, the aggregates, compile_results and make_cat's defaults.
It subsumes test_get_mcal_flags_flags_absent_result_and_missing_flags_key
(deleted) and the flag assertions added to the absent-objects test
(reverted; test_absent_obj_id_keeps_sentinel_fill also pins them). The
three log_run_health tests become one table over its failure modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drive process() on a tile whose every object has no stamps: it must log
the 0-fitted error and still write an empty catalogue. Catches the health
check not being called and a raise re-added in process(), which the
log_run_health table alone cannot see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cailmdaley
cailmdaley force-pushed the ngmix-flag-wholesale-failure branch from 95d097e to f4b2ada Compare September 26, 2026 00:53
centroid_source="wcs" reads the coadd-centroid OFFSET the stamp
extractor writes into every vignette epoch entry. Vignettes cut before
that extractor carry none, and until now that meant make_ngmix_observation
raised for every object in turn while Ngmix.process's per-object
exception handling quietly turned the whole tile into an empty
catalogue. check_wcs_centroid_offset inspects the first object with
epochs once, up front, and raises with the fix (re-extract with the
current vignetmaker, or set centroid_source='hsm') instead of letting
the run masquerade as healthy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cailmdaley cailmdaley changed the title ngmix: record silent metacal failures in mcal_flags; fail loudly at 100% MCAL_FLAGS == 0 only when metacal actually succeeded Sep 26, 2026
@cailmdaley
cailmdaley marked this pull request as ready for review September 26, 2026 01:18
cailmdaley and others added 2 commits September 26, 2026 06:55
Move the all-empty-store guard from ngmix_runner's SqliteDict pre-scan
into Ngmix.process itself: a tile whose galaxy entries are all 'empty',
or whose PSF entries are all {}, now takes the same per-object skip path
as any other unfittable object, reaches log_run_health, and writes the
five-HDU empty catalogue campaign completeness checks expect. The runner
no longer returns early (and no longer needs sqlitedict directly).

Adds an end-to-end test through the shipped shapepipe_run launcher,
covering both the empty-galaxy-store and empty-PSF-store cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_galaxy_cut_rejects_each_nonfinite_shear_component parametrizes the
existing non-finite-shear invariant over both g components (not just
g1/mixed cases) and every metacal type, so a first-component-only guard
in get_mcal_flags would leave a g2-only NaN/inf clean.

test_process_counts_flagged_fits_across_batches drives Ngmix.process
through real batched saves with known fitter outcomes and asserts the
run-health error line only appears when every fitted object is flagged,
closing the gap where n_flagged going disconnected from the fit loop
left the whole suite green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6272edc removed both of 84e40a5's runner-level early returns for a
wholesale-empty tile (all PSF entries {}, or all galaxy entries "empty"),
moving the check into Ngmix.process's per-object loop so the empty
catalogue and run-health log still got written. But the first of those
returns was a crash guard: it must run before anything reads the galaxy
vignette store, because unpickling that store's many-epoch arrays can
trigger a C-level malloc crash. Nobody reproduced the crash, but the
original ordering was deliberate and a malloc crash kills a campaign job,
so restore it instead of trusting that Ngmix.process's later read is safe.

Both guards are back in ngmix_runner, at their original position and in
their original order (PSF first), with Martin's comment intact. Instead of
returning silently, each now writes the same five-HDU empty catalogue and
run-health error line that a zero-fitted Ngmix.process run produces, via a
new write_empty_tile_output helper -- so #879's completeness check still
sees a catalogue for a wholesale-empty tile, without ever constructing an
Ngmix instance (which would open the galaxy vignette store) to get it.
write_empty_tile_output, and the empty_metacal_output/write_ngmix_fits
helpers it's built from, are shared with Ngmix.compile_results/save_results
so both paths produce byte-identical empty catalogues from one source.

6272edc's other change -- checking an object's PSF entry before its galaxy
entry inside Ngmix.process's per-object loop, to skip allocating the galaxy
stamp when the PSF alone is already empty -- is a genuine per-object
optimisation for a tile that is only partly empty, independent of the
tile-wide crash guard, and is kept.

Added a test asserting the PSF-all-empty guard never opens the galaxy
store: the galaxy store is a corrupted (non-sqlite) file, so opening it
would raise. Checked by hand that swapping the two guards' order turns
this red with the expected sqlite3.DatabaseError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cailmdaley

Copy link
Copy Markdown
Contributor Author

see question on CosmoStat/sp_validation#354 (comment)

cailmdaley and others added 2 commits September 28, 2026 17:30
Conflicts were docstring-adjacent: keep both this branch's text and
develop's @sc decision tags in Ngmix.process and ngmix_runner, and keep
both the galaxy-cut tests and develop's N_EPOCH_SLOTS tests in
test_make_cat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uemfjv9ybCwtKtprksZbVY
Drop the ShapePipe-only FLAG_NO_RESULT = 2**30. get_type_flags keeps a
fitter's own flags and maps every case with no finite shear -- a type
reporting flags == 0 without a finite g, a result without flags, an
absent type, and hence an object ngmix never fit -- to
ngmix.flags.LM_FUNC_NOTFINITE (2**12). Every flag column now carries
only ngmix's own bits.

The never-fit contract folds into make_cat's failure-sentinel site, and
astra.yaml's catalogue_assembly.failure_sentinels rationale records that
never-fit rows fail the MCAL_FLAGS == 0 cut rather than passing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uemfjv9ybCwtKtprksZbVY
@cailmdaley cailmdaley changed the title MCAL_FLAGS == 0 only when metacal actually succeeded Metacal flags are nonzero whenever a type has no measured shear, using ngmix's own bits Sep 28, 2026
@cailmdaley
cailmdaley merged commit ca9651d into develop Sep 28, 2026
3 checks passed
@cailmdaley

Copy link
Copy Markdown
Contributor Author

@martinkilbinger in the end I mapped our failure cases onto the existing ngmix bits so the dtype doesn't need to be changed in sp_validation. however there were other problems with the bit-writing on the SP_validation side, so CosmoStat/sp_validation#354 still fixes these but is not high-priority.

@cailmdaley
cailmdaley deleted the ngmix-flag-wholesale-failure branch September 28, 2026 15:47
cailmdaley added a commit that referenced this pull request Sep 28, 2026
Resolve tests/module/test_make_cat.py by keeping both the make_cat_runner
end-to-end test (no spread-model column) and #854's metacal-flag tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uemfjv9ybCwtKtprksZbVY
cailmdaley added a commit that referenced this pull request Sep 28, 2026
Picks up #854 (metacal flags). Two conflicts, both sides kept: Ngmix.process
ends with the epoch-cuts info log followed by log_run_health, and
ngmix_runner imports both the EPOCH_* constants and write_empty_tile_output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cailmdaley added a commit that referenced this pull request Sep 28, 2026
* fix(make_cat): return actual saved count from save_sm_data

save_sm_data returned the n_obj argument it was given instead of the
number of rows it actually saved, so the runner's SExtractor-vs-
spread-model size check compared a value with itself and could never
detect a mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bvNTrAmZxcfb1ee83ApPK

* fix(make_cat): fix crashing size-mismatch log and honest spread-model logging

- The size-mismatch warning called w_log(...) directly; w_log is a
  logger, not a callable, so a real mismatch crashed instead of being
  reported. Use w_log.warning(...), matching other runners, and fix
  the malformed message.
- With no spread-model input, the runner logged "setting spread model
  to 99" but wrote no SPREAD_MODEL column at all. The log now says
  what actually happens; behaviour for the committed three-input path
  (SM_DO_CLASSIFICATION off) is unchanged.
- SM_DO_CLASSIFICATION = True with no spread-model input is now a
  config error (there is nothing to classify on), raised clearly
  instead of being silently ignored.
- A missing SM_STAR_THRESH/SM_GAL_THRESH under classification now
  raises a clear ValueError instead of a bare configparser.NoOptionError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bvNTrAmZxcfb1ee83ApPK

* test(make_cat): cover spread-model path defects in save_sm_data/make_cat_runner

One test per defect fixed in make_cat_runner/save_sm_data, each
verified to fail against the pre-fix code and pass after: save_sm_data
returning its true saved count, the size-mismatch warning logging
without crashing, honest no-input logging with no SPREAD_MODEL column
written (matching the committed three-input path), refusing
SM_DO_CLASSIFICATION without a spread-model input, and a clear error
for a missing SM_STAR_THRESH/SM_GAL_THRESH.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bvNTrAmZxcfb1ee83ApPK

* test(make_cat): fix fixture defects flagged in review

- The classification-without-input test never wrote its ngmix
  catalogue, so on the pre-fix code it failed with
  catalogueFileNotFound instead of exercising the intended assertion.
  It now writes a matching ngmix catalogue and fails with "DID NOT
  RAISE ValueError" against the pre-fix runner, confirmed locally.
- Single-field structured arrays (NUMBER only) get collapsed by
  FITSCatalogue._save_to_fits into one row with a vector-valued
  column whenever save_sextractor_data re-saves them, so every
  SExtractor-like fixture now carries a second scalar field
  (_numbered_data) and tests assert the NUMBER column comes back as
  scalar ids matching the input.
- Present tense throughout: docstrings describe what each test
  protects, not the prior buggy behaviour; the section heading drops
  the branch-name reference.
- New tests grouped under TestSpreadModelPathDefects so a concurrent
  append to this file's end (PR #854) merges as one class-sized block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014bvNTrAmZxcfb1ee83ApPK

* Remove the spread model from ShapePipe

The spread-model star/galaxy classifier did not work reliably when last
run and separates stars from galaxies worse than the downstream
(sp_validation, size-based) classification, so ShapePipe no longer
computes or carries it.

- Delete spread_model_runner and spread_model_package.
- make_cat_runner takes exactly three inputs (tile sexcat, galaxy PSF,
  ngmix); save_sm_data and the SM_DO_CLASSIFICATION / SM_STAR_THRESH /
  SM_GAL_THRESH keys are gone, along with the tests of that path.
- Drop SM_DO_CLASSIFICATION from the committed and image-sims make_cat
  configs and the SPREAD_* columns from final_cat.param and
  example/unions_800/cat_matched.param; tidy docs and refs.bib.
- astra.yaml: star_galaxy_classification is now [HARDCODED]; the
  spread_model_inline option is excluded, with the reasons.
- A make_cat_runner end-to-end test checks every detection is kept with
  no classification column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
cailmdaley added a commit that referenced this pull request Sep 28, 2026
… feat/wire-external-masks

Resolved by keeping develop's side everywhere #879's pre-squash content
conflicted, then re-applying this branch's own changes (a846e08..dc11dab):

- astra.yaml: the mask-cut decision moves into the masking sub-analysis as
  masking.mask_default_cut, in the #875 format (no Anchor sentence; the
  make_cat contract tag cites it). masking.sky_mask_application now selects
  catalogue_columns, since the committed config sets MASK_EXT_PATHS;
  config_tile_Mc.ini's MASK_EXT_PATHS carries its tag. universes pinned.
- cfis_image_sims/config_tile_Mc.ini: rebuilt from develop's cfis copy
  (spread model gone), still without MASK_EXT_PATHS or per-epoch slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cailmdaley added a commit that referenced this pull request Sep 28, 2026
… feat/retire-versioning

Resolved by keeping develop's side where #879's pre-squash content
conflicted, then re-applying this branch's own changes (a846e08..bfb7efc).
Two by hand: docs/source/pipeline_canfar.md stays retired (develop's one-line
path fix goes with it), and the rebuilt tutorial drops the spread-model step
that #911 removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cailmdaley added a commit that referenced this pull request Sep 28, 2026
Brings in #879 (campaign merges), #911 (spread model removed), #854,
#905 and #923.

- run_config: input_types:/machines: table resolver kept; develop's
  required `run:` and unresolved-$ check folded in (now over every
  non-table key).
- create_final_cat.read_data keeps develop's raise on a missing column;
  the branch's skip-missing path is dropped (final_cat_merge is the one
  producer of final_cat_<run>.hdf5).
- final_cat.param carries both NUMBER and TILE_UNIQUE_ID.
- DAG tests follow the committed tile_detection default (data plans
  tile_get_catalogue); params pin regenerated for the tile_detect switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DksuyF9YrZAwHQeAMXsxp4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make_cat: objects ngmix never fit get NGMIX sentinels but MCAL_FLAGS = 0 ngmix mcal_flags records nothing when metacal fails wholesale

1 participant