← RFC

Per-link mixed-SF — does the capacity gain survive the FRAME-LEVEL costs? (#66 Phase 2 gate)

Status: Stage-2 result. Date: 2026-06-22. Tag: sim-result-v6. Reproduce: cd sim && PYTHONPATH=. python3 -m experiments.per_link_sf Regression test: sim/tests/test_per_link_frame.py (11 tests). Model: sim/loomwave_sim/sf_select.pyper_cell_frame_capacity / per_link_frame_capacity (adds the beacon + guards to the v5 data-slot-only capacity).

This is the gating analysis before any per-link firmware. It answers ONE question with numbers: does the per-link capacity gain survive the real frame-level costs, and WHERE (member count N / SNR spread)?

sim-result-v5 (adaptive-sf-selection-result.md) measured the idealized data-slot-only gap: per-link beats per-cell ×1.70 (N=10) → ×2.00 (N=200). But it counted only member data slots — no beacon. Its own "Open follow-ups" flagged the missing piece: "charge the per-link beacon/packing overhead against its capacity (close the gap to its net value)." That is exactly this sim.

The catch (adaptive-modulation-design.md §6 Phase-2): per-link runs each member's DATA slot at its own fast (close) SF, but the BEACON must TX at the SLOWEST member's SF so every member can decode the schedule. A slow beacon (SF11 ≈ 1.2 s, SF12 ≈ 2.2 s at BW125) is a FIXED per-frame overhead. At small N it can be a bigger fraction of the frame than all the data slots. Per-cell pays no such penalty — its beacon runs at the cell SF everyone already decodes.

Everything below is a real run. The frame is frame_t = beacon@beacon_sf + Σ member_slots@(their SF) + N·guard, with: - beacon airtime from the repo's unit-tested ToA law (phy.time_on_air_ms); - beacon size from the spec — 80 B fixed (header+SRC+EPOCH+CELL_DESC+FRAME_T+SLOTMAP_LEN+ NEXT_PTR+SIG) + 3 B/SLOTMAP entry, and +1 B/grant for per-link's modulation byte (packet-format.md §8.1, design-note §4.2 option A); - guard G = 3 ms/slot (mac-spec.md §2.1, as-built for ±20 ppm / FRAME_T); - per-member SNR from the real path-loss model (phy.PathLossModel, n=2.7, 17 dBm), members uniform-over-area in a 6 km cell so SNR spans the SF ladder (the regime the rules diverge).


1. Headline — NET capacity (beacon charged) vs N

Aggregate delivered NET capacity (pkt/s, 162 B frames), median path (no fading), averaged over 12 cell-geometry draws. "ideal gain" is the v5 data-slot-only gap, for the erosion comparison.

N static-SF9 net per-cell net (SF) per-link net (beacon SF) NET gain beacon-frac cell / link ideal gain (v5)
5 0.973 0.632 (SF9.8) 1.023 (SF9.8) ×1.62 11.7% / 20.0% ×1.80
10 1.043 0.667 (SF9.8) 1.127 (SF9.8) ×1.69 6.9% / 12.6% ×1.81
25 1.092 0.607 (SF10) 1.185 (SF10) ×1.95 3.7% / 8.3% ×2.06
50 1.107 0.614 (SF10) 1.186 (SF10) ×1.93 2.6% / 6.1% ×2.01
100 1.115 0.617 (SF10) 1.223 (SF10) ×1.98 2.1% / 5.1% ×2.05
200 1.119 0.619 (SF10) 1.229 (SF10) ×1.98 1.8% / 4.6% ×2.05

The per-link gain SURVIVES the frame-level costs. Net gain is ×1.62 (N=5) → ×1.98 (N=200) — within a few % of the idealized ×1.7–2.0. The slow beacon erodes the prize only at small N (net ×1.62 vs ideal ×1.80 at N=5, a ~10% haircut) and the erosion vanishes by N≥25 (×1.95 net vs ×2.06 ideal). The reason: the beacon is one frame shared across all members, while the data slots scale with N — so as N grows the beacon amortizes and the net gain converges to the idealized gap.

(static-SF9 net pps looks competitive in absolute terms only because it drops the ~27% of stragglers below SF9's floor and runs the survivors fast — the v5 reach/capacity trade is unchanged: per-cell and per-link reach 100% of members, static-SF9 ~73%.)


2. The crossover — where does the slow beacon win?

Fine N-scan of the net per-link gain (12 seeds):

N per-cell net per-link net NET gain verdict
2 0.769 0.950 ×1.24 marginal
3 0.831 1.147 ×1.38 marginal
4 0.759 1.096 ×1.44 marginal
5 0.632 1.023 ×1.62 per-link wins
10 0.667 1.127 ×1.69 per-link wins
25 0.607 1.185 ×1.95 per-link wins
50 0.614 1.186 ×1.93 per-link wins
100 0.617 1.223 ×1.98 per-link wins

The mechanism is the beacon fraction in §1: at N=5 the per-link beacon is 20% of the frame (vs per-cell's 11.7%); by N=50 it is 6.1% (vs 2.6%); by N=200 4.6% (vs 1.8%). The per-link beacon is always ~2–3× the per-cell fraction (slower SF + the +1 B/grant), but it shrinks as 1/N, so the absolute erosion is only material in the small-N corner.


Vary the cell radius (hence the SNR/SF spread), N=50, 12 seeds. Tight spread → members share an SF → per-link ≈ per-cell. Wide spread → per-link wins more on data, but the slowest-SF beacon also gets slower — so net gain is the resultant of both.

cell radius SF spread (min..max) per-cell SF per-cell net per-link net NET gain ideal gain
1.5 km SF7..SF7 SF7 3.571 3.553 ×0.99 ×1.00
3.0 km SF7..SF7 SF7 3.571 3.553 ×0.99 ×1.00
4.5 km SF7..SF9 SF9 1.111 2.334 ×2.10 ×2.20
6.0 km SF7..SF10 SF10 0.614 1.186 ×1.93 ×2.01
7.5 km SF7..SF11 SF11 0.285 0.601 ×2.11 ×2.20
9.0 km SF7..SF12 SF12 0.154 0.341 ×2.22 ×2.32

The gain is entirely a spread phenomenon. With NO spread (all members at SF7, radius ≤ 3 km) per-link is ×0.99 — a slight net LOSS: the data slots are identical, so the only difference is per-link's +1 B/grant larger beacon. The instant the cell spans even SF7..SF9 (radius ≥ 4.5 km), per-link jumps to ×2.1, and it stays ×1.9–2.2 across all wider spreads. The slow-beacon penalty does NOT cancel the spread win even at the SF12 extreme (×2.22 net vs ×2.32 ideal — only a ~4% beacon haircut) because the slow members whose presence forces the slow beacon are exactly the ones whose long data slots per-link is saving everyone else from.

Takeaway: per-link pays off iff the cell spans ≥ 2 SF steps; in a single-SF cell it is a small net loss and must not be enabled.


4. The cheaper variant — cap the beacon SF, drop bottom stragglers to fallback

The recommendation §5 lever: cap the beacon SF and DON'T schedule members slower than the cap (they fall to the contention/fallback plane instead of dragging the beacon slow). Trades reach for a faster beacon + frame. Per-link, N=50, 12 seeds:

beacon cap reach beacon SF per-link net vs uncapped
none 50.0 SF10 1.186 ×1.00
SF11 50.0 SF10 1.186 ×1.00
SF10 50.0 SF10 1.186 ×1.00
SF9 37.4 SF9 1.772 ×1.49
SF8 24.2 SF8 2.789 ×2.35

At N=10 and N=200 the same shape holds (capping at SF9: ×1.36 and ×1.49 net throughput respectively, dropping ~25% of members). Capping at SF10 or SF11 is a no-op here because the worst member already lands at SF10 — the cap only bites once it is below the natural slowest SF.

The cap is a real, cheap throughput lever — but it is a policy trade (it deliberately sheds the bottom ~25% of members to the slower fallback plane to win ~50% throughput for the core). It belongs to the operator's reach-vs-capacity knob, not the default. Note the beacon fraction barely moves under the cap (6.1% → 5.6% at N=50): the cap's win is the faster DATA slots and a faster frame, not primarily a cheaper beacon. The slow beacon was never the dominant cost at N≥25 — the cap mostly buys speed by abandoning stragglers, which per-cell-auto and the fallback plane already let you do without per-link's machinery.


5. Recommendation

Build per-link — it clears the gate — but scope it by N and spread.

  1. The per-link net gain survives the frame-level costs. Charging the slow beacon + guards + the +1 B/grant byte against per-link leaves ×1.6–2.0 net capacity over per-cell, essentially the idealized v5 gap. The slow beacon does not erode the prize except marginally at N<5. This is the number #66 Phase-2 needed and v5 deferred.

  2. Gate per-link on TWO conditions, both cheap to evaluate at the coordinator:

  3. N ≥ 5 scheduled members — below that the net gain is a marginal ×1.2–1.4 and not worth the variable-width-slot + per-grant-byte machinery; run per-cell-auto.
  4. SF spread ≥ 2 steps (the coordinator already knows every member's SF from LINK_REPORTS) — in a single-SF cell per-link is a ×0.99 net loss (the larger beacon for no data win). Only flip per-link on once max(member_sf) − min(member_sf) ≥ 2.

These are a 2-line guard on the existing per-cell path; per-link is the spread+scale specialization, per-cell-auto remains the safe default and the small-cell / tight-spread choice.

  1. The beacon-SF cap is a worthwhile OPERATOR knob, not a default. Capping at SF9 buys ~×1.5 throughput by shedding ~25% of stragglers to the fallback plane. It is a reach-vs-capacity policy lever; expose it, default it OFF (per-link already serves 100% with the slow beacon).

  2. Sequencing unchanged from v5: Phase 1 (per-cell-auto) ships first — thrash-free, zero-wire-cost, the reach win. Phase 2 (per-link) is justified and now net-quantified: the ×1.6–2.0 prize is real at N≥5 / spread≥2, and v5's churn result still applies (per-link costs ~100× the SF-flip churn, hysteresis 2.5–3 dB).


6. What's measured vs idealized

Measured (real runs): - Every capacity number is the repo's unit-tested ToA law over the real path-loss model. The beacon airtime, the beacon size (spec field-level), the guard, the slowest-SF-beacon pinning, the per-link variable-slot sum, the crossover, the spread sweep, and the cap trade are all computed and seed-averaged, not asserted. - The NET gain, the beacon fraction, the crossover N, and the spread sensitivity are direct run outputs (experiments/per_link_sf.py).

Idealized (honest boundaries — same spirit as v5 §5): - No intra-frame SF-retune guard modeled. A real radio must re-tune its modem between heterogeneous per-link slots (a per-slot settle/PLL-lock the per-cell single-SF frame never pays). We fold this into the single ~3 ms G guard rather than charging a separate per-slot retune. If field retune is materially > 3 ms it would shave the small-N net gain further (the guard is N·G, so it scales with members) — the safe direction is to re-measure G once per-link firmware exists. The N≥25 results are insensitive to it (guards are <2% of the frame there). - No intra-cell PHY collisions. TDMA gives each member a slot; v4 validated the collision-free plane on the full SimPy model. This isolates the SF-selection + frame-geometry question. - Median path, no fading for the structural net gap (so it is geometry, not a fading artifact); v5's churn loop already characterized fading/thrash and is unchanged. - Datasheet SNR floors, uncalibrated (SF7 −7.5 … SF12 −20 dB, design-note §3). The gap depends on SF-ladder spacing (~2.5 dB/step), not the zero point; absolute reach moves with the GL5712-UX calibration. - One frame per member per superframe (no multi-slot members); the contention/fallback plane the §4 cap sheds stragglers to is modeled only as "not scheduled here," not simulated for its own delivery — its capacity is out of scope for this cell-level question.

Open follow-ups: charge a measured per-slot retune guard once per-link firmware can report it; BW as a second selection axis (BW250 ~2× rate for ~3 dB); correlated fading + a real mobility trace under the frame-level model; the fallback-plane delivery for stragglers the SF-cap sheds.