@@ -147,7 +147,8 @@ double ltsDataSectionRms(const Bytes& serialized, Modulation mod, CodeRate rate)
147147 return rmsOf (probe.encodeFrameLight (serialized));
148148}
149149
150- MeasuredSNR measureEstimatedSNR (float snr_db, uint32_t seed, const Bytes& payload) {
150+ MeasuredSNR measureEstimatedSNR (float snr_db, uint32_t seed, const Bytes& payload,
151+ bool fading = false ) {
151152 constexpr Modulation kMod = Modulation::QPSK ;
152153 constexpr CodeRate kRate = CodeRate::R1_4 ;
153154 MeasuredSNR out;
@@ -172,15 +173,29 @@ MeasuredSNR measureEstimatedSNR(float snr_db, uint32_t seed, const Bytes& payloa
172173
173174 auto audio = withSilence (samples);
174175
175- ultra::ota_channel_core::SimulatedChannel channel;
176- channel.setSeed (seed);
177- channel.configure (snr_db, ultra::ota_channel_core::ChannelType::AWGN );
178- // Keep the channel's per-burst TX level policy out of the calibration
179- // (it would re-normalize the burst and undo the known-truth injection
180- // above — see file header, decision 2).
181- channel.setTxBurstNormalizationEnabled (false );
182- channel.transmitFromA (audio);
183- audio = channel.receiveForB (audio.size ());
176+ if (fading) {
177+ // FADING section (Stage-2 contract): ITU-R F.1487 Good (0.1 Hz/0.5 ms
178+ // Watterson, unit mean tap power) at the same noise convention — the
179+ // per-seed reading samples ONE fade state; the ENSEMBLE across seeds
180+ // is the fade average (ergodicity), and this harness records readings
181+ // even for frames that fail to decode, so unlike the live decoded-
182+ // frames ring it has NO survivor bias.
183+ ultra::ota_channel_core::WattersonChannelModel model (
184+ ultra::ota_channel_core::itu_r_f1487::good (snr_db), seed);
185+ std::vector<float > faded;
186+ model.process (audio, 0 , faded);
187+ audio = std::move (faded);
188+ } else {
189+ ultra::ota_channel_core::SimulatedChannel channel;
190+ channel.setSeed (seed);
191+ channel.configure (snr_db, ultra::ota_channel_core::ChannelType::AWGN );
192+ // Keep the channel's per-burst TX level policy out of the calibration
193+ // (it would re-normalize the burst and undo the known-truth injection
194+ // above — see file header, decision 2).
195+ channel.setTxBurstNormalizationEnabled (false );
196+ channel.transmitFromA (audio);
197+ audio = channel.receiveForB (audio.size ());
198+ }
184199
185200 StreamingDecoder decoder;
186201 decoder.setLogPrefix (" CAL" );
@@ -274,6 +289,39 @@ int main() {
274289 std::cout << " \n " ;
275290 }
276291
292+ // ═══ FADING contract (Stage-2: what the RX-authority consumes) ═══
293+ // Per-frame readings on ITU Good fading sample single fade states; the
294+ // ENSEMBLE linear-mean must equal the dial (mean-power definition) and the
295+ // dB-mean must sit BELOW the linear-mean (Jensen). Tolerance 3.0 dB on the
296+ // ensemble mean: 16 one-frame fade samples, per-sample dB std ~4 ->
297+ // std(mean) ~1.1 dB, so 3.0 is a ~2.7-sigma gate (stable across seeds).
298+ std::cout << " \n fading (ITU Good) | lin-mean bias | dB-mean | n\n " ;
299+ for (float snr : {10 .f , 20 .f }) {
300+ double lin_sum = 0.0 , db_sum = 0.0 ;
301+ int n = 0 ;
302+ for (uint32_t s2 = 0 ; s2 < 16 ; ++s2) {
303+ const auto m = measureEstimatedSNR (snr, 0x7000u + s2 * 104729u ,
304+ payload, /* fading=*/ true );
305+ if (std::isfinite (m.reading )) {
306+ lin_sum += std::pow (10.0 , m.reading / 10.0 );
307+ db_sum += m.reading ;
308+ ++n;
309+ }
310+ }
311+ const double lin_mean_db = n ? 10.0 * std::log10 (lin_sum / n) : std::nan (" " );
312+ const double db_mean = n ? db_sum / n : std::nan (" " );
313+ std::cout << std::setw (10 ) << (int )snr << " | " << std::fixed
314+ << std::setprecision (1 ) << std::setw (7 ) << lin_mean_db << " "
315+ << std::showpos << std::setw (5 ) << (lin_mean_db - snr)
316+ << std::noshowpos << " | " << std::setw (6 ) << db_mean
317+ << " | " << n;
318+ const bool ok = n >= 10 && std::isfinite (lin_mean_db) &&
319+ std::abs (lin_mean_db - snr) <= 3.0 &&
320+ db_mean <= lin_mean_db + 0.3 ;
321+ if (!ok) { ++failed_points; std::cout << " <- FAIL (fading ensemble)" ; }
322+ std::cout << " \n " ;
323+ }
324+
277325 if (failed_points > 0 ) {
278326 std::cout << " \n FAIL: " << failed_points << " point(s) outside +/-" << std::fixed
279327 << std::setprecision (1 ) << tol
0 commit comments