feat(notch): protected-band remez notch stage + global settings + pipeline dispatch (ticket 44) #43

Merged
ykp merged 1 commit from protected-notch-stage into master 2026-09-28 20:19:45 +02:00
Owner

Round 9 W2 — the signal-processing stage for protected bands (spec §5.3.3 is the authority; #42 shipped the W1 data layer).

The stage — protected_notch.py (new)

One remez FIR notch per counted record, applied successively in table order:

  • One-sided complex-baseband semantics via the band filter's mix–filter–remix pattern (§5.3.2): mix the record's stopband centre to DC, apply a real type-1 lowpass prototype (stop [0, w], pass from w + Transition — the remez shape deduced from the record), remix. A plain real symmetric filter would also cut the mirror band — for a 985–995 MHz record the mirror (RF 1005–1015) survives at +0.26 dB while the notch reaches −73 dB at centre (wide transition, cap unhit).
  • Stopband weight from Rejection: δp/δs with δp = 10^(ripple/20)−1 and δs = 10^(−rejection/20).
  • Order: Kaiser transition estimate, clamped at the tap cap and forced odd; a clamp warns (the achievable rejection may fall below the target — e.g. a 0.5 MHz transition @ 200 MHz needs 1449 taps, clamps to 512 → −37 dB measured). Pathological geometries fall back to the minimum design rather than aborting the chain.
  • Occupied-span gate: a record whose cut span misses [fc−BW/2, fc+BW/2] contributes no filter; zero counted records ⇒ bit-identical passthrough.

Global settings (operator decision #7)

notch_passband_ripple_db (default 0.05, range [0.001, 1.0] dB) and notch_tap_cap (default 512, range [16, 4096]) — validators, JSON persistence (tolerant of old files), and two new settings-dialog rows.

Dispatch

pipeline.apply_post_chain gains the ProtectedNotch stage between BandIsolation and Windowing; docstring chain updated.

Tests — tests/test_protected_notch.py (18)

Design shape (odd taps, mix centre, cap hit), out-of-baseband skip, one-sided semantics (the headline contract), stopband-weight math, cascade composition (order-invariant in the interior — each stage is diagonal in frequency; the convolution-edge samples legitimately differ), span-gate skip, no-mutation, passthroughs, count helper, post-chain wiring, settings defaults/validation/round-trip/form mapping, tap-cap flow, determinism. Premise sets (settings to_dict keys, dialog field shape) updated.

Gate

  • pytest: 1278 passed / 63 skipped, coverage 94.53 %
  • ruff + mypy: clean
Round 9 W2 — the signal-processing stage for protected bands (spec §5.3.3 is the authority; #42 shipped the W1 data layer). ## The stage — `protected_notch.py` (new) One remez FIR notch per counted record, applied **successively in table order**: - **One-sided complex-baseband semantics** via the band filter's mix–filter–remix pattern (§5.3.2): mix the record's stopband **centre** to DC, apply a real type-1 lowpass prototype (stop `[0, w]`, pass from `w + Transition` — the remez shape **deduced from the record**), remix. A plain real symmetric filter would also cut the mirror band — for a 985–995 MHz record the mirror (RF 1005–1015) survives at **+0.26 dB** while the notch reaches **−73 dB** at centre (wide transition, cap unhit). - **Stopband weight from `Rejection`**: `δp/δs` with `δp = 10^(ripple/20)−1` and `δs = 10^(−rejection/20)`. - **Order**: Kaiser transition estimate, clamped at the tap cap and forced odd; a clamp **warns** (the achievable rejection may fall below the target — e.g. a 0.5 MHz transition @ 200 MHz needs 1449 taps, clamps to 512 → −37 dB measured). Pathological geometries fall back to the minimum design rather than aborting the chain. - Occupied-span gate: a record whose cut span misses `[fc−BW/2, fc+BW/2]` contributes no filter; zero counted records ⇒ **bit-identical passthrough**. ## Global settings (operator decision #7) `notch_passband_ripple_db` (default **0.05**, range [0.001, 1.0] dB) and `notch_tap_cap` (default **512**, range [16, 4096]) — validators, JSON persistence (tolerant of old files), and two new settings-dialog rows. ## Dispatch `pipeline.apply_post_chain` gains the **ProtectedNotch stage between BandIsolation and Windowing**; docstring chain updated. ## Tests — `tests/test_protected_notch.py` (18) Design shape (odd taps, mix centre, cap hit), out-of-baseband skip, one-sided semantics (the headline contract), stopband-weight math, cascade composition (**order-invariant in the interior** — each stage is diagonal in frequency; the convolution-edge samples legitimately differ), span-gate skip, no-mutation, passthroughs, count helper, post-chain wiring, settings defaults/validation/round-trip/form mapping, tap-cap flow, determinism. Premise sets (settings `to_dict` keys, dialog field shape) updated. ## Gate - `pytest`: **1278 passed / 63 skipped**, coverage **94.53 %** - `ruff` + `mypy`: clean
feat(notch): protected-band remez notch stage + global settings + pipeline dispatch (ticket 44)
All checks were successful
test / test (ubuntu-latest) (pull_request) Successful in 2m47s
0d2138b8f3
Round 9 W2 — the signal-processing stage for protected bands
(spec §5.3.3):

- protected_notch.py (new): one remez FIR notch per counted record,
  applied successively in table order. One-sided complex-baseband
  semantics via the band filter's mix–filter–remix pattern: mix the
  record's stopband CENTRE to DC, apply a real type-1 lowpass prototype
  (stop [0, w], pass from w+Transition — the remez shape deduced from
  the record), remix. A real symmetric filter alone would also cut the
  mirror band (RF 1005–1015 for a 985–995 record — measured +0.26 dB,
  survives); the mixed prototype cuts exactly the one-sided span
  (−73 dB at centre with the 512-tap cap unhit).
- stopband weight from Rejection: δp/δs with δp = 10^(ripple/20)−1
  (passband-ripple setting), δs = 10^(−rejection/20); order from the
  Kaiser transition estimate clamped at the tap cap (warn on clamp:
  the achievable rejection may fall below the target — 0.5 MHz
  transition @ 200 MHz needs 1449 taps, clamps to 512 → −37 dB);
  pathological geometries fall back to the minimum design.
- settings.py: notch_passband_ripple_db (default 0.05, range
  [0.001, 1.0] dB) + notch_tap_cap (default 512, range [16, 4096]) —
  operator decision #7 (global settings); validators, to_dict/load
  (tolerant of old files), dialog field mapping.
- settings_dialog.py: two new rows (Notch passband ripple (dB), Notch
  tap cap); separator/buttons shifted.
- pipeline.py: ProtectedNotch stage between BandIsolation and
  Windowing; gate = ProtectedBands non-empty; occupied-span gate
  (cut span vs [fc−BW/2, fc+BW/2]) skips out-of-span records;
  zero records ⇒ bit-identical passthrough.
- tests/test_protected_notch.py (18 tests): design shape (odd taps,
  mix centre, cap hit), out-of-baseband skip, one-sided semantics
  (notch −73 dB / mirror +0.01 dB / passband +0.01 dB with wide
  transitions), stopband-weight math, cascade composition (order
  invariant in the interior — each stage is diagonal in frequency;
  the edge samples legitimately differ), span-gate skip, no-mutation,
  empty passthrough, count helper, post-chain wiring (notch applied /
  no records bit-identical), settings defaults/validation/round-trip/
  form mapping, tap-cap flows into the design, determinism.
- premise updates: settings to_dict key set, dialog field-shape set.

Gate: pytest 1278 passed / 63 skipped (headless tier), coverage
94.53 %; ruff + mypy clean.
ykp merged commit 4a223950e4 into master 2026-09-28 20:19:45 +02:00
Sign in to join this conversation.
No description provided.