Skip to content

Wire-level CW=8 negotiation + D8PSK gate SNR>=22 (throughput push)#27

Merged
secup merged 13 commits intomainfrom
experimental/throughput-push
May 5, 2026
Merged

Wire-level CW=8 negotiation + D8PSK gate SNR>=22 (throughput push)#27
secup merged 13 commits intomainfrom
experimental/throughput-push

Conversation

@secup
Copy link
Copy Markdown
Owner

@secup secup commented May 5, 2026

Summary

  • CW=8 negotiated on the wire via CONNECT_ACK.data_frame_cw_count byte (frame payload 25→26 B) and MODE_CHANGE.payload[5] (was reserved). Initiator embeds forced CW (or 0=AUTO), responder picks via recommendCWCount(rate) and echoes the chosen value. --cw-count N is now a one-sided forced override that propagates over the wire.
  • D8PSK gate raised from SNR≥20 to SNR≥22 in good fading. 10-seed Mac↔Pi5 hw sweep showed the prior SNR=20 gate had 38% retx-hit rate and a catastrophic 270 bps tail; SNR=22 drops retx-hit to 17% with no storms and +23% mean throughput vs DQPSK alternative.
  • Deadlock fix. Previous host-side callback re-entered the protocol mutex (caught with seed=1 sim A/B); new design moves CW selection into the protocol layer and extends the data-mode-changed callback signature so hosts update encoder/decoder directly without re-entering.

What's broken / what's fixed

  • recommendCWCount(rate) is rate-only — both peers compute the same CW from the same negotiated rate, no SNR/fading drift hazard. R1/2, R2/3, R3/4 → 8; R1/4 → 4.
  • applyDataMode(mod, rate, cw_count=0) triggers requeuePendingChunks on rate-changed OR cw-changed (was rate-changed only).
  • CONNECT_ACK retry timer now computed AFTER cw is finalized (Codex finding 4).
  • requestModeChange honors config_.forced_cw_count so --cw-count survives mid-transfer rate adaptation (Codex finding 1, fixed in 2ff2332).
  • DataModeChangedCallback signature: (mod, rate, cw_count, snr, fading) — host code uses param directly instead of re-entering protocol_.setForcedFrameCodewords() (the deadlock fix).

Test plan

  • ctest --test-dir build -j4 — 35/35 green incl. ConnectionPolicy, ConnectionAdaptive, FrameV2, WaveformPolicy
  • Sim A/B seed=1: both peers log "Negotiated CW count: 8 for DQPSK R1/2", clean transfer
  • Sim sweep 10 seeds × SNR=15+20: 20/20 PASS, CW=8 negotiated every run
  • Hardware A/B Mac↔Pi5 5KB R1/2 SNR=15 good: 1448 bps, 19 frames, 0 retx (vs 39 frames at CW=4)
  • Hardware sweep 10 seeds × SNR=20: characterized D8PSK 38% retx-hit (motivated gate raise)
  • Hardware sweep 10 seeds × SNR=22: D8PSK 17% retx-hit, mean 1783 bps (justified new gate)
  • Hardware sweep 10 seeds × SNR=24: 43% retx-hit incl. 2 FAILs (confirmed not-better)
  • CI Build Matrix passes on Linux + Windows
  • --cw-count 4 forced override sanity check after merge
  • Optional: 30-seed sim sweep at the new D8PSK floor SNR=22

Hardware data motivating the throughput case

condition bps frames retx notes
pre-fix CW=4 (manual) 1077 n/a 2 original baseline
post-fix CW=8 manual 1615 n/a 0 +50% with --cw-count 8
post-fix CW=8 auto 1448 19 0 no flag needed

🤖 Generated with Claude Code

