Skip to content

Commit fe7c9b2

Browse files
committed
fix: improve modulation degradation cues
1 parent 9f58ecd commit fe7c9b2

File tree

3 files changed

+82
-10
lines changed

3 files changed

+82
-10
lines changed

app/modules/modulation/engine.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
("us", "3.1"): 1024, # log2 = 10
1818
}
1919

20+
DEGRADED_QAM_THRESHOLDS = {
21+
("us", "3.1"): 256,
22+
}
23+
2024
DISCLAIMER = (
2125
"Health indices and modulation statistics are estimates based on periodic "
2226
"polling samples and may not reflect every modulation change between polls."
@@ -132,6 +136,16 @@ def _group_channels_by_protocol(channels):
132136
return dict(groups)
133137

134138

139+
def _degraded_qam_threshold(direction, docsis_version, default_threshold):
140+
"""Return the modulation threshold that counts as degraded.
141+
142+
Most protocol groups keep the legacy threshold. US DOCSIS 3.1 is stricter
143+
because 128QAM already represents a substantial drop from the normal
144+
1024QAM operating point.
145+
"""
146+
return DEGRADED_QAM_THRESHOLDS.get((direction, docsis_version), default_threshold)
147+
148+
135149
# ── Multi-day overview (distribution v2) ─────────────────────────────
136150

137151

@@ -265,7 +279,10 @@ def _build_protocol_group(version, direction, by_date, sorted_dates, threshold):
265279

266280
# Count degraded channels for this day
267281
degraded = _count_degraded_channels_day(
268-
by_date[date_str], version, direction, threshold
282+
by_date[date_str],
283+
version,
284+
direction,
285+
_degraded_qam_threshold(direction, version, threshold),
269286
)
270287

271288
days.append({
@@ -285,7 +302,11 @@ def _build_protocol_group(version, direction, by_date, sorted_dates, threshold):
285302

286303
# Overall degraded channels
287304
degraded_overall = _count_degraded_channels_overall(
288-
by_date, sorted_dates, version, direction, threshold
305+
by_date,
306+
sorted_dates,
307+
version,
308+
direction,
309+
_degraded_qam_threshold(direction, version, threshold),
289310
)
290311

291312
return {
@@ -398,8 +419,9 @@ def compute_intraday(snapshots, direction, tz_name, date_str, low_qam_threshold=
398419
hi = _health_index_for_group(
399420
[(l, q) for _, l, q in timeline], direction, version
400421
)
401-
degraded = any(q is not None and q <= low_qam_threshold for _, _, q in timeline)
402-
summary = _channel_summary(periods, max_qam, low_qam_threshold)
422+
degraded_threshold = _degraded_qam_threshold(direction, version, low_qam_threshold)
423+
degraded = any(q is not None and q <= degraded_threshold for _, _, q in timeline)
424+
summary = _channel_summary(periods, max_qam, degraded_threshold)
403425

404426
# Simplify timeline to transition points only
405427
simplified = _simplify_timeline(timeline)

app/modules/modulation/static/main.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ var QAM_COLORS = {
1010
'4QAM': '#ef4444',
1111
'16QAM': '#f97316',
1212
'64QAM': '#eab308',
13+
'128QAM': '#84cc16',
1314
'256QAM': '#22c55e',
14-
'1024QAM': '#10b981',
15+
'512QAM': '#14b8a6',
16+
'1024QAM': '#06b6d4',
1517
'4096QAM': '#3b82f6',
1618
'OFDM': '#8b5cf6',
1719
'OFDMA': '#8b5cf6',
@@ -521,7 +523,7 @@ function renderChannelTimeline(canvasId, timeline) {
521523
var dataPoints = timeline.map(function(t) { return modSortOrder(t.modulation); });
522524
var n = labels.length;
523525
var textColor = _cssVar('--text-secondary') || '#9ca3af';
524-
var qamLabels = ['4QAM', '16QAM', '64QAM', '256QAM', '1024QAM', '4096QAM', 'OFDM', 'OFDMA', 'Unknown'];
526+
var qamLabels = ['4QAM', '16QAM', '64QAM', '128QAM', '256QAM', '512QAM', '1024QAM', '4096QAM', 'OFDM', 'OFDMA', 'Unknown'];
525527

526528
var xData = [];
527529
for (var i = 0; i < n; i++) xData.push(i);
@@ -533,7 +535,7 @@ function renderChannelTimeline(canvasId, timeline) {
533535
height: h,
534536
scales: {
535537
x: { time: false, range: function() { return [-0.5, n - 0.5]; } },
536-
y: { range: [-0.5, 8.5] }
538+
y: { range: [-0.5, 10.5] }
537539
},
538540
axes: [
539541
{
@@ -554,7 +556,7 @@ function renderChannelTimeline(canvasId, timeline) {
554556
scale: 'y',
555557
stroke: textColor,
556558
grid: { stroke: 'rgba(255,255,255,0.04)', width: 1 },
557-
splits: function() { return [0,1,2,3,4,5,6,7,8]; },
559+
splits: function() { return [0,1,2,3,4,5,6,7,8,9,10]; },
558560
values: function(u, vals) { return vals.map(function(v) { return qamLabels[v] || ''; }); },
559561
font: '10px system-ui',
560562
size: 60
@@ -607,8 +609,8 @@ function densityClass(v) {
607609
return 'critical';
608610
}
609611
function modSortOrder(mod) {
610-
var order = { '4QAM': 0, '16QAM': 1, '64QAM': 2, '256QAM': 3, '1024QAM': 4, '4096QAM': 5, 'OFDM': 6, 'OFDMA': 7, 'Unknown': 8 };
611-
return order[mod] !== undefined ? order[mod] : 9;
612+
var order = { '4QAM': 0, '16QAM': 1, '64QAM': 2, '128QAM': 3, '256QAM': 4, '512QAM': 5, '1024QAM': 6, '4096QAM': 7, 'OFDM': 8, 'OFDMA': 9, 'Unknown': 10 };
613+
return order[mod] !== undefined ? order[mod] : 11;
612614
}
613615

614616
function destroyCharts() {

tests/test_modulation_engine.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_parse_qam_order,
99
_canonical_label,
1010
_distribution_pct,
11+
_degraded_qam_threshold,
1112
_health_index,
1213
_health_index_for_group,
1314
_low_qam_pct,
@@ -240,6 +241,15 @@ def test_mixed_qam_us_30(self):
240241
assert _health_index_for_group(obs, "us", "3.0") == 50.0
241242

242243

244+
class TestDegradedThresholds:
245+
def test_default_threshold_unchanged(self):
246+
assert _degraded_qam_threshold("us", "3.0", 16) == 16
247+
assert _degraded_qam_threshold("ds", "3.0", 16) == 16
248+
249+
def test_us31_uses_higher_threshold(self):
250+
assert _degraded_qam_threshold("us", "3.1", 16) == 256
251+
252+
243253
# ── _low_qam_pct ──
244254

245255
class TestLowQamPct:
@@ -448,6 +458,26 @@ def test_mixed_protocol_groups(self):
448458
assert "3.0" in versions
449459
assert "3.1" in versions
450460

461+
def test_us31_128qam_counts_as_degraded(self):
462+
us_channels = [
463+
{"channel_id": 41, "modulation": "128QAM", "docsis_version": "3.1"},
464+
]
465+
snaps = [_make_snapshot("2026-03-01T10:00:00Z", us_channels=us_channels)]
466+
result = compute_distribution_v2(snaps, "us", "UTC")
467+
pg = result["protocol_groups"][0]
468+
assert pg["docsis_version"] == "3.1"
469+
assert pg["degraded_channel_count"] == 1
470+
471+
def test_us31_512qam_not_counted_as_degraded(self):
472+
us_channels = [
473+
{"channel_id": 41, "modulation": "512QAM", "docsis_version": "3.1"},
474+
]
475+
snaps = [_make_snapshot("2026-03-01T10:00:00Z", us_channels=us_channels)]
476+
result = compute_distribution_v2(snaps, "us", "UTC")
477+
pg = result["protocol_groups"][0]
478+
assert pg["docsis_version"] == "3.1"
479+
assert pg["degraded_channel_count"] == 0
480+
451481
def test_per_day_data(self):
452482
snaps = [
453483
_make_snapshot("2026-03-01T10:00:00Z", us_channels=_make_channels(["64QAM"])),
@@ -619,6 +649,24 @@ def test_multi_protocol_groups(self):
619649
result = compute_intraday(snaps, "us", "UTC", "2026-03-01")
620650
assert len(result["protocol_groups"]) == 2
621651

652+
def test_us31_channel_summary_shows_128qam_degradation(self):
653+
snaps = [
654+
_make_snapshot("2026-03-01T10:00:00Z",
655+
us_channels=[{"channel_id": 41, "modulation": "1024QAM",
656+
"docsis_version": "3.1", "frequency": "29.775 - 64.775"}]),
657+
_make_snapshot("2026-03-01T14:00:00Z",
658+
us_channels=[{"channel_id": 41, "modulation": "128QAM",
659+
"docsis_version": "3.1", "frequency": "29.775 - 64.775"}]),
660+
_make_snapshot("2026-03-01T18:00:00Z",
661+
us_channels=[{"channel_id": 41, "modulation": "512QAM",
662+
"docsis_version": "3.1", "frequency": "29.775 - 64.775"}]),
663+
]
664+
result = compute_intraday(snaps, "us", "UTC", "2026-03-01")
665+
pg = next(pg for pg in result["protocol_groups"] if pg["docsis_version"] == "3.1")
666+
ch = pg["channels"][0]
667+
assert ch["degraded"] is True
668+
assert "128QAM" in ch["summary"]
669+
622670

623671
# ── Legacy compute_distribution (v1 compat) ──
624672

0 commit comments

Comments
 (0)