C(++)ECCO
C++ Error Control COding: a header-only library for ECC simulations and experiments, modeling complete coding systems across arbitrary finite fields and complex inter-field relationships - Christian Senger <senger@inue.uni-stuttgart.de>
Loading...
Searching...
No Matches
blocks.hpp
Go to the documentation of this file.
1/**
2 * @file blocks.hpp
3 * @brief Communication system blocks library
4 * @author Christian Senger <senger@inue.uni-stuttgart.de>
5 * @version 2.2.5
6 * @date 2026
7 *
8 * @copyright
9 * Copyright (c) 2026, Christian Senger <senger@inue.uni-stuttgart.de>
10 *
11 * Licensed for noncommercial use only, including academic teaching, research, and personal non-profit purposes.
12 * Commercial use is prohibited without a separate commercial license. See the [LICENSE](../../LICENSE) file in the
13 * repository root for full terms and how to request a commercial license.
14 *
15 * @section Description
16 *
17 * Channel models, modulation/demodulation, and field multiplexing blocks for error-control
18 * coding experiments. Provided blocks:
19 *
20 * - **Channels**: SDMEC (errors-and-erasures over any 𝔽_q), SDMC, BSC, BEC, BAC, AWGN, BI-AWGN.
21 * - **Modulation**: NRZ and BPSK with configurable constellation.
22 * - **Demodulation**: hard-decision (NRZDemapper, BPSKDemapper) and soft-decision (LLRCalculator).
23 * - **Field multiplexing**: DEMUX/MUX between an extension field and a subfield.
24 * - **Chaining**: `operator>>` for left-to-right block composition.
25 *
26 * All blocks expose element-wise, vector, and matrix overloads via the @ref CECCO::details::BlockProcessor
27 * CRTP base; see its documentation for the canonical chain example.
28 *
29 * @see @ref CECCO::details::BlockProcessor — CRTP foundation and chain example
30 * @see @ref CECCO::SubfieldOf, @ref CECCO::ExtensionOf — concepts behind DEMUX/MUX
31 */
32
33#ifndef BLOCKS_HPP
34#define BLOCKS_HPP
35
36#include <numbers>
37
38#include "fields.hpp"
39#include "vectors.hpp"
40
41/*
42// transitive
43#include <algorithm>
44#include <cmath>
45#include <complex>
46#include <ios>
47#include <random>
48#include <stdexcept>
49#include <string>
50#include <type_traits>
51#include <utility>
52
53#include "field_concepts_traits.hpp"
54#include "matrices.hpp"
55*/
56
57namespace CECCO {
58
59namespace details {
60
61/**
62 * @brief CRTP base providing element-wise, vector, and matrix `operator()` overloads
63 * @tparam T Derived class (CRTP)
64 * @tparam InputType Element type accepted by the block
65 * @tparam OutputType Element type produced by the block
66 *
67 * Derived blocks implement a single-element `operator()(const InputType&)`; this base then
68 * generates `Vector<OutputType> operator()(const Vector<InputType>&)` and the matrix
69 * counterpart (plus rvalue versions, with in-place reuse when InputType == OutputType).
70 *
71 * Catch-all overloads accept any other @ref CECCO::FiniteFieldType and throw at runtime —
72 * useful inside `if constexpr (std::is_same_v<F, InputType>)` guards in non-template
73 * contexts, where the discarded branch must still compile.
74 *
75 * @code{.cpp}
76 * Vector<Fp<2>> message = {1, 0, 1, 1};
77 * BPSKMapper map;
78 * AWGN awgn(6.0, map.get_a(), map.get_b());
79 * BPSKDemapper demap;
80 *
81 * Vector<Fp<2>> r;
82 * message >> map >> awgn >> demap >> r;
83 * @endcode
84 */
85template <typename T, typename InputType, typename OutputType>
86class BlockProcessor : private NonCopyable {
87 protected:
88 const T& derived() const noexcept { return static_cast<const T&>(*this); }
89
90 T& derived() noexcept { return static_cast<T&>(*this); }
91
92 public:
93 /**
94 * @brief Apply the block element-wise to each vector entry
95 * @param in Input vector
96 * @return Vector of processed outputs
97 */
99 Vector<OutputType> res(in.get_n());
100 for (size_t i = 0; i < in.get_n(); ++i) res.set_component(i, derived()(in[i]));
101 return res;
102 }
103
104 /**
105 * @brief Apply the block element-wise (rvalue input)
106 * @param in Input vector (moved)
107 * @return Vector of processed outputs
108 */
110 const size_t n = in.get_n();
111 Vector<OutputType> res(n);
112 for (size_t i = 0; i < n; ++i) res.set_component(i, derived()(std::move(in[i])));
113 return res;
114 }
115
116 /**
117 * @brief Apply the block element-wise to each matrix entry
118 * @param in Input matrix
119 * @return Matrix of processed outputs
120 */
122 Matrix<OutputType> res(in.get_m(), in.get_n());
123 for (size_t i = 0; i < res.get_m(); ++i) {
124 for (size_t j = 0; j < res.get_n(); ++j) res.set_component(i, j, derived()(in(i, j)));
125 }
126 return res;
127 }
128
129 /**
130 * @brief Apply the block element-wise (rvalue input)
131 * @param in Input matrix (moved)
132 * @return Matrix of processed outputs (reuses input storage when InputType == OutputType)
133 */
135 if constexpr (std::is_same_v<InputType, OutputType>) {
136 // Same type: process in-place and reuse storage
137 for (size_t i = 0; i < in.get_m(); ++i) {
138 for (size_t j = 0; j < in.get_n(); ++j) in.set_component(i, j, derived()(std::move(in(i, j))));
139 }
140 return std::move(in);
141 } else {
142 // Different types: create new matrix
143 Matrix<OutputType> res(in.get_m(), in.get_n());
144 for (size_t i = 0; i < res.get_m(); ++i) {
145 for (size_t j = 0; j < res.get_n(); ++j) res.set_component(i, j, derived()(std::move(in(i, j))));
146 }
147 return res;
148 }
149 }
150
151 /**
152 * @brief Catch-all for inputs whose element type differs from `InputType`
153 * @tparam U Any @ref CECCO::FiniteFieldType other than `InputType`
154 *
155 * Always throws `std::logic_error` at runtime. See the class doc for the `if constexpr`
156 * use case that motivates these overloads.
157 */
158 template <FiniteFieldType U>
159 requires(!std::is_same_v<U, InputType>)
161 throw std::logic_error("BlockProcessor can only accept inputs from " + InputType::get_info());
162 }
163
164 template <FiniteFieldType U>
167 throw std::logic_error("BlockProcessor can only accept inputs from " + InputType::get_info());
168 }
169
170 template <FiniteFieldType U>
173 throw std::logic_error("BlockProcessor can only accept inputs from " + InputType::get_info());
174 }
175};
176
177/** @brief CRTP base for blocks where input and output element types coincide. */
178template <typename T, typename ElementType>
179using SameTypeProcessor = BlockProcessor<T, ElementType, ElementType>;
180
181/** @brief CRTP base for blocks mapping bits to complex symbols (e.g. NRZ/BPSK modulation). */
182template <typename T>
183using EncoderProcessor = BlockProcessor<T, Fp<2>, std::complex<double>>;
184
185/** @brief CRTP base for blocks mapping complex symbols back to bits (hard demod). */
186template <typename T>
187using DecoderProcessor = BlockProcessor<T, std::complex<double>, Fp<2>>;
188
189/** @brief CRTP base for blocks producing log-likelihood ratios from complex symbols. */
190template <typename T>
191using LLRProcessor = BlockProcessor<T, std::complex<double>, double>;
192
193} // namespace details
194
195/**
196 * @brief Symmetric Discrete Memoryless Erasure Channel (SDMEC) over any finite field 𝔽_q
197 * @tparam T Finite field type for channel input/output symbols
198 *
199 * q-ary symmetric channel with independent error and erasure processes. Each transmitted
200 * symbol is changed to a uniformly random different value with observed probability pe,
201 * marked erased (overwriting any error) with observed probability px, and otherwise passed
202 * through unchanged.
203 *
204 * For px = 0 this reduces to the traditional symmetric channel (use @ref CECCO::SDMC, or
205 * @ref CECCO::BSC for q = 2). For pe = 0 it is the erasure channel (use @ref CECCO::BEC for
206 * q = 2). Erasure support requires the @ref CECCO_ERASURE_SUPPORT macro at compile time.
207 *
208 * Capacity (bits/symbol):
209 * C = (1 − px) · [ log₂(q) + (1 − p̃)·log₂(1 − p̃) + p̃·log₂(p̃) − p̃·log₂(q − 1) ],
210 * where p̃ = pe / (1 − px) is the conditional error probability.
211 *
212 * @code{.cpp}
213 * using F4 = Ext<Fp<2>, {1, 1, 1}>;
214 * SDMEC<F4> channel(0.05, 0.1); // 5% errors, 10% erasures
215 * Vector<F4> r = Vector<F4>(20).randomize() >> channel;
216 * double C = channel.get_capacity();
217 * @endcode
218 *
219 * @see @ref CECCO::SDMC, @ref CECCO::BSC, @ref CECCO::BEC, @ref CECCO::BAC
220 */
221template <FieldType T>
223 public:
224 // Bring base class operator() overloads into scope
226
227 /**
228 * @brief Construct with error probability pe and erasure probability px
229 * @param pe Symbol error probability ∈ [0, 1]
230 * @param px Symbol erasure probability ∈ [0, 1 − pe]; default 0
231 *
232 * @throws std::out_of_range if pe ∉ [0, 1] or px ∉ [0, 1 − pe]
233 * @throws std::invalid_argument if px ≠ 0 and @ref CECCO_ERASURE_SUPPORT is not defined
234 */
235 SDMEC(double pe, double px = 0.0);
236
237 /**
238 * @brief Process one symbol
239 * @param in Input symbol
240 * @return Output symbol (possibly erased when @ref CECCO_ERASURE_SUPPORT is defined)
241 */
242 T operator()(const T& in);
243
244 /** @brief Observed symbol error probability pe. */
245 double get_pe() const noexcept { return error_dist.p(); }
246
247 /** @brief Observed symbol erasure probability px. */
248 double get_px() const noexcept { return erasure_dist.p(); }
249
250 /**
251 * @brief Shannon capacity in bits per symbol
252 * @return C = (1 − px) · [log₂(q) + (1 − p̃)·log₂(1 − p̃) + p̃·log₂(p̃) − p̃·log₂(q − 1)],
253 * where p̃ = pe / (1 − px). Edge cases pe ∈ {0, 1} use lim_{x→0} x·log₂(x) = 0.
254 */
255 double get_capacity() const noexcept
257
258 /**
259 * @brief Bhattacharyya parameter γ = 2·√(pe·(1 − pe))
260 *
261 * @throws std::logic_error if px ≠ 0 (γ is undefined for channels with erasures)
262 *
263 * @note Defined only for binary channels (q = 2).
264 */
265 long double get_Bhattacharyya_param() const
266 requires(std::is_same_v<T, Fp<2>>)
267 {
268 if (get_px() != 0.0)
269 throw std::logic_error("Bhattacharyya parameter is not defined for channels with erasures");
270 const long double pe = get_pe();
271 return 2.0L * std::sqrtl(pe * (1.0L - pe));
272 }
273
274 private:
275 std::geometric_distribution<unsigned long long> error_dist;
276 unsigned long long error_trials{0};
277 unsigned long long error_failures_before_hit;
278 std::geometric_distribution<unsigned long long> erasure_dist;
279 unsigned long long erasure_trials{0};
280 unsigned long long erasure_failures_before_hit;
281};
282
283template <FieldType T>
284SDMEC<T>::SDMEC(double pe, double px) {
285#ifndef CECCO_ERASURE_SUPPORT
286 if (px != 0.0) throw std::invalid_argument("px != 0 requires CECCO_ERASURE_SUPPORT");
287#endif
288 if (pe < 0.0 || pe > 1.0)
289 throw std::out_of_range("SDMEC error probability must be in [0,1], got: " + std::to_string(pe));
290 if (px < 0.0 || px > 1.0 - pe)
291 throw std::out_of_range("SDMEC erasure probability must be in [0," + std::to_string(1.0 - pe) +
292 "], got: " + std::to_string(px));
293
294 error_dist = std::geometric_distribution<unsigned long long>(pe / (1.0 - px));
295 error_failures_before_hit = error_dist(gen());
296 erasure_dist = std::geometric_distribution<unsigned long long>(px);
297 if (px > 0.0) {
298 erasure_failures_before_hit = erasure_dist(gen());
299 }
300}
301
302template <FieldType T>
303T SDMEC<T>::operator()(const T& in) {
304 if (error_dist.p() == 0.0 && erasure_dist.p() == 0.0) return in;
305 T res(in);
306 if (error_trials == error_failures_before_hit) {
307 res.randomize_force_change();
308 error_trials = 0;
309 error_failures_before_hit = error_dist(gen());
310 } else {
311 ++error_trials;
312 }
313#ifdef CECCO_ERASURE_SUPPORT
314 if (erasure_dist.p() == 0.0) return res;
315 if (erasure_trials == erasure_failures_before_hit) {
316 res.erase();
317 erasure_trials = 0;
318 erasure_failures_before_hit = erasure_dist(gen());
319 } else {
320 ++erasure_trials;
321 }
322#endif
323 return res;
324}
325
326template <FieldType T>
327double SDMEC<T>::get_capacity() const noexcept
328 requires(FiniteFieldType<T>)
329{
330 const double pe = error_dist.p();
331 const double q = static_cast<double>(T::get_size());
332
333 const double term1 = (pe > 0.0 && pe < 1.0) ? pe * std::log2(pe) : 0.0;
334 const double term2 = (pe > 0.0 && pe < 1.0) ? (1 - pe) * std::log2(1 - pe) : 0.0;
335
336 double res = std::log2(q) + term2 + term1 - pe * std::log2(q - 1);
337
338 const double px = erasure_dist.p();
339 res *= (1 - px);
340
341 return res;
342}
343
344/**
345 * @brief Symmetric Discrete Memoryless Channel — errors only, no erasures
346 * @tparam T Finite field type for channel input/output symbols
347 *
348 * Convenience wrapper for `SDMEC<T>(pe, 0.0)` with a single-parameter constructor.
349 *
350 * @see @ref CECCO::SDMEC for the full errors-and-erasures channel
351 * @see @ref CECCO::BSC for the binary case
352 */
353template <FieldType T>
354class SDMC : public SDMEC<T> {
355 public:
356 /**
357 * @brief Construct with symbol error probability pe
358 * @param pe Symbol error probability ∈ [0, 1]
359 */
360 SDMC(double pe) : SDMEC<T>(pe, 0.0) {}
361};
362
363/**
364 * @brief Binary Symmetric Channel — type alias for `SDMC<Fp<2>>`
365 *
366 * Bits are flipped with probability pe.
367 *
368 * @see @ref CECCO::SDMC, @ref CECCO::BEC
369 */
370using BSC = SDMC<Fp<2>>;
371
372/**
373 * @brief Binary Erasure Channel — symbols are received correctly or marked erased
374 *
375 * Convenience wrapper for `SDMEC<Fp<2>>(0.0, px)`. Requires @ref CECCO_ERASURE_SUPPORT.
376 *
377 * @see @ref CECCO::SDMEC, @ref CECCO::BSC
378 */
379class BEC : public SDMEC<Fp<2>> {
380 public:
381 /**
382 * @brief Construct with symbol erasure probability px
383 * @param px Symbol erasure probability ∈ [0, 1]
384 */
385 BEC(double px) : SDMEC<Fp<2>>(0.0, px) {}
386};
387
388/**
389 * @brief Binary Asymmetric Channel (Z-channel) — 0 is preserved; 1 flips to 0 with probability p
390 *
391 * @code{.cpp}
392 * BAC bac(0.1); // 10% probability that 1 flips to 0
393 * Vector<Fp<2>> c = {0, 1, 1, 0};
394 * Vector<Fp<2>> r = c >> bac;
395 * double C = bac.get_capacity();
396 * @endcode
397 *
398 * @see @ref CECCO::BSC, @ref CECCO::BEC
399 */
400class BAC : public details::SameTypeProcessor<BAC, Fp<2>> {
401 public:
402 using details::SameTypeProcessor<BAC, Fp<2>>::operator();
403
404 /**
405 * @brief Construct with flip probability p
406 * @param p Probability that 1 → 0, p ∈ [0, 1]
407 *
408 * @throws std::out_of_range if p ∉ [0, 1] or 0 < p < 1e−9
409 *
410 * @note p = 0 is the perfect channel (capacity 1); p = 1 makes only 0 transmissible (capacity 0).
411 */
412 BAC(double p) : bsc(p) {}
413
414 /**
415 * @brief Process one bit
416 * @param in Input bit
417 * @return Output: 0 is preserved; 1 flips to 0 with probability p
418 */
419 Fp<2> operator()(const Fp<2>& in) {
420 if (in == Fp<2>(0)) return in;
421 return bsc(in);
422 }
423
424 /** @brief Flip probability p (probability that 1 → 0). */
425 double get_pe() const noexcept { return bsc.get_pe(); }
426
427 /**
428 * @brief Shannon capacity in bits per symbol
429 * @return C = log₂(1 + (1 − p)·p^{p/(1−p)}); edge cases p ∈ {0, 1} handled explicitly.
430 */
431 double get_capacity() const noexcept {
432 const double pe = bsc.get_pe();
433
434 if (pe == 0.0) return 1.0;
435 if (pe == 1.0) return 0.0;
436
437 return std::log2(1 + (1 - pe) * std::pow(pe, pe / (1 - pe)));
438 }
439
440 private:
441 BSC bsc;
442};
443
444/**
445 * @brief Non-Return-to-Zero (NRZ) mapper for binary modulation
446 *
447 * Maps 𝔽_2 symbols to real constellation points (a − b/2, 0) and (a + b/2, 0), where `a` is
448 * the DC offset and `b` is the constellation distance. Energy per bit is Eb = a² + b²/4
449 * (unit symbol duration assumed).
450 *
451 * Typical configurations: BPSK (a = 0, b = 2, constellation {−1, +1}, Eb = 1) and
452 * OOK (a = 1, b = 2, constellation {0, +2}, Eb = 2).
453 *
454 * @code{.cpp}
455 * NRZMapper ook(1.0, 2.0);
456 * Vector<Fp<2>> bits = {0, 1, 0, 1};
457 * Vector<std::complex<double>> signal = bits >> ook; // (0, 2, 0, 2)
458 * @endcode
459 *
460 * @see @ref CECCO::BPSKMapper, @ref CECCO::AWGN, @ref CECCO::NRZDemapper
461 */
463 public:
464 // Bring base class operator() overloads into scope
466
467 /**
468 * @brief Construct with constellation parameters
469 * @param a DC offset (real-axis shift)
470 * @param b Constellation distance (separation between symbols)
471 */
472 constexpr NRZMapper(double a, double b) noexcept : a(a), b(b) {}
473
474 /** @name Constellation Parameters
475 * @{
476 */
477
478 /** @brief Energy per bit Eb = a² + b²/4. */
479 constexpr double get_Eb() const noexcept { return (a * a) + (b * b) / 4.0; }
480
481 /** @brief DC offset parameter `a`. */
482 constexpr double get_a() const noexcept { return a; }
483
484 /** @brief Constellation distance parameter `b`. */
485 constexpr double get_b() const noexcept { return b; }
486
487 /** @} */
488
489 /**
490 * @brief Map a bit to its constellation point
491 * @param in Binary input
492 * @return (a − b/2, 0) for input 0; (a + b/2, 0) for input 1
493 */
494 constexpr std::complex<double> operator()(const Fp<2>& in) noexcept {
495 if (in == Fp<2>(0))
496 return {a - b / 2.0, 0};
497 else
498 return {a + b / 2.0, 0};
499 }
500
501 private:
502 const double a{};
503 const double b{};
504};
505
506/**
507 * @brief Binary Phase Shift Keying mapper — `NRZMapper` with a = 0, b = 2
508 *
509 * Antipodal constellation {−1, +1}, Eb = 1.
510 *
511 * @see @ref CECCO::NRZMapper, @ref CECCO::BPSKDemapper
512 */
513class BPSKMapper : public NRZMapper {
514 public:
515 /** @brief Construct BPSK mapper (a = 0, b = 2). */
516 constexpr BPSKMapper() noexcept : NRZMapper(0.0, 2.0) {}
517};
518
519/**
520 * @brief Additive White Gaussian Noise channel for complex-valued symbols
521 *
522 * Adds independent 𝒩(0, σ²) noise to the real and imaginary parts of each input symbol.
523 * The noise variance is derived from the SNR Eb/N₀ and the constellation parameters
524 * supplied at construction: σ² = Eb / (2 · 10^(EbN0dB/10)).
525 *
526 * For NRZ/BPSK input the bit error probability after a hard decision is
527 * Pe = ½ · erfc(√( b²·Eb/N₀ / (4·Eb) )),
528 * available via @ref get_pe.
529 *
530 * @code{.cpp}
531 * BPSKMapper map;
532 * AWGN awgn(6.0, map.get_a(), map.get_b()); // 6 dB Eb/N₀
533 * Vector<Fp<2>> message = {0, 1, 1, 0};
534 * Vector<std::complex<double>> y = message >> map >> awgn;
535 * @endcode
536 *
537 * @see @ref CECCO::NRZMapper, @ref CECCO::BPSKMapper, @ref CECCO::BI_AWGN, @ref CECCO::LLRCalculator
538 */
539class AWGN : public details::SameTypeProcessor<AWGN, std::complex<double>> {
540 public:
541 // Bring base class operator() overloads into scope
542 using details::SameTypeProcessor<AWGN, std::complex<double>>::operator();
543
544 /**
545 * @brief Construct with SNR and constellation parameters
546 * @param EbNodB SNR Eb/N₀ in dB
547 * @param a DC offset of the source constellation
548 * @param b Constellation distance of the source constellation
549 */
550 AWGN(double EbNodB, double a, double b)
551 : Eb(NRZMapper(a, b).get_Eb()),
552 dist(0, std::sqrt(0.5 * Eb / std::pow(10.0, EbNodB / 10.0))),
553 pe(calculate_pe(EbNodB, Eb, b)) {}
554
555 /** @name Noise Parameters
556 * @{
557 */
558
559 /** @brief Noise variance σ² (per real component). */
560 double get_variance() const noexcept {
561 const double s = dist.stddev();
562 return s * s;
563 }
564
565 /** @brief Noise standard deviation σ (per real component). */
566 double get_standard_deviation() const noexcept { return dist.stddev(); }
567
568 /** @brief Theoretical hard-decision bit error probability for the configured constellation/SNR. */
569 constexpr double get_pe() const noexcept { return pe; }
570
571 /** @} */
572
573 /**
574 * @brief Add Gaussian noise to a complex symbol
575 * @param in Input symbol
576 * @return Symbol with independent Gaussian noise added to each real component
577 */
578 std::complex<double> operator()(const std::complex<double>& in) {
579 std::complex<double> res(in.real() + dist(gen()), in.imag() + dist(gen()));
580 return res;
581 }
582
583 /**
584 * @brief Shannon capacity in bits per symbol (real signaling)
585 * @return C = ½·log₂(1 + Eb/σ²) for one real dimension (NRZ/BPSK).
586 *
587 * For complex signaling with noise on both I and Q the formula is C = log₂(1 + Eb/(2σ²)).
588 */
589 double get_capacity() const noexcept { return 1 / 2.0 * std::log2(1 + Eb / get_variance()); }
590
591 private:
592 const double Eb{};
593 std::normal_distribution<double> dist;
594 const double pe{};
595
596 /**
597 * @brief Closed-form NRZ/BPSK hard-decision Pe
598 * @param EbNodB SNR Eb/N₀ in dB
599 * @param Eb Energy per bit of the source constellation
600 * @param b Constellation distance
601 * @return Pe = ½·erfc(√( b²·10^(EbNodB/10) / (4·Eb) ))
602 */
603 static double calculate_pe(double EbNodB, double Eb, double b) noexcept;
604};
605
606double AWGN::calculate_pe(double EbNodB, double Eb, double b) noexcept {
607 // For NRZ/BPSK: Pe = 0.5 * erfc(b/(2*sigma))
608 // where b is constellation distance and sigma is noise std dev
609 const double EbN0_linear = std::pow(10.0, EbNodB / 10.0);
610
611 // Signal-to-noise ratio in terms of constellation distance
612 // SNR = (b/2)² / σ² = (b/2)² / (No/2) = (b/2)² * 2/No = (b²/2) / No
613 // where No = Eb/EbN0_linear
614 const double constellation_snr = (b * b * EbN0_linear) / (4.0 * Eb);
615
616 return 0.5 * std::erfc(std::sqrt(constellation_snr));
617}
618
619/**
620 * @brief Binary-Input AWGN — fused @ref NRZMapper + @ref AWGN block
621 *
622 * Maps binary inputs through an internal NRZMapper and then through AWGN, yielding noisy
623 * complex symbols ready for hard decision (@ref NRZDemapper) or soft decision
624 * (@ref LLRCalculator). Default parameters give BPSK (a = 0, b = 2).
625 *
626 * @code{.cpp}
627 * BI_AWGN channel(6.0); // BPSK at 6 dB
628 * Vector<Fp<2>> c = {1, 0, 1, 0};
629 * Vector<Fp<2>> r = c >> channel >> NRZDemapper(channel); // hard decisions
630 * @endcode
631 *
632 * @see @ref CECCO::AWGN, @ref CECCO::NRZDemapper, @ref CECCO::LLRCalculator
633 */
634class BI_AWGN : public details::BlockProcessor<BI_AWGN, Fp<2>, std::complex<double>> {
635 public:
636 using details::BlockProcessor<BI_AWGN, Fp<2>, std::complex<double>>::operator();
637
638 /**
639 * @brief Construct with SNR and constellation parameters
640 * @param EbN0dB SNR Eb/N₀ in dB
641 * @param a NRZ DC offset (default 0 for BPSK)
642 * @param b NRZ constellation distance (default 2 for BPSK)
643 */
644 BI_AWGN(double EbN0dB, double a = 0.0, double b = 2.0) : encoder(a, b), transmission(EbN0dB, a, b) {}
645
646 /**
647 * @brief Map a bit and add noise
648 * @param in Input bit
649 * @return NRZ symbol corrupted by AWGN
650 */
651 std::complex<double> operator()(const Fp<2>& in) { return transmission(encoder(in)); }
652
653 /** @brief Internal NRZ mapper (for constructing matching demappers/LLR calculators). */
654 const NRZMapper& get_encoder() const noexcept { return encoder; }
655
656 /** @brief Internal AWGN block (for noise statistics). */
657 const AWGN& get_transmission() const noexcept { return transmission; }
658
659 /**
660 * @brief Shannon capacity in bits per symbol
661 * @return C ∈ [0, 1], computed by composite Simpson's rule (no closed form for BI-AWGN).
662 *
663 * Returns 0 if the constellation distance is 0 and 1 if the noise vanishes.
664 */
665 double get_capacity() const noexcept;
666
667 /** @brief Theoretical hard-decision bit error probability for this constellation/SNR. */
668 constexpr double get_pe() const noexcept { return transmission.get_pe(); }
669
670 /** @brief Noise standard deviation σ. */
671 double get_sigma() const noexcept { return transmission.get_standard_deviation(); }
672
673 /**
674 * @brief Bhattacharyya parameter γ = exp(−b²/(8σ²))
675 */
676 long double get_Bhattacharyya_param() const noexcept {
677 const long double b = encoder.get_b();
678 const long double sigma = transmission.get_standard_deviation();
679 return std::expl(-(b * b) / (8.0L * sigma * sigma));
680 }
681
682 private:
683 NRZMapper encoder;
684 AWGN transmission;
685};
686
687double BI_AWGN::get_capacity() const noexcept {
688 const double a = encoder.get_a();
689 const double b = encoder.get_b();
690 const double sigma = transmission.get_standard_deviation();
691
692 if (b == 0.0) return 0.0; // no separation -> capacity zero
693 if (sigma == 0.0) return 1.0; // noiseless -> capacity one
694
695 auto f_Y_star = [a, b, sigma](double x) -> double {
696 double m = 1.0 / (2.0 * sigma * std::sqrt(2.0 * std::numbers::pi));
697 double s0 = std::exp(-0.5 * std::pow((x - (a - b / 2.0)) / sigma, 2.0));
698 double s1 = std::exp(-0.5 * std::pow((x - (a + b / 2.0)) / sigma, 2.0));
699 return m * (s0 + s1);
700 };
701
702 auto g = [&f_Y_star](double x) -> double {
703 double fy = f_Y_star(x);
704 if (fy <= 0.0) return 0.0; // avoid log2(0)
705 return fy * std::log2(fy);
706 };
707
708 const double K = 8.0;
709 const double mu0 = a - b / 2.0;
710 const double mu1 = a + b / 2.0;
711 const double lb = std::min(mu0, mu1) - K * sigma;
712 const double ub = std::max(mu0, mu1) + K * sigma;
713
714 // ---- composite Simpson's rule on [lb, ub] ----
715 const int N = 16000; // must be even, tune for accuracy/speed
716 const double h = (ub - lb) / N;
717
718 double sum = g(lb) + g(ub);
719 for (int i = 1; i < N; ++i) {
720 double x = lb + i * h;
721 sum += (i % 2 == 0 ? 2.0 : 4.0) * g(x);
722 }
723
724 const double integral = h * sum / 3.0; // ≈ ∫ g(x) dx
725
726 return std::clamp(-integral - std::log2(sigma * std::sqrt(2 * std::numbers::pi * std::numbers::e)), 0.0, 1.0);
727}
728
729/**
730 * @brief Non-Return-to-Zero (NRZ) hard-decision demapper
731 *
732 * Maximum-likelihood threshold detector for NRZ-modulated symbols received over AWGN:
733 * outputs 1 if Re(r) ≥ a, otherwise 0, where `a` is the DC offset of the corresponding
734 * @ref CECCO::NRZMapper. The imaginary part is ignored.
735 *
736 * @code{.cpp}
737 * NRZMapper map(0.5, 1.0); // Constellation: {0, 1}
738 * NRZDemapper demap(map); // Threshold at a = 0.5
739 * Vector<std::complex<double>> y = {{0.3, 0.1}, {0.8, −0.2}};
740 * Vector<Fp<2>> c_est = y >> demap; // {0, 1}
741 * @endcode
742 *
743 * @see @ref CECCO::NRZMapper, @ref CECCO::BPSKDemapper, @ref CECCO::LLRCalculator
744 */
746 public:
747 // Bring base class operator() overloads into scope
749
750 /**
751 * @brief Construct from the corresponding mapper
752 * @param nrz Source mapper whose DC offset becomes the decision threshold
753 */
754 constexpr NRZDemapper(const NRZMapper& nrz) noexcept : a(nrz.get_a()) {}
755
756 /**
757 * @brief Construct from a BI-AWGN channel (convenience)
758 * @param bi_awgn Channel whose internal mapper provides the decision threshold
759 */
760 NRZDemapper(const BI_AWGN& bi_awgn) noexcept : a(bi_awgn.get_encoder().get_a()) {}
761
762 /**
763 * @brief Hard-decide a received symbol
764 * @param in Received complex value (imaginary part ignored)
765 * @return 1 if Re(in) ≥ a, else 0
766 */
767 constexpr Fp<2> operator()(const std::complex<double>& in) noexcept {
768 if (in.real() >= a)
769 return {1};
770 else
771 return {0};
772 }
773
774 private:
775 const double a{}; ///< Decision threshold (DC offset from mapper)
776};
777
778/**
779 * @brief BPSK hard-decision demapper — `NRZDemapper` with threshold 0
780 *
781 * @see @ref CECCO::BPSKMapper, @ref CECCO::NRZDemapper
782 */
783class BPSKDemapper : public NRZDemapper {
784 public:
785 /** @brief Construct BPSK demapper (threshold 0). */
786 constexpr BPSKDemapper() noexcept : NRZDemapper(BPSKMapper()) {}
787};
788
789/**
790 * @brief Log-Likelihood Ratio calculator for NRZ-over-AWGN soft demodulation
791 *
792 * Computes LLR(r) = b·(a − Re(r)) / σ² in nats, where (a, b) are the NRZ constellation
793 * parameters and σ² is the AWGN noise variance. Sign convention: positive LLR ⇒ bit 0,
794 * negative ⇒ bit 1; magnitude indicates reliability. Output suitable for belief
795 * propagation, LDPC, turbo, and other soft-decision decoders.
796 *
797 * Construct from a matching @ref NRZMapper + @ref AWGN pair, or directly from a
798 * @ref BI_AWGN. See the @ref BI_AWGN class doc for an end-to-end chain example.
799 *
800 * @see @ref CECCO::NRZDemapper for the hard-decision counterpart
801 */
803 public:
804 // Bring base class operator() overloads into scope
806
807 /**
808 * @brief Construct from a matching mapper and channel
809 * @param nrz Source mapper providing (a, b)
810 * @param transmission Channel providing σ
811 */
812 LLRCalculator(const NRZMapper& nrz, const AWGN& transmission) noexcept
813 : a(nrz.get_a()), b(nrz.get_b()), sigmasq(std::pow(transmission.get_standard_deviation(), 2.0)) {}
814
815 /**
816 * @brief Construct from a BI-AWGN channel (convenience)
817 * @param bi_awgn Channel providing (a, b) and σ
818 */
819 LLRCalculator(const BI_AWGN& bi_awgn) noexcept
820 : a(bi_awgn.get_encoder().get_a()),
821 b(bi_awgn.get_encoder().get_b()),
822 sigmasq(std::pow(bi_awgn.get_transmission().get_standard_deviation(), 2.0)) {}
823
824 /**
825 * @brief Compute LLR of a received symbol
826 * @param in Received complex symbol
827 * @return LLR = b·(a − Re(in)) / σ² in nats; positive ⇒ bit 0, negative ⇒ bit 1
828 */
829 double operator()(const std::complex<double>& in) noexcept { return b * (a - in.real()) / sigmasq; }
830
831 private:
832 const double a{}; ///< DC offset from mapper
833 const double b{}; ///< Constellation distance from mapper
834 const double sigmasq{}; ///< Noise variance from channel
835};
836
837/**
838 * @brief Field demultiplexer — expand 𝔽_E elements into 𝔽_S coefficient vectors/matrices
839 * @tparam E Extension field
840 * @tparam S Subfield of E (S ⊆ E)
841 *
842 * Each E element decomposes into [E:S] coefficients over S via the polynomial basis of the
843 * extension. For a `Vector<E>` of length n, the resulting `Matrix<S>` has [E:S] rows and n
844 * columns; column j holds the coefficients of element j.
845 *
846 * @code{.cpp}
847 * using F2 = Fp<2>;
848 * using F4 = Ext<F2, {1, 1, 1}>; // 𝔽_4 = 𝔽_2[x]/(x² + x + 1)
849 * DEMUX<F4, F2> demux;
850 *
851 * Vector<F4> v = {F4(0), F4(1), F4(2), F4(3)};
852 * Matrix<F2> M = v >> demux; // 2×4 matrix over 𝔽_2
853 * @endcode
854 *
855 * @see @ref CECCO::MUX for the inverse
856 * @see @ref CECCO::Ext, @ref CECCO::SubfieldOf
857 */
858template <FiniteFieldType E, FiniteFieldType S>
859 requires(SubfieldOf<E, S>)
860class DEMUX : private details::NonCopyable {
861 public:
862 constexpr DEMUX() = default;
863
864 /**
865 * @brief Expand a single element into its subfield-coefficient vector
866 * @param in Extension field element
867 * @return Length-[E:S] vector of S coefficients
868 */
869 constexpr Vector<S> operator()(const E& in) { return in.template as_vector<S>(); }
870
871 /**
872 * @brief Expand a vector of elements into a subfield-coefficient matrix
873 * @param in Vector of n extension field elements
874 * @return [E:S] × n matrix; column j holds the coefficients of `in[j]`
875 */
876 Matrix<S> operator()(const Vector<E>& in);
877};
878
879template <FiniteFieldType E, FiniteFieldType S>
880 requires(SubfieldOf<E, S>)
881Matrix<S> DEMUX<E, S>::operator()(const Vector<E>& in) {
882 if (in.get_n() == 0) return Matrix<S>(0, 0);
883
884 auto temp = in[0].template as_vector<S>();
885 Matrix<S> M(temp.get_n(), in.get_n());
886
888
889 for (size_t i = 1; i < in.get_n(); ++i) {
890 auto col_vector = in[i].template as_vector<S>();
892 }
893
894 return M;
895}
896
897/**
898 * @brief Field multiplexer — reconstruct 𝔽_E elements from 𝔽_S coefficients
899 * @tparam S Subfield
900 * @tparam E Extension of S
901 *
902 * Inverse of @ref DEMUX. The input matrix must have [E:S] rows; each column is interpreted
903 * as the coefficient vector of one E element, and the output `Vector<E>` has one entry per
904 * input column. `original >> DEMUX{} >> MUX{} == original` for compatible field pairs.
905 *
906 * @see @ref CECCO::DEMUX for the inverse direction and a usage example
907 * @see @ref CECCO::Ext, @ref CECCO::ExtensionOf
908 */
911class MUX : private details::NonCopyable {
912 public:
913 constexpr MUX() = default;
914
915 /**
916 * @brief Reconstruct an element from its subfield-coefficient vector
917 * @param in Length-[E:S] coefficient vector
918 * @return Reconstructed extension field element
919 */
920 constexpr E operator()(const Vector<S>& in) { return E(in); }
921
922 /**
923 * @brief Reconstruct a vector of elements from a subfield-coefficient matrix
924 * @param in [E:S] × n matrix of S coefficients
925 * @return Length-n vector of E elements (column j of `in` becomes element j)
926 */
927 Vector<E> operator()(const Matrix<S>& in);
928};
929
930template <FiniteFieldType S, FiniteFieldType E>
931 requires(ExtensionOf<S, E>)
932Vector<E> MUX<S, E>::operator()(const Matrix<S>& in) {
933 Vector<E> v(in.get_n());
934 for (size_t i = 0; i < in.get_n(); ++i) v.set_component(i, E(in.get_col(i)));
935
936 return v;
937}
938
939/**
940 * @brief Function-call chaining: `x >> f` ≡ `f(x)`
941 * @tparam LHS Input type
942 * @tparam RHS Callable on `LHS`
943 * @param lhs Input value (perfect-forwarded)
944 * @param rhs Callable (perfect-forwarded)
945 * @return `rhs(lhs)`
946 *
947 * Selected when `RHS` is invocable with `LHS`. Disabled for `std::ios_base`-derived types
948 * to avoid clashing with stream operators. The companion overload below handles the case
949 * where the right-hand side is an assignment target.
950 */
951template <class LHS, class RHS>
953 requires(RHS&& r, LHS&& x) { std::forward<RHS>(r)(std::forward<LHS>(x)); }
954decltype(auto) operator>>(LHS&& lhs, RHS&& rhs) {
955 return std::forward<RHS>(rhs)(std::forward<LHS>(lhs));
956}
957
958/**
959 * @brief Assignment chaining: `x >> dst` ≡ `dst = x; return dst`
960 * @tparam LHS Input type
961 * @tparam RHS Assignment target type (assignable from `LHS`)
962 * @param lhs Input value
963 * @param dst Destination variable
964 * @return Reference to `dst` for continued chaining
965 *
966 * Selected only when the callable overload above is not viable, so block calls always win
967 * over capture-into-variable when both are possible. Useful for capturing intermediate
968 * results in a chain (`c >> map >> x >> awgn >> y >> demap >> r;`).
969 */
970template <class LHS, class RHS>
972 (!requires(RHS&& r, LHS&& x) { std::forward<RHS>(r)(std::forward<LHS>(x)); }) &&
973 requires(RHS& dst, LHS&& x) { dst = std::forward<LHS>(x); }
975 dst = std::forward<LHS>(lhs);
976 return dst;
977}
978
979} // namespace CECCO
980
981#endif
Additive White Gaussian Noise channel for complex-valued symbols.
Definition blocks.hpp:539
double get_standard_deviation() const noexcept
Noise standard deviation σ (per real component).
Definition blocks.hpp:566
double get_capacity() const noexcept
Shannon capacity in bits per symbol (real signaling).
Definition blocks.hpp:589
constexpr double get_pe() const noexcept
Theoretical hard-decision bit error probability for the configured constellation/SNR.
Definition blocks.hpp:569
double get_variance() const noexcept
Noise variance σ² (per real component).
Definition blocks.hpp:560
AWGN(double EbNodB, double a, double b)
Construct with SNR and constellation parameters.
Definition blocks.hpp:550
std::complex< double > operator()(const std::complex< double > &in)
Add Gaussian noise to a complex symbol.
Definition blocks.hpp:578
Binary Asymmetric Channel (Z-channel) — 0 is preserved; 1 flips to 0 with probability p.
Definition blocks.hpp:400
double get_capacity() const noexcept
Shannon capacity in bits per symbol.
Definition blocks.hpp:431
BAC(double p)
Construct with flip probability p.
Definition blocks.hpp:412
Fp< 2 > operator()(const Fp< 2 > &in)
Process one bit.
Definition blocks.hpp:419
double get_pe() const noexcept
Flip probability p (probability that 1 → 0).
Definition blocks.hpp:425
Binary Erasure Channel — symbols are received correctly or marked erased.
Definition blocks.hpp:379
BEC(double px)
Construct with symbol erasure probability px.
Definition blocks.hpp:385
Binary-Input AWGN — fused NRZMapper + AWGN block.
Definition blocks.hpp:634
double get_capacity() const noexcept
Shannon capacity in bits per symbol.
Definition blocks.hpp:687
double get_sigma() const noexcept
Noise standard deviation σ.
Definition blocks.hpp:671
const AWGN & get_transmission() const noexcept
Internal AWGN block (for noise statistics).
Definition blocks.hpp:657
constexpr double get_pe() const noexcept
Theoretical hard-decision bit error probability for this constellation/SNR.
Definition blocks.hpp:668
const NRZMapper & get_encoder() const noexcept
Internal NRZ mapper (for constructing matching demappers/LLR calculators).
Definition blocks.hpp:654
long double get_Bhattacharyya_param() const noexcept
Bhattacharyya parameter γ = exp(−b²/(8σ²)).
Definition blocks.hpp:676
std::complex< double > operator()(const Fp< 2 > &in)
Map a bit and add noise.
Definition blocks.hpp:651
BI_AWGN(double EbN0dB, double a=0.0, double b=2.0)
Construct with SNR and constellation parameters.
Definition blocks.hpp:644
BPSK hard-decision demapper — NRZDemapper with threshold 0.
Definition blocks.hpp:783
constexpr BPSKDemapper() noexcept
Construct BPSK demapper (threshold 0).
Definition blocks.hpp:786
Binary Phase Shift Keying mapper — NRZMapper with a = 0, b = 2.
Definition blocks.hpp:513
constexpr BPSKMapper() noexcept
Construct BPSK mapper (a = 0, b = 2).
Definition blocks.hpp:516
Field demultiplexer — expand 𝔽_E elements into 𝔽_S coefficient vectors/matrices.
Definition blocks.hpp:860
constexpr DEMUX()=default
constexpr Vector< S > operator()(const E &in)
Expand a single element into its subfield-coefficient vector.
Definition blocks.hpp:869
Prime field 𝔽_p ≅ ℤ/pℤ
Definition fields.hpp:1647
Log-Likelihood Ratio calculator for NRZ-over-AWGN soft demodulation.
Definition blocks.hpp:802
LLRCalculator(const NRZMapper &nrz, const AWGN &transmission) noexcept
Construct from a matching mapper and channel.
Definition blocks.hpp:812
LLRCalculator(const BI_AWGN &bi_awgn) noexcept
Construct from a BI-AWGN channel (convenience).
Definition blocks.hpp:819
double operator()(const std::complex< double > &in) noexcept
Compute LLR of a received symbol.
Definition blocks.hpp:829
Non-Return-to-Zero (NRZ) hard-decision demapper.
Definition blocks.hpp:745
NRZDemapper(const BI_AWGN &bi_awgn) noexcept
Construct from a BI-AWGN channel (convenience).
Definition blocks.hpp:760
constexpr NRZDemapper(const NRZMapper &nrz) noexcept
Construct from the corresponding mapper.
Definition blocks.hpp:754
constexpr Fp< 2 > operator()(const std::complex< double > &in) noexcept
Hard-decide a received symbol.
Definition blocks.hpp:767
Non-Return-to-Zero (NRZ) mapper for binary modulation.
Definition blocks.hpp:462
constexpr std::complex< double > operator()(const Fp< 2 > &in) noexcept
Map a bit to its constellation point.
Definition blocks.hpp:494
constexpr double get_b() const noexcept
Constellation distance parameter b.
Definition blocks.hpp:485
constexpr double get_Eb() const noexcept
Energy per bit Eb = a² + b²/4.
Definition blocks.hpp:479
constexpr NRZMapper(double a, double b) noexcept
Construct with constellation parameters.
Definition blocks.hpp:472
constexpr double get_a() const noexcept
DC offset parameter a.
Definition blocks.hpp:482
Symmetric Discrete Memoryless Erasure Channel (SDMEC) over any finite field 𝔽_q.
Definition blocks.hpp:222
double get_pe() const noexcept
Observed symbol error probability pe.
Definition blocks.hpp:245
double get_px() const noexcept
Observed symbol erasure probability px.
Definition blocks.hpp:248
T operator()(const T &in)
Process one symbol.
Definition blocks.hpp:303
SDMEC(double pe, double px=0.0)
Construct with error probability pe and erasure probability px.
Definition blocks.hpp:284
long double get_Bhattacharyya_param() const
Bhattacharyya parameter γ = 2·√(pe·(1 − pe)).
Definition blocks.hpp:265
double get_capacity() const noexcept
Shannon capacity in bits per symbol.
Definition blocks.hpp:327
CRTP base providing element-wise, vector, and matrix operator() overloads.
Definition blocks.hpp:86
Vector< OutputType > operator()(Vector< InputType > &&in)
Apply the block element-wise (rvalue input).
Definition blocks.hpp:109
Vector< OutputType > operator()(const Vector< InputType > &in)
Apply the block element-wise to each vector entry.
Definition blocks.hpp:98
const T & derived() const noexcept
Definition blocks.hpp:88
Mixin that deletes the copy operations and defaults the move operations.
Contains implementation details not to be exposed to the user. Functions and classes here may change ...
Definition blocks.hpp:59
BlockProcessor< T, std::complex< double >, Fp< 2 > > DecoderProcessor
CRTP base for blocks mapping complex symbols back to bits (hard demod).
Definition blocks.hpp:187
BlockProcessor< T, ElementType, ElementType > SameTypeProcessor
CRTP base for blocks where input and output element types coincide.
Definition blocks.hpp:179
BlockProcessor< T, Fp< 2 >, std::complex< double > > EncoderProcessor
CRTP base for blocks mapping bits to complex symbols (e.g. NRZ/BPSK modulation).
Definition blocks.hpp:183
BlockProcessor< T, std::complex< double >, double > LLRProcessor
CRTP base for blocks producing log-likelihood ratios from complex symbols.
Definition blocks.hpp:191
Provides a framework for error correcting codes.
Definition blocks.hpp:57
std::mt19937 & gen()
Current thread's random number generator.
Definition helpers.hpp:127
decltype(auto) operator>>(LHS &&lhs, RHS &&rhs)
Function-call chaining: x >> f ≡ f(x).
Definition blocks.hpp:954
RHS & operator>>(LHS &&lhs, RHS &dst)
Assignment chaining: x >> dst ≡ dst = x; return dst.
Definition blocks.hpp:974