secup and others added 13 commits May 4, 2026 11:34
D8PSK was disabled in the OFDM rate ladder with a "TEMPORARY: fails
on any fading" note that's been stale since the 2026-03-15 CPE
correction + per-symbol pilot tracking landed. cli_simulator sweeps
on the experimental branch confirm D8PSK now works in fading:

  good fading (cli_simulator 7-message test):
    R1/2 SNR=8:   FAIL (cliff)
    R1/2 SNR=10:  PASS, 4 retx
    R1/2 SNR=12:  PASS, 2 retx
    R1/2 SNR=15:  PASS, 0 retx
    R2/3 SNR=10:  PASS, 28 retx (high — below cliff)
    R2/3 SNR=12:  PASS, 45 retx (very high)
    R2/3 SNR=15:  PASS, 0 retx
    R2/3 SNR=20:  PASS, 1 retx
    R3/4 SNR=20:  PASS, 6 retx (border, AWGN-only)

  moderate fading: D8PSK R1/2 stable at SNR>=15 (3-6 retx).

Conservative gate added in waveform_selection.hpp:
  - D8PSK R3/4 at AWGN (fading<0.15) AND SNR>=22
  - D8PSK R2/3 at AWGN AND SNR>=18, OR good fading (fading<0.65) AND SNR>=15
  - D8PSK R1/2 at good fading (fading<0.65) AND SNR>=10
  - DQPSK fallback otherwise (preserves all documented baselines)

Throughput math at the most common operating point:
  Before: DQPSK R1/2 good fading SNR=15 → ~2.3 kbps usable
  After:  D8PSK R1/2 good fading SNR=15 → ~3.4 kbps usable (+47%)
  After:  D8PSK R2/3 good fading SNR=15 → ~5.0 kbps usable (+117%)
        (adaptive promotion past bootstrap cap; first MODE_CHANGE)

In auto-rate cli_simulator regression sweep, all conditions PASS:
  good fading SNR=12: D8PSK R1/2, 1 retx, PASS
  good fading SNR=15: D8PSK R1/2, 0 retx, PASS
  good fading SNR=20: D8PSK R1/2, 0 retx, PASS (R2/3 after MODE_CHANGE)
  good fading SNR=25: D8PSK R1/2, 0 retx, PASS
  moderate SNR=12-25: DQPSK R1/4 to R1/2 unchanged, all PASS

Tests updated: test_protocol mode_change test (was DQPSK R2/3 at
SNR=22, now D8PSK R2/3 capped); test_waveform_policy expectations
flipped from DQPSK to D8PSK at the new gate thresholds, plus 4
new boundary checks pinning the D8PSK ladder.

ctest 35/35.

Branch: experimental/throughput-push (NOT main). Codex audited
this change as the highest-leverage tonight lever among 20
candidates surveyed; remaining levers (16-QAM, 32-QAM, larger
LDPC, HARQ-IR, per-subcarrier bit loading) all need multi-day
research-level work and are deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex (audit, 2 passes)
7-message sweep suggested D8PSK R2/3 at SNR>=15 good fading was
zero-retx. Follow-up 5KB file-transfer test at SNR=18 good fading
showed 31 retx for 28 frames — D8PSK R2/3 is too tight there. The
short test doesn't expose the failure mode the longer transfer does
because a 7-message exchange completes before the channel hits its
worst fade events.

Tightened the D8PSK R2/3 gate to three conditional tiers:
  - Clean AWGN (fading<0.10) at SNR>=15: R2/3 (preserves the
    adaptive-upgrade test path; sweep clean)
  - Slight residual fading (<0.15) at SNR>=18: R2/3
  - Real good fading (<0.65) at SNR>=20: R2/3 (file-transfer-tested)

D8PSK R1/2 stays at SNR>=10 fading<0.65 (unchanged) — that's the
dependable +47 % win over DQPSK R1/2 that doesn't break under
file-transfer load.

Verified:
  ctest 35/35 (test_connection_adaptive R2/3 promotion path stays
  reachable at SNR=15 fading=0.05)
  cli_simulator --snr 18 --fading good --file 5120 --test: PASS
  (was FAIL with the looser R2/3 gate)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
isHighThroughputOFDMMode() previously rejected anything that wasn't
DQPSK, so the new D8PSK R1/2+ rates fell back to the default OFDM
window=8. Extending the predicate to D8PSK R1/2/R2/3/R3/4 puts the
new rates on the same window=16 selective-repeat track that DQPSK
high-rate modes already use.

