Skip to content

Fix psrflux loading bugs affecting the secondary-spectrum axes - #32

Merged
danielreardon merged 1 commit into
masterfrom
fix-loader-axes
Jul 27, 2026
Merged

Fix psrflux loading bugs affecting the secondary-spectrum axes#32
danielreardon merged 1 commit into
masterfrom
fix-loader-axes

Conversation

@danielreardon

Copy link
Copy Markdown
Owner

Background

Found while investigating a user report of a "misaligned" secondary spectrum in high-frequency-resolution FAST data (PSR B1933+16, ~500 Hz and ~2.9 kHz channels).

I audited the whole chain from loading a psrflux dynamic spectrum through to plotting the secondary spectrum. calc_sspec and plot_sspec came out clean — verified empirically:

  • zero-delay and zero-Doppler land on exactly the right bin for both even and odd nf/nt (the transform is zero-padded to a power of two, so input parity is irrelevant);
  • pre-whitening introduces no shiftconvolve2d(..., mode='valid') moves the sample grid by half a pixel, but taking |F|² discards that linear phase (synthetic features land on the identical bin with prewhite on and off);
  • plot_sspec uses centres_to_edges + pcolormesh consistently with plot_dyn, so cells are centred on their coordinates, and the fitted-arc overlay shares that convention (its vertex sits exactly on the DC cell).

All of the real defects were in the loader.

Fixes

1. remove_short_subs deleted far more than it should (High)

The loop used <= together with a redundant, always-true sdt >= 0 guard. Once the surviving sub-integrations became uniformly spaced, sdt fell to 0 and the condition collapsed to dt0 - dt <= 0, so it kept deleting uniformly spaced subints.

Reproduced: a 6-subint file with one short leading subint lost 4 of them (only 2 survived), silently truncating the observation and its time axis. It now stops as soon as the first subint is no longer a short outlier.

2. Channel bandwidth off-by-one (Medium)

psrflux frequencies are channel centres, so the spacing is span/(nchan-1), but df was computed as span/nchan. df is now taken from the median difference between channel centres (also robust to an occasional missing channel).

For the bundled 400 MHz / 512-channel example file this changes df from 0.77972 to 0.78125 MHz (= 400/512, exactly). Since the delay axis is built as tdel ~ 1/df, this directly mis-scaled the secondary-spectrum delay axis — by ~1/(nchan−1), so significant for small channel counts and negligible for very large ones.

3. Non-uniform channels were silently assumed uniform (High, when it applies)

The ACF and secondary-spectrum FFTs assume a uniform frequency grid, but nothing checked. Loading now warns when the channel spacing is non-uniform (gaps or stitched sub-bands), and separately when the number of unique channel centres doesn't match the file's channel count.

4. Channel-ordering detection made independent of bw/df

Descending-frequency files are now detected from the first channel block rather than inferred from a negative bandwidth, so the flux-row flip no longer depends on how bw/df are derived. Verified to agree with the previous behaviour on the bundled example file.

Verification

  • New tests/test_dynspec_loading.py (12 tests): remove_short_subs removes exactly the short subint / leaves a uniform cadence intact, df matches the true spacing for several channel counts and for the real example file, non-uniform channels warn (and uniform ones don't), descending and ascending files keep each flux value matched to its (frequency, time), and the delay axis reflects the corrected df.
  • Full suite: 86 passed (74 existing + 12 new) — no regressions.
  • flake8 CI gate (E9,F63,F7,F82) returns 0; the new test file is style-clean.
  • Checked against the bundled J0437-4715 file: nchan/nsub/dt/tobs/bw unchanged, no spurious warnings, frequencies correctly ascending.

Note for users

The df change slightly alters the delay-axis scaling of previously computed secondary spectra (it is now correct). The effect is ~1/(nchan−1) — noticeable for low channel counts, well under 0.01% for the very large channel counts typical of baseband data.

🤖 Generated with Claude Code


Generated by Claude Code

Found while investigating a report of a "misaligned" secondary spectrum in
high-frequency-resolution FAST data. The secondary-spectrum FFT and plotting
code were audited and found to be coordinate-correct (zero-delay and
zero-Doppler land on the right bins for both even and odd channel/subint
counts, pre-whitening introduces no shift, and plot_sspec handles cell
centres/edges consistently); the defects were all in the loader.

remove_short_subs (dynspec.py):
- The loop condition used `<=` together with a redundant, always-true
  `sdt >= 0` guard. Once the surviving sub-integrations became uniformly
  spaced, sdt fell to 0 and the test collapsed to `dt0 - dt <= 0`, so it
  kept deleting uniformly spaced subints. A 6-subint file with one short
  leading subint lost 4 of them (only 2 survived), silently truncating the
  observation and its time axis. Now stops as soon as the first subint is no
  longer a short outlier, and never runs the array down.

Channel bandwidth (dynspec.py):
- psrflux frequencies are channel centres, so the spacing is span/(nchan-1),
  but df was computed as span/nchan. df is now taken from the median
  difference between channel centres, which is also robust to an occasional
  missing channel. For the bundled 400 MHz / 512-channel example file this
  changes df from 0.77972 to 0.78125 MHz (= 400/512, exactly). Because the
  delay axis is built as tdel ~ 1/df, this directly mis-scaled the
  secondary-spectrum delay axis (~1/(nchan-1); large for small channel
  counts, negligible for very large ones).

Non-uniform channels (dynspec.py):
- The ACF and secondary-spectrum FFTs assume a uniform frequency grid, but
  nothing checked for one. Loading now warns when the channel spacing is
  non-uniform (gaps or stitched sub-bands), which distorts the delay axis,
  and separately warns when the number of unique channel centres does not
  match the channel count from the file.

Channel ordering:
- Descending-frequency files are now detected from the first channel block
  rather than inferred from a negative bandwidth, which keeps the flux-row
  flip independent of how bw/df are derived. Verified to agree with the
  previous behaviour on the bundled example file, and covered by tests that
  check each flux value stays matched to its (frequency, time).

Adds tests/test_dynspec_loading.py with 12 regression tests. Full suite:
86 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6cv3jHNLYv1TN2YWDqi3
@danielreardon

Copy link
Copy Markdown
Owner Author

Fixes for independently identified dynspec loading bugs.

@danielreardon
danielreardon merged commit 804e194 into master Jul 27, 2026
3 checks passed
@danielreardon
danielreardon deleted the fix-loader-axes branch July 27, 2026 00:37
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