feat(filters): shared remez bandpass prototype — unified filter tuning (D1–D5) #51

Merged
ykp merged 1 commit from remez-filter-rewrite into master 2026-10-07 20:44:31 +02:00
Owner

Summary

Operator-confirmed rewrite (decisions D1–D5) of the bandpass primitive on the protected-band notches' Parks–McClellan core — one shared remez prototype, two band layouts.

What was wrong

Two FIR designs coexisted:

  • the protected-band notch: remez, settings-driven ripple/tap cap;
  • the bandpass primitive (bandpass.py): Kaiser+firwin, fixed 70 dB, shape-factor transition, a decimated narrow-transition apply path, and the AGWN quarter-point geometry needed to place its −3 dB edges; the post-chain BandFilter duplicated the Kaiser recipe in pipeline.py; band isolation was gated by the geometry heuristic band_isolation_min_gap_ratio (1.1 × max-BW) that silently degraded with fallback_union.

The rewrite

  • fir_prototype.py (new): the notch design machinery extracted — order_estimate, stopband_weight (δp/δs), design_remez_prototype (stopband-first = the notch layout, byte-identical after the refactor; passband-first = the bandpass layout), apply_mixed (mix → convolve "same" → remix), PrototypeDesign (warnings collected, callers emit them).
  • bandpass.py rewritten on the passband layout: flat top exactly on [fmin, fmax] (the AGWN quarter-point −3 dB trick retired, D3); explicit transition_hz / rejection_db / ripple_db / tap_cap parameters — the fixed 70 dB constant, the 1.08 shape factor and the decimated narrow-transition path retired (D4); real odd taps; FilterParams.from_settings() as the single settings funnel; loud ValueError when remez refuses even the minimal design.
  • Callers: agwn._band_filters and band_isolation._band_design_map move to exact-edge geometry (direct / mirrored+conjugate / DC-straddle split, each half flat at DC); pipeline._apply_band_filter becomes the DC-centred form of the same prototype (no _design_lowpass duplicate, no decimation).
  • Settings (D5): notch_passband_ripple_db → filter_passband_ripple_db and notch_tap_cap → filter_tap_cap (legacy JSON keys mapped on load); new band_rejection_db (default 70, range 10–120) and filter_transition_mhz (default 1.0); band_isolation_min_gap_ratio removed — the band-isolation fitness gate is now min_adjacent_gap_mhz ≥ filter_transition_mhz (refuse/fallback_union policy unchanged, L17 wording updated, band_gap_ratio helper removed). Settings-dialog rows relabelled ("Filter passband ripple (dB)", "Filter tap cap", "Band rejection (dB)", "Filter transition band (MHz)").
  • Spec/docs: spec.md §3.1 / §5.3.2 / §5.3.3, architecture.md module map, subband-filtering.md Q-F2/Q-F3.