isSpeculativeHighRateOFDM() also now recognizes D8PSK R2/3 and R3/4
as speculative — so window=16 applies only on near-AWGN, falling
back to window=8 in real fading. R1/2 (the dependable D8PSK win)
is non-speculative and gets window=16 unconditionally when the
D8PSK gate fires (SNR>=10 fading<0.65).

Verified:
- ctest 35/35 (test_connection_policy padding-policy assertion
  flipped: D8PSK R2/3 now fires the same partial-burst padding
  policy as DQPSK R2/3, which is the right behavior post-promotion)
- cli_simulator 5KB file transfer SNR=18 good fading auto-rate:
  PASS, window=16 D8PSK R1/2 in initial mode log
- cli_simulator SNR=15 good fading R1/4 documented baseline: PASS

Combined throughput estimate on the experimental branch vs main:
  Good fading SNR=15: ~2.3 kbps → ~3.4 kbps (D8PSK R1/2 + window=16)
  Good fading SNR=20: ~3.4 kbps → ~5.0 kbps (D8PSK R2/3 + window=16)
  AWGN SNR=27:        ~3.9 kbps → ~5.9 kbps (D8PSK R3/4 + window=16)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the experimental branch's audit + execution + measurement.

Bottom line: 10 kbps in good fading is unreachable per Codex Shannon-
ceiling math. Real wins delivered:
  - D8PSK R1/2 in good fading SNR>=10: +47 % over DQPSK R1/2
  - D8PSK R2/3 in clean conditions: +47 % over DQPSK R2/3
  - D8PSK R3/4 on AWGN SNR>=24: +47 % over DQPSK R3/4
  - ARQ window=16 extended to D8PSK rates

Combined: ~3.4 kbps in typical good fading SNR=15 (was ~2.3 kbps),
~5.0 kbps in clean conditions SNR=20 (was ~3.4 kbps).

Documents three Codex audit passes, the sweep that picked the gate
thresholds, and the file-transfer stress test that tightened R2/3.
Lists the deferred levers (16-QAM, larger LDPC, HARQ-IR, per-
subcarrier bit loading) for the next throughput attempt.

Branch: experimental/throughput-push (not main). Awaits OTA
validation before merging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex (audit, 3 passes)
Mac↔Pi5 audio loopback A/B with synthetic-channel injection (5KB
file transfers, R1/2 forced, good fading) revealed a 10 dB sim-vs-
hardware gap on D8PSK:

  Forced D8PSK vs DQPSK R1/2 at SNR=15-20 good fading:
    SNR=15: DQPSK 1078 bps (2 retx) > D8PSK  728 bps (5-40 retx)
    SNR=18: DQPSK 1234 bps (0 retx) > D8PSK  641 bps (38 retx)
    SNR=20: DQPSK 1247 bps (0 retx) < D8PSK 1595 bps (0 retx)  ← +28 % win

Watterson simulator showed D8PSK R1/2 working cleanly at SNR=10
because Watterson doesn't model: soundcard ADC/DAC quantization,
audio AGC residual, anti-aliasing filter response, real audio-chain
phase noise. D8PSK's 8-phase decision is far more sensitive to
those than DQPSK's 4-phase.

Gates tightened based on real measurements:
  D8PSK R1/2: was SNR>=10 fading<0.65; now SNR>=20 fading<0.65
  D8PSK R2/3: was SNR>=15-20 fading<0.65 (multi-tier); now AWGN
              only — fading<0.10 SNR>=18 OR fading<0.15 SNR>=22
  D8PSK R3/4: unchanged — AWGN-only SNR>=24
  DQPSK fallback for everything below the new D8PSK floor

Adaptive promotion test: SNR=20 auto-rate D8PSK R1/2 at start;
adaptive only attempts R2/3 when fading drops below 0.10 (the
post-promotion measurement window) so the 486-bps "promotion-
collapsed" failure path observed earlier no longer triggers.

Also added --mod option to tools/run_hw_test.sh so future hardware
A/B comparisons can force modulation directly.

