← RFC

BW/CR adaptation — analysis result (#76)

Status: sim/analysis result (2026-06-23). Answers #76: should the coordinator's modulation authority extend from SF-only (the shipped per-cell/per-link adaptation, #66) to SF + bandwidth (+ coding rate), and does it net capacity/robustness beyond the SF gains already shipped? Experiment: sim/experiments/bw_cr_adaptation.py (uses the sim's real PHY: phy.time_on_air_ms airtime + the per-SF DEMOD_SNR_FLOOR_DB + the BW-dependent noise floor). Figure: bw_cr_adaptation.png.

The model — two sensitivity/airtime levers, at different rates

A link (SF, BW) closes iff rssi − noise_floor(BW) − DEMOD_SNR_FLOOR[SF] ≥ margin (6 dB, the sf_select default), where noise_floor(BW) = −174 + 10·log10(BW) + NF (BW125 −117 dBm, BW250 −114, BW500 −111 — wider bandwidth raises the floor ~3 dB/doubling). Airtime halves per SF step and per BW doubling. So the two levers trade sensitivity for speed at different exchange rates, which is exactly why they are not redundant:

Lever airtime ×0.5 costs granularity
SF step (e.g. SF9→SF8) ~2.5 dB sensitivity coarse (factor-2 jumps), floors at SF7
BW double (e.g. 125→250) ~3.0 dB sensitivity a second axis, usable below the SF7 floor
CR (4/5→4/8) adds airtime (+12/+23/+35%) not a speed lever — coding gain (robustness)

Result 1 — BW unlocks capacity SF-only structurally cannot reach

SF-only adaptation saturates at SF7/BW125 = 85 ms for the telemetry frame: SF7 is the fastest spreading factor, so once a link is strong enough for SF7 at BW125, SF has no faster gear. Every strong link — the near-field members, the median good-geometry link, the whole bench — is pinned there. Adding bandwidth takes that same link to SF7/BW500 = 21 ms, a −75% airtime cut (×4 capacity) (bw_cr_adaptation.py §1). Across a −124…−88 dBm sweep, SF+BW cuts telemetry airtime at every sampled RSSI (mean −57% where it helps, max −75%). Because cell capacity (members served, message slots per frame) scales as 1/airtime, this is a direct ×1.5–4 capacity multiplier for strong-link cells — larger than the ×1.6–2.0 the per-link SF work nets (#66), and structurally orthogonal: SF picks the gear, BW picks the lane width.

Result 2 — BW also fills the gaps between SF's coarse 2× steps

Even away from the SF7 floor, BW gives finer control: at RSSI −118 SF-only must use SF7/BW125 (85 ms) but SF+BW reaches SF8/BW250 (80 ms) at the same 6 dB margin; at −116, SF9/BW500 (70 ms) beats SF7/BW125 (85 ms). A member with an intermediate margin no longer rounds down to the next whole SF — it spends the margin on the lever that fits (bw_cr_adaptation.py §2).

Result 3 — CR is a robustness lever, not a capacity one

4/5 (cr=1) is the fastest coding rate; 4/6/4/7/4/8 cost +12/+23/+35% airtime (bw_cr_adaptation.py §3). So CR is never selected for capacity. Its value is the orthogonal axis the SNR-floor model doesn't capture: forward-error-correction coding gain against burst interference and fading — exactly the partial-collision and deep-fade conditions a hidden-node hub sees. CR adaptation is therefore a reliability knob: raise CR on a member whose link is marginal or whose frames are being clobbered, paying airtime for resilience; leave it at 4/5 for the clean, capacity-bound common case.

Design recommendation

Extend the coordinator's modulation authority from SF to the (SF, BW) pair, reusing the #66 machinery: - Per-cell: worst-member-driven (SF, BW) — the cell runs the fastest pair the worst member supports. A drop-in extension of per-cell auto-SF: the selector already ranks by airtime; widen its candidate set from {SF}×{125} to {SF}×{125,250,500}. - Per-link (the high-capacity case): each member's slot at its own (SF, BW); the beacon at the most-sensitive pair (slowest SF and narrowest BW = BW125, so every member hears it), members TX at their own pair, the coordinator retunes (SF, BW) per block — the same per-block retune the per-link SF path already does (set_modulation already re-sends BW with SF), just widening the per-member descriptor. - CR: a separate, conservative robustness override (default 4/5; raise per-member on measured loss/marginality), not part of the capacity selection.

Caveats / risks (flag for the firmware stage)

Verdict

Yes — BW adaptation nets substantially beyond SF-only, primarily by lifting the SF7 capacity floor (×4 for strong links) and secondarily by filling the inter-SF gaps. It is the larger capacity lever for high-link-budget cells and composes with the shipped per-link SF work. CR is worth adding as a separate robustness override but never as a capacity move. Recommend implementing (SF, BW) selection as the next modulation-authority increment (a new issue off #66), CR as a follow-on reliability knob.