Tests

  • test_bandpass.py rewritten for the remez acceptance: exact-edge flatness within the ripple bound, stopband at the rejection target (both lobes), transition crossover inside the transition region, in/out-of-band tone apply, tap-cap clamp + achievable-rejection warning, validation errors, factory parity.
  • test_pipeline.py: ref_band_stage re-pinned bitwise to the new _apply_band_filter; narrow-span/short-length tests de-decimated.
  • test_agwn.py: the byte-identity oracle re-pinned to an independent replay of the current design (the ticket-27 single-block invariant, now against the remez bandpass).
  • test_protected_notch.py: renamed settings + legacy-key mapping + form round-trip; the design contract tests untouched and green (notch behavior unchanged).
  • test_comb_iq.py: k=0 periodicity assertion moved to the float-noise floor (the 865-tap remez span filter drives scipy's FFT convolution path).

Gate: 1294 passed / 64 skipped (headless UI tier), ruff check clean, mypy clean.

## Summary Operator-confirmed rewrite (decisions **D1–D5**) of the bandpass primitive on the **protected-band notches' Parks–McClellan core** — one shared remez prototype, two band layouts. ## What was wrong Two FIR designs coexisted: - the protected-band notch: remez, settings-driven ripple/tap cap; - the bandpass primitive (`bandpass.py`): Kaiser+firwin, **fixed 70 dB**, shape-factor transition, a decimated narrow-transition apply path, and the AGWN quarter-point geometry needed to place its −3 dB edges; the post-chain BandFilter duplicated the Kaiser recipe in `pipeline.py`; band isolation was gated by the geometry heuristic `band_isolation_min_gap_ratio` (1.1 × max-BW) that silently degraded with `fallback_union`. ## The rewrite - **`fir_prototype.py` (new)**: the notch design machinery extracted — `order_estimate`, `stopband_weight` (δp/δs), `design_remez_prototype` (stopband-first = the notch layout, **byte-identical** after the refactor; passband-first = the bandpass layout), `apply_mixed` (mix → convolve "same" → remix), `PrototypeDesign` (warnings collected, callers emit them). - **`bandpass.py` rewritten** on the passband layout: flat top **exactly on `[fmin, fmax]`** (the AGWN quarter-point −3 dB trick retired, D3); explicit `transition_hz` / `rejection_db` / `ripple_db` / `tap_cap` parameters — the fixed 70 dB constant, the 1.08 shape factor and the decimated narrow-transition path retired (D4); real odd taps; `FilterParams.from_settings()` as the single settings funnel; loud `ValueError` when remez refuses even the minimal design. - **Callers**: `agwn._band_filters` and `band_isolation._band_design_map` move to exact-edge geometry (direct / mirrored+conjugate / DC-straddle split, each half flat at DC); `pipeline._apply_band_filter` becomes the DC-centred form of the same prototype (no `_design_lowpass` duplicate, no decimation). - **Settings (D5)**: `notch_passband_ripple_db` → **`filter_passband_ripple_db`** and `notch_tap_cap` → **`filter_tap_cap`** (legacy JSON keys mapped on load); new **`band_rejection_db`** (default 70, range 10–120) and **`filter_transition_mhz`** (default 1.0); **`band_isolation_min_gap_ratio` removed** — the band-isolation fitness gate is now `min_adjacent_gap_mhz ≥ filter_transition_mhz` (`refuse`/`fallback_union` policy unchanged, L17 wording updated, `band_gap_ratio` helper removed). Settings-dialog rows relabelled ("Filter passband ripple (dB)", "Filter tap cap", "Band rejection (dB)", "Filter transition band (MHz)"). - **Spec/docs**: `spec.md` §3.1 / §5.3.2 / §5.3.3, `architecture.md` module map, `subband-filtering.md` Q-F2/Q-F3. ## Tests - `test_bandpass.py` rewritten for the remez acceptance: exact-edge flatness within the ripple bound, stopband at the rejection target (both lobes), transition crossover inside the transition region, in/out-of-band tone apply, tap-cap clamp + achievable-rejection warning, validation errors, factory parity. - `test_pipeline.py`: `ref_band_stage` re-pinned bitwise to the new `_apply_band_filter`; narrow-span/short-length tests de-decimated. - `test_agwn.py`: the byte-identity oracle re-pinned to an independent replay of the **current** design (the ticket-27 single-block invariant, now against the remez bandpass). - `test_protected_notch.py`: renamed settings + legacy-key mapping + form round-trip; the design contract tests untouched and green (notch behavior unchanged). - `test_comb_iq.py`: k=0 periodicity assertion moved to the float-noise floor (the 865-tap remez span filter drives scipy's FFT convolution path). **Gate**: 1294 passed / 64 skipped (headless UI tier), `ruff check` clean, `mypy` clean.
feat(filters): shared remez bandpass prototype — unified filter tuning (D1-D5)
Some checks failed
test / test (ubuntu-latest) (pull_request) Failing after 2m10s
fff514c9b2
Operator-confirmed rewrite of the bandpass primitive on the protected-band
notches' Parks-McClellan core (remez everywhere, D1) with the band layout
reversed (passband first):

- fir_prototype.py (new): the notch design machinery extracted — order
  estimate, dp/ds weights, design_remez_prototype (stopband-first = the
  notch layout, byte-identical after the refactor; passband-first = the
  bandpass layout), apply_mixed (mix -> convolve same -> remix),
  PrototypeDesign (warnings collected, callers emit).
- bandpass.py rewritten on the passband layout: flat top EXACTLY on
  [fmin, fmax] (the AGWN quarter-point -3 dB trick retired, D3), explicit
  transition_hz/rejection_db/ripple_db/tap_cap parameters (the fixed 70 dB
  constant, the 1.08 shape factor and the decimated narrow-transition path
  retired, D4), real odd taps, FilterParams.from_settings() as the single
  settings funnel, loud ValueError when remez refuses the minimal design.
- Callers: agwn._band_filters and band_isolation._band_design_map move to
  exact-edge geometry (direct/mirrored+conjugate/DC-straddle, halves flat
  at DC); pipeline._apply_band_filter becomes the DC-centred form of the
  same prototype (no _design_lowpass duplicate, no decimation).
- Settings (D5): notch_passband_ripple_db -> filter_passband_ripple_db and
  notch_tap_cap -> filter_tap_cap (legacy JSON keys mapped on load); new
  band_rejection_db (default 70, range 10-120) and filter_transition_mhz
  (default 1.0); band_isolation_min_gap_ratio removed — the band-isolation
  fitness gate is min_adjacent_gap >= filter_transition_mhz (refuse/
  fallback_union policy unchanged, L17 wording updated, band_gap_ratio
  helper removed). Settings dialog rows relabelled accordingly.
- spec.md 3.1/5.3.2/5.3.3, architecture.md module map and
  subband-filtering.md Q-F2/Q-F3 updated to the shared-prototype design.
- Tests: test_bandpass.py rewritten for the remez acceptance (exact-edge
  flatness, stopband at the rejection target both lobes, transition
  crossover, tap-cap clamp warning, validation); test_pipeline re-pinned
  bitwise to the new _apply_band_filter; test_agwn oracle re-pinned to an
  independent replay of the current design; notch settings tests + legacy
  key mapping; comb-iq k=0 periodicity assertion moved to the float-noise
  floor (the 865-tap remez span filter drives scipy's FFT convolution).
- Gate: 1294 passed / 64 skipped (headless), ruff clean, mypy clean.
ykp merged commit 8af8706663 into master 2026-10-07 20:44:31 +02:00
ykp deleted branch remez-filter-rewrite 2026-10-07 20:44:31 +02:00
Sign in to join this conversation.
No description provided.