Verified:
- ctest 35/35
- Auto-rate SNR=15 good fading 5KB: PASS, 904 bps (DQPSK R1/2)
- Auto-rate SNR=20 good fading 5KB: PASS, 1130 bps (D8PSK R1/2)
- Hardware smoke 4/4: PASS

Honest position on the original "10 kbps in good fading" ask:
- Theoretical sim ceiling: ~5 kbps with D8PSK R2/3 + window=16
- Real hardware ceiling: ~1.6 kbps payload (D8PSK R1/2 SNR=20 forced)
- 10 kbps is physically unreachable on this PHY shape; this branch
  delivers a real ~28 % hardware-measured win at SNR>=20 good
  fading, no regression on documented baselines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5KB file-transfer A/B on Mac↔Pi5 audio loopback at SNR=15/18/20
good fading showed the simulator overpromised D8PSK by 10 dB. Real
cliff is SNR=20 in fading; below that, DQPSK is faster than D8PSK
because the 8-phase modulation eats more noise from soundcard
quantization + audio-chain phase jitter than the LDPC code can
recover.

Hardware-validated D8PSK R1/2 win at SNR>=20 good fading: +28 %
(1247 → 1595 bps payload, both 0 retx).

10 kbps remains unreachable. On hardware the practical ceiling
is ~1.6 kbps payload at the operating points we care about.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pushed back on apparent throughput regression. Investigation
findings, all from real Mac↔Pi5 audio loopback:

(a) "We had 2000+ bps DQPSK" was correct — for R2/3 and R3/4, not
    R1/2 (the fading-fallback rate).
        DQPSK R2/3 good SNR=20: 1422 bps (sim said 1837)
        DQPSK R3/4 AWGN SNR=25: 2058 bps (sim said 2508)

(b) "Window 16 doesn't work on fading" — not borne out by hardware
    A/B at SNR=15 good/moderate. Window=16 actually wins by 12-28 %
    over window=8 across every tested condition. The 8-frame burst
    interleaver still aligns: window=16 = 2 burst groups in series.
    Window=16 restored.

(c) D8PSK R2/3 on AWGN SNR=22+ is the real win discovered tonight:
    2382-2410 bps payload, 0 retx, beats DQPSK R3/4 (2058) at the
    same channel. Auto-rate triggers it correctly at SNR=22 AWGN
    delivering 2406 bps with 0 retx.

(d) D8PSK R3/4 ceiling on hardware: ~2620 bps at SNR=30 AWGN —
    diminishing returns vs R2/3.

Updated THROUGHPUT_PUSH_2026-05-04.md with these measurements and
the auto-rate validation. Honest 10 kbps answer remains: no, but
the 2 kbps target is achievable today on D8PSK R2/3 in AWGN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pushed: "I feel main has more speed." Settled with data —
rebuilt main on Pi5, ran identical conditions to the experimental-
branch tests:

  Test                              MAIN     EXPERIMENTAL   Δ
  DQPSK R1/2 SNR=15 good (forced)   1073 bps  1077 bps      tied
  DQPSK R2/3 SNR=20 good (forced)   1415 bps  1422 bps      tied
  DQPSK R3/4 SNR=25 AWGN (forced)   2057 bps  2058 bps      tied
  AUTO       SNR=22 AWGN            1837 bps  2406 bps      +31 %

Forced-mode tests are tied — the experimental commits don't change
the modulator or LDPC layer, only the rate-ladder gates. Experimental
wins where the gate fires (auto-rate clean conditions): main picks
DQPSK R2/3, experimental picks D8PSK R2/3, same code rate but 1.5×
bits-per-symbol = +31 % real throughput, 0 retx either branch.

No regression on any tested point. Main is not faster than this
branch on this hardware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pushed: "1077 bps DQPSK R1/2 SNR=15 good is too low —
investigate." Hardware sweep on Mac↔Pi5 with --cw-count 8
(existing CLI opt-in from commit 6cc77ea):

  Mode/Rate            Channel      SNR  cw=4         cw=8         Δ
  DQPSK R1/2           good          15  1077 bps    1615 bps    +50 %
  DQPSK R1/2           good          12  ~1100 bps   1594 bps    +45 %
  DQPSK R1/2           moderate      15  1234 bps    1594 bps    +29 %
  DQPSK R3/4           AWGN          25  2057 bps    2360 bps    +14 %
  D8PSK R2/3           AWGN          22  2406 bps    2906 bps    +21 %
  D8PSK R3/4           AWGN          27  2620 bps    3127 bps    +19 %

