Skip to content

calibration: receptor-aware FLAG fraction QA gate#44

Merged
jakobtfaber merged 3 commits into
mainfrom
fix/flag-fraction-receptor-aware
May 4, 2026
Merged

calibration: receptor-aware FLAG fraction QA gate#44
jakobtfaber merged 3 commits into
mainfrom
fix/flag-fraction-receptor-aware

Conversation

@jakobtfaber
Copy link
Copy Markdown
Contributor

Summary

Fixes the long-standing _check_flag_fraction() bug that misread axis 0 of bandpass .b calibration tables as antennas. Real CASA shape is (rows, receptors, channels) = (1872, 2, 48) for 117 antennas × 16 SPWs × 2 polarizations × 48 channels — rows are antenna×SPW solutions, NOT antennas. Treating axis 0 as antennas produced false "464 dead antennas" log lines and inflated effective flag fraction enough to falsely fail strict QA on pristine data.

The fix joins FLAG rows with the ANTENNA1 column and aggregates per (antenna, receptor) pair via the new _flag_fraction_excluding_dead_receptors() helper.

Why this matters

Validated on real production bandpass tables at /stage/dsa110-contimg/ms/:

Table OLD (buggy) NEW (receptor-aware)
2026-01-25T22:26:05_0~23.b 448 "dead antennas" (impossible — only 117 exist), eff. = 4.93% 35 antennas with 63 dead receptors, eff. = 1.04%
2026-02-15T22:26:05_0~23.b 448 "dead antennas", eff. = 4.93% 35 antennas with 63 dead receptors, eff. = 1.04%

The buggy 4.93% was perilously close to the 5% strict threshold — slightly worse data would have caused false QA failures across production dates, not just the 3C48 smoke. This was a latent threshold-margin bug across all dates, not a 3C48-specific issue.

The original 3C48 smoke (#38) reported "464 dead antennas" + false strict-gate failure on data that was actually pristine (8e-6 flagged after the fix).

Changes

  • dsa110_continuum/calibration/calibration.py
    • _check_flag_fraction(): aggregates per (antenna, receptor) via the new helper; logs now mention "dead receptors across N antennas".
    • _flag_fraction_excluding_dead_receptors(flags, antenna_ids, *, dead_threshold=0.99): pure-numpy helper. Returns dict with effective_flag_fraction, dead_receptor_count, dead_antenna_count, working_receptor_count, working_flagged, working_total.
    • Receptor axis chosen heuristically (cell axis with size ≤ 4) — documented inline.
  • tests/test_calibration_flag_fraction.py (new, 5 tests, all passing)
    • Realistic (1872, 2, 48) shape, no dead receptors
    • One dead receptor excluded
    • Both receptors of one antenna dead
    • Regression test: 116 fully-flagged ROWS must NOT be reported as 116 dead antennas (this is the original bug)
    • Partial flagging in one receptor

What was deliberately NOT included

The original change lived in a stash that mixed this fix with a wholesale dsa110_contimg → dsa110_continuum import-rename across ~50 unrelated files. This PR cherry-picks only the FLAG-fix hunks. The import migration belongs in a separate, independently-reviewable PR.

Test plan

  • New unit tests pass (5/5)
  • Calibration-area test suite passes (59/59)
  • Spec-compliance review: surgical, no scope creep
  • Code-quality review: approved with minor issues (all addressed in 02849db)
  • Validated on real production .b tables (script: outputs/2026-05-04-flag-fraction-fix-validation/)
  • Reviewer confirms diff is the cherry-pick described, not the broader stash
  • Reviewer confirms _check_flag_fraction docstring + helper reflect new semantics

Related

🤖 Generated with Claude Code

jakobtfaber and others added 2 commits May 4, 2026 01:43
_check_flag_fraction() was misreading axis 0 of bandpass .b FLAG arrays
as antennas. Real DSA-110 .b shape is (rows, receptors, channels) =
(antennas*spws, 2, 48). Treating axis 0 as antennas inflated the
"dead antenna" count and caused strict QA to falsely fail.

Aggregate per (antenna, receptor) by joining FLAG with ANTENNA1.
Real 3C48 .b table now reports 1/124,416 = 8e-6 flagged working samples,
five orders of magnitude below the 5% strict threshold.

Add unit tests covering: realistic shape, dead receptor exclusion,
both-receptors-dead, axis-0-not-antennas regression, and partial flagging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Update _check_flag_fraction docstring to say "receptors" not "antennas"
  (matches the new aggregation behavior).
- Document the receptor-axis heuristic: CASA polarization axes are always
  ≤4; channel axes are always >4.
- Replace duplicate set(antenna_ids) computation with shared
  unique_antennas variable.
- Add effective_flag_fraction == 0 assertion to
  test_both_receptors_dead_one_antenna for symmetry with the single-dead
  case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 09:05
The comment "nrow = number of antennas" was the exact buggy assumption
the receptor-aware fix corrects. Keeping it next to the new correct
explanation would actively mislead future readers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jakobtfaber jakobtfaber merged commit 5a9e36d into main May 4, 2026
1 check passed
@jakobtfaber jakobtfaber deleted the fix/flag-fraction-receptor-aware branch May 4, 2026 09:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR narrows the calibration QA logic for bandpass tables so _check_flag_fraction() stops treating calibration-table rows as antennas and instead aggregates flags by (antenna, receptor) using ANTENNA1. In the broader calibration pipeline, that prevents false strict-QA failures on otherwise good production bandpass solves.

Changes:

  • Reworked bandpass flag-fraction QA in calibration.py to exclude fully flagged antenna/receptor pairs instead of misreading row axis 0 as antennas.
  • Added _flag_fraction_excluding_dead_receptors() to compute effective flag fraction and dead-receptor statistics from row-major FLAG arrays plus ANTENNA1.
  • Added a focused unit test file covering realistic bandpass shapes, dead-receptor exclusion, the original axis-0 regression, and partial-flagging behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
dsa110_continuum/calibration/calibration.py Updates the calibration QA gate to aggregate FLAG values by antenna/receptor and report dead-receptor-aware statistics.
tests/test_calibration_flag_fraction.py Adds unit tests for the new flag-fraction helper and the original row-axis regression scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to +10
from dsa110_continuum.calibration.calibration import (
_flag_fraction_excluding_dead_receptors,
)
Comment on lines +524 to +525
effective_flag_fraction = flag_stats["effective_flag_fraction"]
n_dead = flag_stats["dead_receptor_count"]
Comment on lines +618 to +626
effective_flag_fraction = (
working_flagged / working_total if working_total else float(np.mean(flags))
)
dead_antennas = {antenna_id for antenna_id, _ in dead_receptors}
return {
"effective_flag_fraction": float(effective_flag_fraction),
"dead_receptor_count": len(dead_receptors),
"dead_antenna_count": len(dead_antennas),
"working_receptor_count": len(unique_antennas) * receptor_count - len(dead_receptors),
jakobtfaber added a commit that referenced this pull request May 4, 2026
…ass tables

Add validation script and results for PR #44's receptor-aware FLAG fraction fix.
Script replays both old (buggy axis-0-as-antennas) and new (per-receptor aggregation)
logic on real 2026-01-25 and 2026-02-15 bandpass tables. Both show effective_flag_fraction
drops from buggy 4.9% to correct 1.04% (35 dead antennas, 63 dead receptors, 171 working
receptors with 1,368/131,328 flagged samples). Both pass 5% strict QA gate under
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.

2 participants