feat(core): Protected Band record model, optional JSON key, L17/L18 validation, count helper (ticket 43) #42

Merged
ykp merged 1 commit from protected-bands-core into master 2026-09-28 18:38:50 +02:00
Owner

Round 9 W1 — the data layer for protected bands (spec §3.4, §4.2.5, §7.1 L17/L18 are the authority, merged as #41).

Model (gensig/core/model.py)

  • ProtectedBand frozen record: Fmin/Fmax (absolute RF MHz), Transition (MHz), Rejection (dB) + the derived cut_low_mhz/cut_high_mhz — the remez shape deduced from the record (cut span [Fmin−tr, Fmax+tr], §3.4)
  • ProjectData.ProtectedBands property: sorted by Fmin on assignment, the same assignment conventions as FrequencyBands (TypeError for non-iterables, ValueError coercion messages carrying the cause); reset() clears; edits mark dirty
  • Optional "ProtectedBands" project-JSON key: missing → [] (old files load), full round-trip, saved always (matches FrequencyBands)

Validation (gensig/core/validation.py)

  • L17 L17_PROTECTED_BAND_RECORD: Fmin < Fmax; Transition > 0; Rejection ∈ [10, 120] dB (boundaries legal); band within the instrument range — message carries the record index (ProtectedBands[1])
  • L18 L18_PROTECTED_BAND_OVERLAP: cut spans (transitions included) must be pairwise disjoint — half-open touching is legal; overlap with the signal's own bands is allowed and expected (that's the feature); degenerate records skip the L18 pass so no false positives stack on L17
  • Both ERROR-severity: generation_allowed blocks Generate
  • count_protected_in_span (§4.2.5 label rule): records whose band overlaps [fc − BW/2, fc + BW/2]; transition-only reach does NOT count (the label reflects protected bands, not cut skirts)

Tests

tests/test_protected_bands.py (22 tests): record model/cut span, 4-key requirement, coercion, sort-on-assign, JSON round-trip + optional-key tolerance, reset/dirty, the L17 matrix, the L18 matrix (overlap/touching/transitions-collide/signal-band-overlap-allowed), and the count helper (inside/straddle/outside/transition-only/empty). test_model.py's to_dict key-set premise gains the new key.

Gate

  • pytest: 1261 passed / 63 skipped, coverage 95.42 %
  • ruff + mypy: clean

Next: ticket 44 — the remez notch stage + global settings (W2).

Round 9 W1 — the data layer for protected bands (spec §3.4, §4.2.5, §7.1 L17/L18 are the authority, merged as #41). ## Model (`gensig/core/model.py`) - **`ProtectedBand`** frozen record: `Fmin`/`Fmax` (absolute RF MHz), `Transition` (MHz), `Rejection` (dB) + the derived `cut_low_mhz`/`cut_high_mhz` — the **remez shape deduced from the record** (cut span `[Fmin−tr, Fmax+tr]`, §3.4) - **`ProjectData.ProtectedBands`** property: sorted by `Fmin` on assignment, the same assignment conventions as `FrequencyBands` (TypeError for non-iterables, ValueError coercion messages carrying the cause); `reset()` clears; edits mark dirty - Optional **`"ProtectedBands"` project-JSON key**: missing → `[]` (old files load), full round-trip, saved always (matches `FrequencyBands`) ## Validation (`gensig/core/validation.py`) - **L17** `L17_PROTECTED_BAND_RECORD`: `Fmin < Fmax`; `Transition > 0`; `Rejection ∈ [10, 120]` dB (boundaries legal); band within the instrument range — message carries the record index (`ProtectedBands[1]`) - **L18** `L18_PROTECTED_BAND_OVERLAP`: cut spans (transitions **included**) must be pairwise disjoint — half-open touching is legal; **overlap with the signal's own bands is allowed and expected** (that's the feature); degenerate records skip the L18 pass so no false positives stack on L17 - Both ERROR-severity: `generation_allowed` blocks Generate - **`count_protected_in_span`** (§4.2.5 label rule): records whose **band** overlaps `[fc − BW/2, fc + BW/2]`; transition-only reach does NOT count (the label reflects protected *bands*, not cut skirts) ## Tests `tests/test_protected_bands.py` (22 tests): record model/cut span, 4-key requirement, coercion, sort-on-assign, JSON round-trip + optional-key tolerance, reset/dirty, the L17 matrix, the L18 matrix (overlap/touching/transitions-collide/signal-band-overlap-allowed), and the count helper (inside/straddle/outside/transition-only/empty). `test_model.py`'s `to_dict` key-set premise gains the new key. ## Gate - `pytest`: **1261 passed / 63 skipped**, coverage **95.42 %** - `ruff` + `mypy`: clean Next: ticket 44 — the remez notch stage + global settings (W2).
feat(core): Protected Band record model, optional JSON key, L17/L18 validation, count helper (ticket 43)
All checks were successful
test / test (ubuntu-latest) (pull_request) Successful in 2m57s
1786776ede
Round 9 W1 — the data layer for protected bands (spec §3.4, §4.2.5,
§7.1 L17/L18):

- model.py: ProtectedBand frozen record (Fmin/Fmax absolute RF MHz,
  Transition MHz, Rejection dB) with cut_low_mhz/cut_high_mhz (the
  remez shape deduced from the record: cut [Fmin−tr, Fmax+tr]);
  ProjectData.ProtectedBands property (sorted by Fmin on assignment,
  same conventions as FrequencyBands: TypeError for non-iterables,
  ValueError coercion messages); optional "ProtectedBands" JSON key
  (missing → [], tolerant load); reset() clears; edits mark dirty.
- validation.py: L17_PROTECTED_BAND_RECORD (Fmin < Fmax, Transition > 0,
  Rejection ∈ [10, 120] dB, band within instrument range; message
  carries the record index) and L18_PROTECTED_BAND_OVERLAP (cut spans
  must be disjoint INCLUDING transitions; half-open touching is legal;
  signal-band overlap allowed and expected); degenerate records skip
  the L18 pass (no false positives). Both block generation.
- count_protected_in_span (spec §4.2.5 label rule): records whose BAND
  [Fmin, Fmax] overlaps [fc − BW/2, fc + BW/2] (transition-only reach
  does NOT count — the label reflects protected bands, not cut skirts).
- tests/test_protected_bands.py (22 tests): record model + cut span,
  4-key requirement, dict/record coercion, sort-on-assign, JSON
  round-trip + optional-key tolerance, reset/dirty, L17 matrix
  (boundary 10/120 legal, index-carrying messages), L18 (overlap,
  touching, transitions-collide, signal-band overlap allowed), count
  helper (inside/straddle/outside, transition-only reach, empty cases).
- tests/test_model.py: to_dict key-set premise gains ProtectedBands.

Gate: pytest 1261 passed / 63 skipped (headless tier), coverage
95.42 %; ruff + mypy clean.
ykp merged commit 17abb0e4c3 into master 2026-09-28 18:38:50 +02:00
Sign in to join this conversation.
No description provided.