CW=8 wins everywhere except R2/3 in fading (14 retx vs 0 with cw=4)
because longer frames hit fade events more often. R1/2 — the
dominant fading operating mode — gets a clean +50 % at SNR=15 with
zero retx penalty.

The math: 5.3 s SACK-deferral per ARQ window is fixed overhead.
CW=8 doubles payload-per-frame, so it amortizes that fixed cost
across twice the bytes. The lever has been in --cw-count since
6cc77ea; it just isn't on by default.

Initially attempted to make this auto rate-aware in
Connection::configureArqForCurrentDataMode (R1/2 → CW=8 in the
connection setup) but it tripped the test_connection_adaptive
clean-window accumulator's timing model (longer frames need more
ticks for 3 clean windows). Reverted the auto-bump; documented
as a CLI opt-in. The auto-promote path needs a follow-up to make
adaptive window-counting CW-aware before it can ship as default.

For the user's stated goal of "more than 1077 bps DQPSK R1/2 SNR=15
good fading": deliverable today via `--cw-count 8` = 1615 bps.
Absolute hardware ceiling on this rig is ~3.1 kbps (D8PSK R3/4
AWGN SNR=27 + cw=8); 10 kbps remains unreachable.

ctest 35/35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Attempted to ship CW=8 as the auto-default for R1/2 / R3/4 / R2/3-
AWGN via recommendCWCount() helper in connection_policy.hpp +
auto-application in Connection::configureArqForCurrentDataMode().

Hit two blockers:

1. Encoder/decoder don't see protocol-side CW changes. When protocol
   bumped CW=4→8 on connect, StreamingEncoder / StreamingDecoder
   still had CW=4. Mid-handshake mismatch caused
     `libc++abi: terminating due to uncaught exception of type
      std::__1::system_error: mutex lock failed: Invalid argument`
   during the first burst-flush on hardware (Mac↔Pi5). The modem
   layer is set up upstream of Connection by ProtocolEngine /
   cli_simulator before the connection knows the data rate, with
   no callback path for CW changes downstream.
2. Global kDefaultFixedFrameCodewords = 4 → 8 as a fallback broke
   3 ctest suites (ConnectionPolicy, ConnectionAdaptive, FrameV2
   subprocess aborted). The constant feeds into frame-format math,
   fixed-buffer allocations, and adaptive timing models that all
   assume CW=4 baseline.

Both attempts reverted. Branch experimental/throughput-push stays at
9008011 documenting CW=8 as a CLI-opt-in win (--cw-count 8 today
gives the +50 % real-hardware throughput on DQPSK R1/2 SNR=15 good
fading).

Documented the refactor that ships CW=8 as default: a callback
path for protocol→modem CW-count change notifications + adaptive-
timing-test cw-aware tick budget. ~1-2 days of careful work.

ctest 35/35 (post-revert).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier attempt set CW from a host-side data-mode-changed callback that
re-entered ProtocolEngine::mutex_; non-recursive std::mutex deadlocked
the responder, CONNECT_ACK never drained from tx_queue_, initiator
timed out. Caught with seed=1 sim A/B (baseline 10.5s handshake,
patched 120s timeout, 100% reproducible).

Codex (gpt-5.5 xhigh) review surfaced three more hazards beyond the
deadlock: stale CONNECT_ACK retry timer, decoder fallback to configured
fixed_frame_codewords_ when header read fails, and "callbacks fire
under the protocol mutex". Bottom line: do not ship "both sides
recompute" as the agreement mechanism — carry CW explicitly on the wire.

Wire:
- ConnectFrame.PAYLOAD_SIZE 25→26B, new data_frame_cw_count byte.
  CONNECT carries initiator's forced CW (0=AUTO); CONNECT_ACK carries
  responder's chosen value. Initiator applies the echoed value.
