Skip to content

Commit ea28173

Browse files
author
Dennis Braun
committed
Enhance modulation watchdog: severity based on QAM hierarchy
Modulation downgrades now trigger warning (small drop) or critical (>=3 QAM levels, e.g. 256QAM to 16QAM). Upgrades remain info severity. Messages distinguish 'dropped' vs 'improved'. Details include direction field (downgrade/upgrade) and rank_drop per channel. QAM hierarchy: QPSK/4QAM < 8QAM < 16QAM < 64QAM < 256QAM < 1024QAM < 4096QAM Tests: 1 existing updated, 2 new added (175 total)
1 parent 50c3faf commit ea28173

File tree

2 files changed

+84
-8
lines changed

2 files changed

+84
-8
lines changed

app/event_detector.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@
1111
SNR_CRIT_THRESHOLD = 25.0
1212
UNCORR_SPIKE_THRESHOLD = 1000
1313

14+
# QAM hierarchy: higher value = better modulation
15+
QAM_ORDER = {
16+
"QPSK": 1, "4QAM": 1,
17+
"8QAM": 2,
18+
"16QAM": 3,
19+
"32QAM": 4,
20+
"64QAM": 5,
21+
"128QAM": 6,
22+
"256QAM": 7,
23+
"512QAM": 8,
24+
"1024QAM": 9,
25+
"2048QAM": 10,
26+
"4096QAM": 11,
27+
}
28+
# A drop of this many levels or more counts as critical (e.g. 256QAM → 16QAM = 4 levels)
29+
QAM_CRITICAL_DROP = 3
30+
1431

1532
class EventDetector:
1633
"""Compare consecutive analyses and emit event dicts."""
@@ -165,21 +182,46 @@ def _check_modulation(self, events, ts, cur_analysis, prev_analysis):
165182
cur_us = {ch["channel_id"]: ch.get("modulation", "") for ch in cur_analysis.get("us_channels", [])}
166183
prev_us = {ch["channel_id"]: ch.get("modulation", "") for ch in prev_analysis.get("us_channels", [])}
167184

168-
changed = []
185+
downgrades = []
186+
upgrades = []
169187
for ch_id in set(cur_ds) & set(prev_ds):
170188
if cur_ds[ch_id] != prev_ds[ch_id]:
171-
changed.append({"channel": ch_id, "direction": "DS", "prev": prev_ds[ch_id], "current": cur_ds[ch_id]})
189+
entry = {"channel": ch_id, "direction": "DS", "prev": prev_ds[ch_id], "current": cur_ds[ch_id]}
190+
cur_rank = QAM_ORDER.get(cur_ds[ch_id], 0)
191+
prev_rank = QAM_ORDER.get(prev_ds[ch_id], 0)
192+
entry["rank_drop"] = prev_rank - cur_rank
193+
if cur_rank < prev_rank:
194+
downgrades.append(entry)
195+
else:
196+
upgrades.append(entry)
172197
for ch_id in set(cur_us) & set(prev_us):
173198
if cur_us[ch_id] != prev_us[ch_id]:
174-
changed.append({"channel": ch_id, "direction": "US", "prev": prev_us[ch_id], "current": cur_us[ch_id]})
175-
176-
if changed:
199+
entry = {"channel": ch_id, "direction": "US", "prev": prev_us[ch_id], "current": cur_us[ch_id]}
200+
cur_rank = QAM_ORDER.get(cur_us[ch_id], 0)
201+
prev_rank = QAM_ORDER.get(prev_us[ch_id], 0)
202+
entry["rank_drop"] = prev_rank - cur_rank
203+
if cur_rank < prev_rank:
204+
downgrades.append(entry)
205+
else:
206+
upgrades.append(entry)
207+
208+
if downgrades:
209+
max_drop = max(d["rank_drop"] for d in downgrades)
210+
severity = "critical" if max_drop >= QAM_CRITICAL_DROP else "warning"
211+
events.append({
212+
"timestamp": ts,
213+
"severity": severity,
214+
"event_type": "modulation_change",
215+
"message": f"Modulation dropped on {len(downgrades)} channel(s)",
216+
"details": {"changes": downgrades, "direction": "downgrade"},
217+
})
218+
if upgrades:
177219
events.append({
178220
"timestamp": ts,
179221
"severity": "info",
180222
"event_type": "modulation_change",
181-
"message": f"Modulation changed on {len(changed)} channel(s)",
182-
"details": {"changes": changed},
223+
"message": f"Modulation improved on {len(upgrades)} channel(s)",
224+
"details": {"changes": upgrades, "direction": "upgrade"},
183225
})
184226

185227
def _check_errors(self, events, ts, cur, prev):

tests/test_events.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ def test_us_channel_count_change(self, detector):
235235
assert len(ch_events) == 1
236236
assert "US" in ch_events[0]["message"]
237237

238-
def test_modulation_change(self, detector):
238+
def test_modulation_downgrade_warning(self, detector):
239+
"""256QAM → 64QAM = 2-level drop = warning"""
239240
ds1 = [{"channel_id": 1, "modulation": "256QAM", "power": 3.0, "snr": 35.0,
240241
"correctable_errors": 10, "uncorrectable_errors": 5,
241242
"docsis_version": "3.0", "health": "good", "health_detail": "", "frequency": "602 MHz"}]
@@ -246,7 +247,40 @@ def test_modulation_change(self, detector):
246247
events = detector.check(_make_analysis(ds_total=1, ds_channels=ds2))
247248
mod_events = [e for e in events if e["event_type"] == "modulation_change"]
248249
assert len(mod_events) == 1
250+
assert mod_events[0]["severity"] == "warning"
251+
assert "dropped" in mod_events[0]["message"]
252+
assert mod_events[0]["details"]["direction"] == "downgrade"
253+
254+
def test_modulation_downgrade_critical(self, detector):
255+
"""256QAM → 16QAM = 4-level drop = critical"""
256+
ds1 = [{"channel_id": 1, "modulation": "256QAM", "power": 3.0, "snr": 35.0,
257+
"correctable_errors": 10, "uncorrectable_errors": 5,
258+
"docsis_version": "3.0", "health": "good", "health_detail": "", "frequency": "602 MHz"}]
259+
ds2 = [{"channel_id": 1, "modulation": "16QAM", "power": 3.0, "snr": 35.0,
260+
"correctable_errors": 10, "uncorrectable_errors": 5,
261+
"docsis_version": "3.0", "health": "good", "health_detail": "", "frequency": "602 MHz"}]
262+
detector.check(_make_analysis(ds_total=1, ds_channels=ds1))
263+
events = detector.check(_make_analysis(ds_total=1, ds_channels=ds2))
264+
mod_events = [e for e in events if e["event_type"] == "modulation_change"]
265+
assert len(mod_events) == 1
266+
assert mod_events[0]["severity"] == "critical"
267+
assert "dropped" in mod_events[0]["message"]
268+
269+
def test_modulation_upgrade_info(self, detector):
270+
"""64QAM → 256QAM = upgrade = info"""
271+
ds1 = [{"channel_id": 1, "modulation": "64QAM", "power": 3.0, "snr": 35.0,
272+
"correctable_errors": 10, "uncorrectable_errors": 5,
273+
"docsis_version": "3.0", "health": "good", "health_detail": "", "frequency": "602 MHz"}]
274+
ds2 = [{"channel_id": 1, "modulation": "256QAM", "power": 3.0, "snr": 35.0,
275+
"correctable_errors": 10, "uncorrectable_errors": 5,
276+
"docsis_version": "3.0", "health": "good", "health_detail": "", "frequency": "602 MHz"}]
277+
detector.check(_make_analysis(ds_total=1, ds_channels=ds1))
278+
events = detector.check(_make_analysis(ds_total=1, ds_channels=ds2))
279+
mod_events = [e for e in events if e["event_type"] == "modulation_change"]
280+
assert len(mod_events) == 1
249281
assert mod_events[0]["severity"] == "info"
282+
assert "improved" in mod_events[0]["message"]
283+
assert mod_events[0]["details"]["direction"] == "upgrade"
250284

251285
def test_error_spike(self, detector):
252286
detector.check(_make_analysis(ds_uncorrectable_errors=100))

0 commit comments

Comments
 (0)