- ControlFrame::ModeChangeInfo gains data_frame_cw_count via
  payload[5] (was reserved — no size change).

Policy:
- recommendCWCount(rate) is rate-only: R1/2, R2/3, R3/4 → 8; R1/4 → 4.
  No SNR/fading dependency, so cross-peer agreement collapses to
  "both peers ran the same rate negotiation".
- applyDataMode(mod, rate, cw_count=0): explicit cw from MODE_CHANGE,
  else auto via recommendCWCount(rate). Triggers requeuePendingChunks
  on rate-changed OR cw-changed (was rate-changed only).
- DataModeChangedCallback signature now (mod, rate, cw_count, snr,
  fading). Hosts poke encoder+decoder directly from the param — no
  protocol_.setForcedFrameCodewords() inside the callback, no mutex
  re-entry.
- CONNECT_ACK retry timer is now computed AFTER cw is finalized.

CLI override:
- setForcedFrameCodewords(cw, forced=true). forced=true marks
  config_.forced_cw_count for one-sided wire propagation; forced=false
  is the boot-time path (host wiring up encoder/decoder) that does NOT
  mark forced and thus does not bypass the responder's auto-pick.
- cli_simulator tracks cw_count_forced_ explicitly so only --cw-count
  trips the forced path; default init goes through forced=false.

Sim verification (seed=1, SNR=15 good fading, 5KB DQPSK R1/2 auto):
  baseline (CW=4 default): handshake at 10.5s, transfer done by 39s
  patched: both peers "Negotiated CW count: 8", transfer done by 36s

Hardware A/B (Mac↔Pi5 audio loopback, --inject good fading SNR=15,
DQPSK R1/2 5KB):
  run 1 (auto, init bug had forced=true): 1233 bps, 39 frames, 0 retx
  run 2 (auto, after init bug fixed): 1448 bps, 19 frames, 0 retx
  In-session +17%; frames halved (39→19) confirms CW=8 in effect.
  Prior force-CW measurements: 1077 bps (CW=4) → 1615 bps (CW=8).

ctest: 35/35 green incl. ConnectionPolicy, ConnectionAdaptive, and
FrameV2 — the suites that had broken on the previous abandoned attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review caught: requestModeChange() always set
pending_cw_count_ = recommendCWCount(new_rate), so an operator-forced
--cw-count 4 was silently lost on the first mid-transfer MODE_CHANGE
(rate adapted up → CW jumped to 8 against the operator's wish).

The config_.forced_cw_count field already exists for exactly this
purpose at CONNECT time; just check it again at MODE_CHANGE time.
Cast wrapper kept compatible with int return from sanitize +
recommendCWCount.

Also adds 2026-05-04 CHANGELOG entry covering the full
wire-negotiation work (commits 1a98b4d + this one).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10-seed hardware sweep (Mac↔Pi5 audio loopback, 5KB DQPSK auto,
good fading injected, 2026-05-04 post-CW=8 wire negotiation):

  SNR=20 good: D8PSK retx-hit 38 % (3/8 storms incl. 270 bps)
               mean 1448 bps ≈ DQPSK alt 1444 bps — wash with tail.
  SNR=22 good: D8PSK retx-hit 17 % (1/6 single retx, no storms)
               mean 1783 bps vs DQPSK 1450 — +23 % real win.
  SNR=24 good: D8PSK retx-hit 43 % (3/7 incl. 2 FAILs at 320-374 bps,
               17-78 retx). Counterintuitively WORSE than 22.

The single-seed CLAUDE.md datapoint (SNR=20 D8PSK 1595 bps clean)
that motivated the prior SNR>=20 gate was unrepresentative of the
distribution. Variance hidden in single-seed measurements.

Storms aren't predictable from bulk fading_index (SNR=22 storm hit
at fading=0.45; SNR=24 storms hit at 0.52/0.53/0.58 — all median),
so tightening fading further doesn't help. SNR=22 is the floor.

Test updated to assert the new ladder: SNR=22 good promotes to
D8PSK; SNR=20 good stays DQPSK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@secup secup merged commit 0c360d4 into main May 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant