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
graphs.hpp
Go to the documentation of this file.
1/**
2 * @file graphs.hpp
3 * @brief Trellises and Tanner graphs
4 * @author Christian Senger <senger@inue.uni-stuttgart.de>
5 * @version 2.3.1
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 * Layered trellises and parity-check Tanner graphs used by the Viterbi, BCJR, and
18 * belief-propagation decoders in `codes.hpp`.
19 */
20
21#ifndef GRAPHS_HPP
22#define GRAPHS_HPP
23
24#include <memory>
25#include <variant>
26
27#include "fields.hpp"
28#include "vectors.hpp"
29
30/*
31// transitive
32#include <algorithm>
33#include <array>
34#include <cmath>
35#include <concepts>
36#include <cstddef>
37#include <cstdint>
38#include <fstream>
39#include <iomanip>
40#include <iostream>
41#include <limits>
42#include <optional>
43#include <ranges>
44#include <stdexcept>
45#include <string>
46#include <type_traits>
47#include <unordered_map>
48#include <utility>
49#include <vector>
50
51#include "field_concepts_traits.hpp"
52#include "helpers.hpp"
53*/
54
55namespace CECCO {
56
57namespace details {
58
59/**
60 * @brief Vertex in one trellis layer
61 *
62 * The `id` is also the position within its layer. Decoding workspaces index
63 * arrays by this value.
64 */
65template <FieldType T>
66struct Vertex {
67 /// @brief Construct a vertex with layer-local id `id`
68 explicit Vertex(uint32_t id) noexcept : id(id) {}
69
70 /// @brief Layer-local vertex id
71 uint32_t id;
72};
73
74/**
75 * @brief Edge between two adjacent trellis layers
76 *
77 * @tparam T Field type used for edge labels
78 */
79template <FieldType T>
80struct Edge {
81 /**
82 * @brief Construct an edge `from_id --value--> to_id`
83 *
84 * @param from_id Source vertex id in layer `s`
85 * @param to_id Target vertex id in layer `s + 1`
86 * @param value Field label carried by the edge
87 */
88 Edge(uint32_t from_id, uint32_t to_id, T value) : from_id(from_id), to_id(to_id), value(value) {}
89
90 /// @brief Source vertex id
91 uint32_t from_id;
92 /// @brief Target vertex id
93 uint32_t to_id;
94 /// @brief Edge label
96};
97
98/**
99 * @brief Edge from a check node to a variable node in a Tanner graph
100 *
101 * @tparam T Field type used for edge labels
102 */
103template <FieldType T>
104struct CheckEdge {
105 /**
106 * @brief Construct an edge to variable node `var_id` with label `value`
107 *
108 * @param var_id Incident variable-node id
109 * @param value Parity-check coefficient carried by the edge
110 */
111 CheckEdge(uint32_t var_id, T value) : var_id(var_id), value(value) {}
112
113 /// @brief Incident variable-node id
114 uint32_t var_id;
115 /// @brief Parity-check coefficient (label)
117};
118
119/**
120 * @brief Convert one column of symbol-level LLRs to trellis costs
121 * @tparam T Finite field of the trellis edge labels
122 * @param llrs Matrix whose entry `(a - 1, s)` is `ln(P(r_s | 0) / P(r_s | a))`
123 * @param s Symbol index
124 * @return Costs indexed by field-element label, with cost zero for symbol 0
125 *
126 * Lower cost means greater likelihood. Non-finite LLRs are clamped to ±1e6.
127 */
128template <FiniteFieldType T>
129std::array<double, T::get_size()> symbol_costs_from_llrs(const Matrix<double>& llrs, size_t s) {
130 constexpr size_t q = T::get_size();
131 constexpr double L_MAX = 1.0e6;
132
133 std::array<double, q> costs;
134 costs[0] = 0.0;
135 for (size_t a = 1; a < q; ++a) costs[a] = std::clamp(llrs(a - 1, s), -L_MAX, L_MAX);
136 return costs;
137}
138
139inline double max_star(double a, double b) noexcept {
140 constexpr double neg_inf = -std::numeric_limits<double>::infinity();
141 if (a == neg_inf) return b;
142 if (b == neg_inf) return a;
143 const double d = std::abs(a - b);
144 return std::max(a, b) + ((d > 500.0) ? 0.0 : std::log(1.0 + std::exp(-d)));
145}
146
147} // namespace details
148
149template <FieldType T>
150struct Trellis;
151template <FieldType T>
152std::ostream& operator<<(std::ostream& os, const Trellis<T>& Tr);
153template <FieldType T>
154struct TannerGraph;
155
156/**
157 * @brief Layered trellis with field-labelled edges
158 * @tparam T Field type used for edge labels
159 *
160 * Segment `s` connects layers `s` and `s + 1`. Vertices are stored in @ref V with
161 * the invariant `V[s][i].id == i`; decoding workspaces use these indices for path metrics.
162 *
163 * Supports trellis products, segment merging, Viterbi and BCJR workspaces, and
164 * text and TikZ export.
165 */
166template <FieldType T>
167struct Trellis {
168 /** @name Construction
169 * @{
170 */
171
172 /// @brief Construct the one-vertex source trellis
173 Trellis();
174
175 /**
176 * @brief Add one edge in segment `segment`
177 *
178 * @param segment Segment index; connects layer `segment` to `segment + 1`
179 * @param id_from Source vertex id in layer `segment`
180 * @param id_to Target vertex id in layer `segment + 1`
181 * @param value Edge label
182 * @throws std::invalid_argument if `id_from` is missing, `id_to` breaks the id/index invariant,
183 * or the edge already exists
184 */
185 void add_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value);
186
187 /**
188 * @brief Add one edge in segment `segment`, allowing a parallel edge
189 *
190 * @param segment Segment index; connects layer `segment` to `segment + 1`
191 * @param id_from Source vertex id in layer `segment`
192 * @param id_to Target vertex id in layer `segment + 1`
193 * @param value Edge label
194 * @throws std::invalid_argument if `id_from` is missing or `id_to` breaks the id/index invariant
195 *
196 * Unlike @ref add_edge this does not reject an existing `(id_from, id_to)` edge; use it only
197 * where several labels legitimately connect the same two vertices (e.g. the LC-OSD local trellis).
198 */
199 void add_parallel_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value);
200
201 /**
202 * @brief Product trellis with componentwise added edge labels
203 *
204 * @param other Trellis with the same number of segments
205 * @return Trellis whose vertices are pairs of vertices from the operands
206 * @throws std::invalid_argument if the numbers of segments differ
207 */
208 Trellis operator*(const Trellis& other) const;
209
210 /** @} */
211
212 /** @name Information
213 * @{
214 */
215
216 /// @brief Maximum number of vertices in any layer
217 size_t get_maximum_depth() const noexcept;
218
219 /** @} */
220
221 /** @name Decoding Workspaces
222 * @{
223 */
224
225 /**
226 * @brief Workspace for Viterbi decoding on this trellis
227 *
228 * @tparam cost_t Integral type for hard-decision costs or floating-point type for soft costs
229 *
230 * Stores path metrics for two adjacent layers, edge costs, tie counts, and backpointers.
231 * Construct it from the trellis before calling the Viterbi routines in `codes.hpp`.
232 */
233 template <typename cost_t>
236 /// @brief True for floating-point soft metrics
237 static constexpr bool is_soft = std::is_floating_point_v<cost_t>;
238 /// @brief Initial path cost used for unreachable vertices
239 static constexpr cost_t init =
241
242 /**
243 * @brief Allocate metric arrays for `tr`
244 *
245 * @param tr Trellis whose layer and edge sizes define the workspace
246 */
247 explicit Viterbi_Workspace(const Trellis& tr);
248
249 /**
250 * @brief Set hard-decision edge costs from received word `r`
251 *
252 * @param tr Trellis whose edges define candidate symbols
253 * @param r Received word; length must equal the number of segments
254 * @throws std::invalid_argument if `r.get_n() != tr.E.size()`
255 *
256 * With erasure support, erased received symbols give cost 0 on all outgoing labels.
257 */
258 void calculate_edge_costs(const Trellis& tr, const Vector<T>& r)
260
261 /**
262 * @brief Set soft edge costs from symbol-level LLRs
263 *
264 * @param trellis Trellis whose edges define candidate symbols
265 * @param llrs Symbol-level LLR matrix with q−1 rows and one column per segment
266 * @throws std::invalid_argument if the LLR matrix has the wrong number of rows or columns
267 *
268 * Edge costs are the symbol costs of @ref CECCO::details::symbol_costs_from_llrs: cost 0
269 * on 0-labelled edges and L_s(a) on edges labelled by symbol a.
270 */
271 void calculate_edge_costs(const Trellis& trellis, const Matrix<double>& llrs)
273
274 /// @brief Path costs from the previous layer
276 /// @brief Path costs for the current layer
278 /// @brief Backpointer selected for each vertex and layer
280 /// @brief Number of equal-cost paths seen for randomized tie-breaking
282 /// @brief Edge cost for each segment and edge index
284 /// @brief Optional received word (hard symbols or LLR matrix) shown in TikZ output
286 };
287
288 /**
289 * @brief Workspace for list-output Viterbi decoding on this trellis
290 *
291 * @tparam cost_t Integral type for hard-decision costs or floating-point type for soft costs
292 *
293 * Stores edge costs by segment and, for the priority-first path enumeration, the backward
294 * cost-to-go from every vertex to the final layer. Construct it from the trellis before calling
295 * the list Viterbi routine in `codes.hpp`.
296 */
297 template <typename cost_t>
300 /// @brief True for floating-point soft metrics
301 static constexpr bool is_soft = std::is_floating_point_v<cost_t>;
302 /// @brief Cost of an unreachable vertex or an infeasible path
303 static constexpr cost_t init =
305
306 /**
307 * @brief Allocate metric arrays for `tr`
308 *
309 * @param tr Trellis whose layer and edge sizes define the workspace
310 */
311 explicit ListViterbi_Workspace(const Trellis& tr);
312
313 /**
314 * @brief Set hard-decision edge costs from received word `r`
315 *
316 * @param tr Trellis whose edges define candidate symbols
317 * @param r Received word; length must equal the number of segments
318 * @throws std::invalid_argument if `r.get_n() != tr.E.size()`
319 */
320 void calculate_edge_costs(const Trellis& tr, const Vector<T>& r)
322
323 /**
324 * @brief Set soft edge costs from symbol-level LLRs
325 *
326 * @param trellis Trellis whose edges define candidate symbols
327 * @param llrs Symbol-level LLR matrix with q−1 rows and one column per segment
328 * @throws std::invalid_argument if the LLR matrix has the wrong number of rows or columns
329 */
330 void calculate_edge_costs(const Trellis& trellis, const Matrix<double>& llrs)
332
333 /// @brief Edge cost for each segment and edge index
335 /// @brief Minimum cost from each vertex (by layer and id) to any final-layer vertex
337 };
338
339 /**
340 * @brief Workspace for BCJR forward-backward decoding
341 *
342 * Stores α and β metrics by layer and edge costs by segment. It is used by
343 * `LinearCode<T>::dec_BCJR`.
344 */
346 /// @brief BCJR uses floating-point soft metrics
347 static constexpr bool is_soft = true;
348
349 /**
350 * @brief Allocate metric arrays for `tr`
351 *
352 * @param tr Trellis whose layer and edge sizes define the workspace
353 */
354 explicit BCJR_Workspace(const Trellis& tr);
355
356 /**
357 * @brief Set BCJR edge costs from symbol-level LLRs
358 *
359 * @param trellis Trellis whose edges define candidate symbols
360 * @param llrs Symbol-level LLR matrix with q−1 rows and one column per segment
361 * @throws std::invalid_argument if the LLR matrix has the wrong number of rows or columns
362 *
363 * Edge costs are the symbol costs of @ref CECCO::details::symbol_costs_from_llrs.
364 */
365 void calculate_edge_costs(const Trellis& trellis, const Matrix<double>& llrs)
367
368 /// @brief Forward metrics by layer and vertex id
370 /// @brief Backward metrics by layer and vertex id
372 /// @brief Edge cost for each segment and edge index
374 /// @brief Optional received word (hard symbols or LLR matrix) shown in TikZ output
376 };
377
378 /** @} */
379
380 /** @name Text and TikZ Output
381 * @{
382 */
383
384 /**
385 * @brief Write a segment-by-segment text representation
386 *
387 * @param os Output stream
388 * @param ws Optional workspace; if non-null, edge costs are printed with edge labels
389 * @return Output stream
390 */
391 template <typename WS>
392 std::ostream& print(std::ostream& os, const WS* ws) const;
393
394 /**
395 * @brief Write a segment-by-segment text representation without edge costs
396 *
397 * @param os Output stream
398 * @return Output stream
399 */
400 std::ostream& print(std::ostream& os) const;
401
402 /// @brief Stream output via @ref print
403 friend std::ostream& operator<< <>(std::ostream& os, const Trellis& Tr);
404
405 /**
406 * @brief Write TikZ style definitions
407 *
408 * @param file Output stream
409 */
410 template <typename WS>
411 void tikz_header(std::ostream& file) const;
412
413 /**
414 * @brief Write one TikZ picture of the trellis
415 *
416 * @param file Output stream
417 * @param ws Optional decoding workspace; if non-null, metrics and edge costs are shown
418 * @param frontier Viterbi frontier layer to highlight
419 *
420 * Available only for finite fields of size at most 64.
421 */
422 template <typename WS>
423 void tikz_picture(std::ostream& file, const WS* ws, size_t frontier) const
424 requires FiniteFieldType<T> && (T::get_size() <= 64);
425
426 /**
427 * @brief Export a standalone TikZ fragment with workspace metrics
428 *
429 * @param filename Output filename
430 * @param ws Optional decoding workspace; if non-null, metrics and edge costs are shown
431 *
432 * Available only for finite fields of size at most 64.
433 */
434 template <typename WS>
435 void export_as_tikz(const std::string& filename, const WS* ws) const
436 requires FiniteFieldType<T> && (T::get_size() <= 64);
437
438 /**
439 * @brief Export a standalone TikZ fragment without workspace metrics
440 *
441 * @param filename Output filename
442 *
443 * Available only for finite fields of size at most 64.
444 */
445 void export_as_tikz(const std::string& filename) const
446 requires FiniteFieldType<T> && (T::get_size() <= 64);
447
448 /** @} */
449
450 /** @name Field Conversion
451 * @{
452 */
453
454 /**
455 * @brief Merge groups of base-field segments into extension-field labels
456 *
457 * @tparam U Extension field with `T ⊆ U`
458 * @return Trellis over `U`
459 *
460 * Consecutive groups of `[U:T]` segments are replaced by one segment whose labels
461 * are the corresponding `U` elements. A final incomplete group is zero-padded.
462 */
463 template <FiniteFieldType U>
465 Trellis<U> merge_segments() const;
466
467 /** @} */
468
469 /// @brief Vertices by layer; `V[s][i].id == i`
471 /// @brief Edges by segment; `E[s]` connects `V[s]` to `V[s + 1]`
473};
474
475template <FieldType T>
476Trellis<T>::Trellis() : V(1) {
477 V[0].emplace_back(details::Vertex<T>(0));
478}
479
480template <FieldType T>
481void Trellis<T>::add_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value) {
482 if (segment < E.size() &&
483 std::ranges::any_of(E[segment], [&](const auto& e) { return e.from_id == id_from && e.to_id == id_to; }))
484 throw std::invalid_argument("Edge (" + std::to_string(id_from) + " -> " + std::to_string(id_to) +
485 ") already exists in E[" + std::to_string(segment) + "]!");
486 add_parallel_edge(segment, id_from, id_to, value);
487}
488
489template <FieldType T>
490void Trellis<T>::add_parallel_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value) {
491 if (segment >= E.size()) {
492 V.resize(segment + 2);
493 E.resize(segment + 1);
494 }
495
496 auto& Vf = V[segment];
497 if (std::ranges::find_if(Vf, [id_from](const auto& v) { return v.id == id_from; }) == Vf.cend())
498 throw std::invalid_argument("Start node id " + std::to_string(id_from) + " not found in V[" +
499 std::to_string(segment) + "]!");
500
501 auto& Vt = V[segment + 1];
502 if (std::ranges::find_if(Vt, [id_to](const auto& v) { return v.id == id_to; }) == Vt.cend()) {
503 if (id_to != Vt.size())
504 throw std::invalid_argument("New sink id " + std::to_string(id_to) + " in V[" +
505 std::to_string(segment + 1) +
506 "] must equal its position (id==index invariant)!");
507 Vt.emplace_back(id_to);
508 }
509
510 E[segment].emplace_back(id_from, id_to, value);
511}
512
513template <FieldType T>
514Trellis<T> Trellis<T>::operator*(const Trellis& other) const {
515 if (E.size() != other.E.size()) throw std::invalid_argument("Trellises must have the same number of segments!");
516
517 const size_t num_segments = E.size();
518 std::vector<std::unordered_map<uint64_t, uint32_t>> vmap(num_segments + 1);
519
520 auto get_or_add = [&](size_t s, uint32_t a, uint32_t b) -> uint32_t {
521 const uint64_t key = (static_cast<uint64_t>(a) << 32) | b;
522 auto [it, inserted] = vmap[s].try_emplace(key, static_cast<uint32_t>(vmap[s].size()));
523 return it->second;
524 };
525
526 get_or_add(0, 0, 0);
527
528 Trellis result;
529
530 for (size_t s = 0; s < num_segments; ++s) {
531 for (auto e1 = E[s].cbegin(); e1 != E[s].cend(); ++e1) {
532 for (auto e2 = other.E[s].cbegin(); e2 != other.E[s].cend(); ++e2) {
533 result.add_parallel_edge(s, get_or_add(s, e1->from_id, e2->from_id),
534 get_or_add(s + 1, e1->to_id, e2->to_id), e1->value + e2->value);
535 }
536 }
537 }
538
539 return result;
540}
541
542template <FieldType T>
543size_t Trellis<T>::get_maximum_depth() const noexcept {
544 size_t max = 0;
545 for (size_t s = 0; s < V.size(); ++s)
546 if (V[s].size() > max) max = V[s].size();
547 return max;
548}
549
550template <FieldType T>
551template <typename cost_t>
552 requires std::integral<cost_t> || std::floating_point<cost_t>
563
564template <FieldType T>
565template <typename cost_t>
570{
571 if (r.get_n() != tr.E.size()) throw std::invalid_argument("Vector length must match number of trellis segments!");
572 for (size_t s = 0; s < tr.E.size(); ++s)
573 for (size_t j = 0; j < tr.E[s].size(); ++j)
574#ifdef CECCO_ERASURE_SUPPORT
575 edge_costs[s][j] = r[s].is_erased() ? cost_t{0} : ((tr.E[s][j].value != r[s]) ? cost_t{1} : cost_t{0});
576#else
577 edge_costs[s][j] = (tr.E[s][j].value != r[s]) ? cost_t{1} : cost_t{0};
578#endif
579}
580
581template <FieldType T>
582template <typename cost_t>
586 const Matrix<double>& llrs)
588{
589 if (llrs.get_m() != T::get_size() - 1 || llrs.get_n() != trellis.E.size())
590 throw std::invalid_argument("LLR matrix must have q-1 rows and one column per trellis segment!");
591 for (size_t s = 0; s < trellis.E.size(); ++s) {
593 for (size_t j = 0; j < trellis.E[s].size(); ++j)
595 }
596}
597
598template <FieldType T>
599template <typename cost_t>
607
608template <FieldType T>
609template <typename cost_t>
614{
615 if (r.get_n() != tr.E.size()) throw std::invalid_argument("Vector length must match number of trellis segments!");
616 for (size_t s = 0; s < tr.E.size(); ++s)
617 for (size_t j = 0; j < tr.E[s].size(); ++j)
618#ifdef CECCO_ERASURE_SUPPORT
619 edge_costs[s][j] = r[s].is_erased() ? cost_t{0} : ((tr.E[s][j].value != r[s]) ? cost_t{1} : cost_t{0});
620#else
621 edge_costs[s][j] = (tr.E[s][j].value != r[s]) ? cost_t{1} : cost_t{0};
622#endif
623}
624
625template <FieldType T>
626template <typename cost_t>
630 const Matrix<double>& llrs)
632{
633 if (llrs.get_m() != T::get_size() - 1 || llrs.get_n() != trellis.E.size())
634 throw std::invalid_argument("LLR matrix must have q-1 rows and one column per trellis segment!");
635 for (size_t s = 0; s < trellis.E.size(); ++s) {
637 for (size_t j = 0; j < trellis.E[s].size(); ++j)
639 }
640}
641
642template <FieldType T>
644 alpha.reserve(tr.V.size());
645 beta.reserve(tr.V.size());
646 for (size_t s = 0; s < tr.V.size(); ++s) {
647 alpha.emplace_back(tr.V[s].size(), -std::numeric_limits<double>::infinity());
648 beta.emplace_back(tr.V[s].size(), -std::numeric_limits<double>::infinity());
649 }
650 edge_costs.reserve(tr.E.size());
651 for (size_t s = 0; s < tr.E.size(); ++s) edge_costs.emplace_back(tr.E[s].size());
652}
653
654template <FieldType T>
655void Trellis<T>::BCJR_Workspace::calculate_edge_costs(const Trellis& trellis, const Matrix<double>& llrs)
657{
658 if (llrs.get_m() != T::get_size() - 1 || llrs.get_n() != trellis.E.size())
659 throw std::invalid_argument("LLR matrix must have q-1 rows and one column per trellis segment!");
660 for (size_t s = 0; s < trellis.E.size(); ++s) {
662 for (size_t j = 0; j < trellis.E[s].size(); ++j)
664 }
665}
666
667template <FieldType T>
668template <typename WS>
669std::ostream& Trellis<T>::print(std::ostream& os, const WS* ws) const {
670 if (E.empty()) return os;
671 for (size_t i = 0; i < E.size(); ++i) {
672 if (E[i].empty()) return os;
673 for (size_t j = 0; j < E[i].size(); ++j) {
674 os << "(" << E[i][j].from_id << "--" << E[i][j].value;
675 if (ws) os << "[" << ws->edge_costs[i][j] << "]";
676 os << "--" << E[i][j].to_id << ")";
677 if (j < E[i].size() - 1) os << ", ";
678 }
679 if (i != E.size() - 1) os << std::endl;
680 }
681 return os;
682}
683
684template <FieldType T>
685std::ostream& Trellis<T>::print(std::ostream& os) const {
686 return this->template print<Viterbi_Workspace<uint16_t>>(os, nullptr);
687}
688
689template <FieldType T>
690std::ostream& operator<<(std::ostream& os, const Trellis<T>& Tr) {
691 return Tr.print(os);
692}
693
694template <FieldType T>
695template <typename WS>
696void Trellis<T>::tikz_header(std::ostream& file) const {
697 const double arrow_scale = std::min(1.4 / E.size() * 9.0, 1.4);
698 const double vertex_size = std::min(5.0 / E.size() * 9.0, 5.0);
699
700 file << R"(% required in preamble:
701% \usepackage{amsfonts}
702% \usepackage{bm}
703% \usepackage{tikz}
704% \usetikzlibrary{arrows.meta, backgrounds, calc, positioning}
705
706\tikzset{>={Stealth[scale=)"
707 << arrow_scale << R"(]}}
708\tikzstyle{trellisvertex}=[circle, draw=black, outer sep=0pt, inner sep=0pt, minimum size=)"
709 << vertex_size << R"(pt, left color=gray!80, right color=gray!20])";
710
711 if constexpr (WS::is_soft) {
712 file << R"(
713\tikzstyle{trellisvertexprev}=[trellisvertex, font=\tiny, inner sep=1pt, left color=blue!80, right color=blue!20, text width=4ex, align=center]
714\tikzstyle{trellisvertexcurr}=[trellisvertex, font=\tiny, inner sep=1pt, left color=green!80, right color=green!20, text width=4ex, align=center])";
715 } else {
716 file << R"(
717\tikzstyle{trellisvertexprev}=[trellisvertex, font=\footnotesize, inner sep=2pt, left color=blue!80, right color=blue!20]
718\tikzstyle{trellisvertexcurr}=[trellisvertex, font=\footnotesize, inner sep=2pt, left color=green!80, right color=green!20])";
719 }
721 if constexpr (std::is_same_v<WS, BCJR_Workspace>) {
722 file << R"(
723\tikzstyle{trellisvertexsplit}=[trellisvertexprev, inner sep=1pt, circle split, font=\tiny, inner sep=1pt])";
724 } else {
725 }
726
727 file << R"(
728\tikzstyle{trellisarrow}=[draw, ->, fill=black]
729\tikzstyle{trellisarrowone}=[trellisarrow, fill=red, draw=red]
730\tikzstyle{trellisarrowzero}=[trellisarrow, densely dashed, fill=black, draw=black]
731\tikzstyle{trellisedgelabel}=[below, sloped]
732\tikzstyle{trellispath}=[double distance=.075cm, thick, line join=round, cap=round])";
733}
734
735template <FieldType T>
736template <typename WS>
737void Trellis<T>::tikz_picture(std::ostream& file, const WS* ws, size_t frontier) const
738 requires FiniteFieldType<T> && (T::get_size() <= 64)
739{
740 file << "\n\n\\begin{tikzpicture}[x=\\linewidth/" << E.size() << ", y=1cm]";
741
742 for (size_t s = 0; s < V.size(); ++s) {
743 for (size_t i = 0; i < V[s].size(); ++i) {
744 const char* style = "trellisvertex";
745 bool labeled = false;
746 if (ws) {
747 if constexpr (std::is_same_v<WS, BCJR_Workspace>) {
748 style = "trellisvertexsplit";
749 labeled = true;
750 } else {
751 if (s == frontier) {
752 style = "trellisvertexcurr";
753 labeled = true;
754 } else if (frontier > 0 && s == frontier - 1) {
755 style = "trellisvertexprev";
756 labeled = true;
757 }
758 }
759 }
760 if (!labeled) {
761 file << "\n \\node[" << style << "] (" << s << "_" << V[s][i].id << ") at (" << s << ", "
762 << -static_cast<int>(V[s][i].id) << ") {};";
763 } else {
764 if constexpr (WS::is_soft) {
765 file << std::fixed << std::setprecision(2);
766 if constexpr (std::is_same_v<WS, BCJR_Workspace>) {
767 file << "\n \\node[" << style << "] (" << s << "_" << V[s][i].id << ") at (" << s << ", "
768 << -static_cast<int>(V[s][i].id) << ") {$" << ws->alpha[s][i] << "$\\nodepart{lower} $"
769 << ws->beta[s][i] << "$};";
770 } else {
771 file << "\n \\node[" << style << "] (" << s << "_" << V[s][i].id << ") at (" << s << ", "
772 << -static_cast<int>(V[s][i].id) << ") {$"
773 << ((s == frontier) ? ws->path_costs_prev[i] : ws->path_costs_curr[i]) << "$};";
774 }
775 } else {
776 file << "\n \\node[" << style << "] (" << s << "_" << V[s][i].id << ") at (" << s << ", "
777 << -static_cast<int>(V[s][i].id) << ") {$"
778 << ((s == frontier) ? ws->path_costs_prev[i] : ws->path_costs_curr[i]) << "$};";
779 }
780 }
781 }
782 }
783
784 for (size_t s = 0; s < E.size(); ++s) {
785 for (size_t j = 0; j < E[s].size(); ++j) {
786 const auto& e = E[s][j];
787 const auto label = e.value.get_label();
788 if (label == 0) {
789 file << "\n \\path[trellisarrowzero]";
790 } else if (label == T::get_size() - 1) {
791 file << "\n \\path[trellisarrowone]";
792 } else {
793 const size_t a = 63 - 63 * static_cast<double>(label) / (T::get_size() - 1);
794 const uint8_t r = details::colormap[a][0];
795 const uint8_t g = details::colormap[a][1];
796 const uint8_t b = details::colormap[a][2];
797 file << "\n \\definecolor{color}{RGB}{" << static_cast<int>(r) << ", " << static_cast<int>(g) << ", "
798 << static_cast<int>(b) << "}\\path[trellisarrow, draw=color, fill=color]";
799 }
800 file << " (" << s << "_" << e.from_id << ")";
801 if (ws) {
802 file << " edge[trellisedgelabel] node[black] {\\tiny$";
803 if constexpr (WS::is_soft) file << std::fixed << std::setprecision(2);
804 file << ws->edge_costs[s][j] << "$}";
805 } else {
806 file << " --";
807 }
808 file << " (" << s + 1 << "_" << e.to_id << ");";
809 }
810 }
811
812 file << "\n \\begin{scope}[on background layer]";
814 for (size_t s = 0; s < V.size(); ++s) {
815 file << "\n \\draw [dotted, shorten >=-5mm] (" << s << "_0) to (" << s << ", "
816 << -static_cast<int>(maxdepth) + 1 << ");";
817 }
818
819 if constexpr (!std::is_same_v<WS, BCJR_Workspace>) {
820 if (ws && frontier > 0) {
822 path.reserve(frontier + 1);
823 for (size_t i = 0; i < V[frontier].size(); ++i) {
824 path.clear();
825 size_t v = i;
827 bool complete = true;
828 for (size_t s = frontier; s > 0; --s) {
829 const auto* edge = ws->backptrs[s][v];
830 if (edge == nullptr) { // no survivor recorded (yet), nothing to draw
831 complete = false;
832 break;
833 }
834 v = edge->from_id;
836 }
837 if (!complete) continue;
838 file << "\n \\draw[trellispath, green, double=green!15] (" << frontier - 1 << "_" << path[1]
839 << ") -- (" << frontier << "_" << path[0] << ");";
840 if (path.size() > 2) {
841 file << "\n \\draw[trellispath, blue, double=blue!15]";
842 for (size_t k = 1; k < path.size(); ++k) {
843 file << " (" << frontier - k << "_" << path[k] << ")";
844 if (k + 1 < path.size()) file << " --";
845 }
846 file << ";";
847 }
848 }
849 }
850 }
851
852 file << "\n \\end{scope}"
853 << "\n \\node[node distance=.0cm, below left=of 0_0] {$\\mathfrak{s}$};"
854 << "\n \\node[node distance=.0cm, below right=of " << E.size() << "_0] {$\\mathfrak{t}$};";
855
856 if constexpr (requires { ws->v; }) {
857 if (ws && ws->v.has_value()) {
858 file << "\n \\node[anchor=east] at ($(0_0)+(0,.5)$) {\\small$\\bm{v}=$};";
859 for (size_t s = 0; s < E.size(); ++s) {
860 file << "\n \\node at ($(" << s << "_0)!0.5!(" << s + 1 << "_0)+(0,.5)$) {\\small$";
861 if (s == 0) file << "(";
862 std::visit(
863 [&](const auto& w) {
864 if constexpr (std::is_same_v<std::decay_t<decltype(w)>, Matrix<double>>)
865 file << w(0, s);
866 else
867 file << w[s];
868 },
869 *(ws->v));
870 file << (s + 1 == E.size() ? ")" : ",") << "$};";
871 }
872 }
873 }
875 file << "\n\\end{tikzpicture}\n";
876}
877
878template <FieldType T>
879template <typename WS>
880void Trellis<T>::export_as_tikz(const std::string& filename, const WS* ws) const
881 requires FiniteFieldType<T> && (T::get_size() <= 64)
882{
886 tikz_picture(file, ws, V.size() - 1);
887 file.close();
888}
889
890template <FieldType T>
891void Trellis<T>::export_as_tikz(const std::string& filename) const
892 requires FiniteFieldType<T> && (T::get_size() <= 64)
893{
895}
896
897template <FieldType T>
898template <FiniteFieldType U>
900Trellis<U> Trellis<T>::merge_segments() const {
902 const size_t n = E.size();
903 const size_t full_groups = n / m;
904 const size_t tail = n - full_groups * m;
905 const size_t segments = full_groups + (tail != 0 ? 1 : 0);
906
908
909 // add_parallel_edge accepts a new sink id only in ascending order, which paths cannot guarantee
910 result.V.resize(segments + 1);
912 for (size_t s = 1; s <= segments; ++s) {
913 const size_t src = std::min(s * m, n);
914 result.V[s].reserve(V[src].size());
915 for (size_t i = 0; i < V[src].size(); ++i) result.V[s].emplace_back(static_cast<uint32_t>(i));
916 }
917
918 for (size_t g = 0; g < segments; ++g) {
919 const size_t width = (g < full_groups) ? m : tail;
920 std::vector<std::vector<const details::Edge<T>*>> paths;
921
922 for (auto e = E[g * m].cbegin(); e != E[g * m].cend(); ++e) paths.push_back({std::to_address(e)});
923
924 for (size_t step = 1; step < width; ++step) {
925 std::vector<std::vector<const details::Edge<T>*>> next;
926 for (auto path = paths.cbegin(); path != paths.cend(); ++path)
927 for (auto e = E[g * m + step].cbegin(); e != E[g * m + step].cend(); ++e)
928 if (e->from_id == path->back()->to_id) {
929 auto ext = *path;
932 }
933 paths = std::move(next);
934 }
935
936 for (auto path = paths.cbegin(); path != paths.cend(); ++path) {
937 Vector<T> v(m);
938 for (size_t i = 0; i < width; ++i) v.set_component(i, (*path)[i]->value);
941 }
942
943 return result;
944}
945
946/**
947 * @brief Tanner graph of a parity-check matrix
948 * @tparam T Field type used for edge labels
949 *
950 * Contains `n` variable nodes and one node per parity-check equation. An edge
951 * `(check, variable, value)` represents a nonzero parity-check coefficient.
952 * @ref BP_Workspace provides sum-product decoding state.
953 */
954template <FieldType T>
955struct TannerGraph {
956 /// @brief Construct a Tanner graph with `n` variable nodes and no checks
957 explicit TannerGraph(size_t n) : n(n) {}
958
959 /**
960 * @brief Add a check-to-variable edge `check --value--> var_id`
961 *
962 * @param check Check-node index; checks are created on demand
963 * @param var_id Incident variable-node id
964 * @param value Parity-check coefficient carried by the edge
965 * @throws std::invalid_argument if `var_id` is not a variable node of this graph
966 */
967 void add_edge(size_t check, uint32_t var_id, T value) {
968 if (var_id >= n)
969 throw std::invalid_argument("Variable node id " + std::to_string(var_id) +
970 " out of range for Tanner graph with " + std::to_string(n) + " variable nodes");
971 if (check >= checks.size()) checks.resize(check + 1);
972 checks[check].emplace_back(var_id, value);
974
975 /**
976 * @brief Workspace for sum-product (belief propagation) decoding
977 *
978 * Stores variable-to-check and check-to-variable messages, the intrinsic (channel) costs, and the
979 * running posterior beliefs, each a length-q array per node. It is used by `LinearCode<T>::dec_BP`.
980 */
981 struct BP_Workspace {
982 /// @brief Number of field symbols (message and belief vector length)
983 static constexpr size_t q = T::get_size();
984
985 /**
986 * @brief Allocate message and belief arrays for `g`
987 *
988 * @param g Tanner graph whose variable- and check-node degrees define the workspace
989 */
990 explicit BP_Workspace(const TannerGraph& g) : intrinsic(g.n), posterior(g.n), var_edges(g.n) {
991 m_vc.reserve(g.checks.size());
992 m_cv.reserve(g.checks.size());
993 for (size_t i = 0; i < g.checks.size(); ++i) {
994 m_vc.emplace_back(g.checks[i].size());
995 m_cv.emplace_back(g.checks[i].size());
996 for (size_t e = 0; e < g.checks[i].size(); ++e) var_edges[g.checks[i][e].var_id].emplace_back(i, e);
997 }
998 }
999
1000 /**
1001 * @brief Set intrinsic (channel) costs from symbol-level LLRs
1002 *
1003 * @param g Tanner graph providing the variable-node count
1004 * @param llrs Symbol-level LLR matrix with q−1 rows and one column per variable node
1006 * Intrinsic costs are the negated symbol costs of @ref CECCO::details::symbol_costs_from_llrs.
1008 void calculate_intrinsic(const TannerGraph& g, const Matrix<double>& llrs)
1009 requires FiniteFieldType<T>
1010 {
1011 for (size_t j = 0; j < g.n; ++j) {
1012 const auto costs = details::symbol_costs_from_llrs<T>(llrs, j);
1013 for (size_t a = 0; a < q; ++a) intrinsic[j][a] = -costs[a];
1014 }
1015 }
1016
1017 /// @brief Variable-to-check messages by check and incident edge
1018 std::vector<std::vector<std::array<double, q>>> m_vc;
1019 /// @brief Check-to-variable messages by check and incident edge
1020 std::vector<std::vector<std::array<double, q>>> m_cv;
1021 /// @brief Intrinsic (channel) cost per variable node and symbol
1022 std::vector<std::array<double, q>> intrinsic;
1023 /// @brief Posterior belief per variable node and symbol
1024 std::vector<std::array<double, q>> posterior;
1025 /// @brief Incident (check, edge) pairs for each variable node
1026 std::vector<std::vector<std::pair<size_t, size_t>>> var_edges;
1027 /// @brief Optional received LLR matrix shown in TikZ output
1028 std::optional<Matrix<double>> v;
1029 };
1030
1031 /**
1032 * @brief Write TikZ style definitions
1033 *
1034 * @param file Output stream
1035 */
1036 void tikz_header(std::ostream& file) const {
1037 file << R"(% required in preamble:
1038% \usepackage{amsfonts}
1039% \usepackage{bm}
1040% \usepackage{tikz}
1041% \usetikzlibrary{calc, positioning}
1042
1043\tikzstyle{tannervariable}=[circle, draw=black, inner sep=0pt, minimum size=6pt, left color=gray!80, right color=gray!20]
1044\tikzstyle{tannervariablesoft}=[circle, draw=black, font=\tiny, inner sep=1pt, left color=green!80, right color=green!20, text width=4ex, align=center]
1045\tikzstyle{tannercheck}=[rectangle, draw=black, inner sep=0pt, minimum size=7pt, fill=gray!40]
1046\tikzstyle{tanneredge}=[draw])";
1047 }
1048
1049 /**
1050 * @brief Write one TikZ picture of the Tanner graph
1051 *
1052 * @param file Output stream
1053 * @param ws Optional decoding workspace; if non-null, posterior beliefs and decisions are shown
1054 *
1055 * Available only for finite fields of size at most 64.
1056 */
1057 void tikz_picture(std::ostream& file, const BP_Workspace* ws) const
1058 requires FiniteFieldType<T> && (T::get_size() <= 64)
1059 {
1060 const size_t redundancy = checks.size();
1061 const double width = (n > 1) ? static_cast<double>(n - 1) : 1.0;
1062 file << "\n\n\\begin{tikzpicture}[x=\\linewidth/" << width << ", y=1cm]";
1063
1064 for (size_t j = 0; j < n; ++j) {
1065 if constexpr (T::get_size() == 2) {
1066 if (ws) {
1067 file << "\n \\node[tannervariablesoft] (v" << j << ") at (" << j << ", 0) {$" << std::fixed
1068 << std::setprecision(2) << (ws->posterior[j][0] - ws->posterior[j][1]) << "$};";
1069 const size_t dec = (ws->posterior[j][1] > ws->posterior[j][0]) ? 1 : 0;
1070 file << "\n \\node[anchor=north, font=\\tiny] at ($(v" << j << ")-(0,0.3)$) {$" << dec << "$};";
1071 continue;
1072 }
1073 }
1074 file << "\n \\node[tannervariable] (v" << j << ") at (" << j << ", 0) {};";
1075 }
1076
1077 for (size_t i = 0; i < redundancy; ++i) {
1078 const double x = (redundancy > 1) ? static_cast<double>(i) * (n - 1) / (redundancy - 1) : (n - 1) / 2.0;
1079 file << "\n \\node[tannercheck] (c" << i << ") at (" << x << ", 3) {};";
1080 }
1081
1082 for (size_t i = 0; i < redundancy; ++i) {
1083 for (size_t e = 0; e < checks[i].size(); ++e) {
1084 const auto label = checks[i][e].value.get_label();
1085 if (label == T::get_size() - 1) {
1086 file << "\n \\path[tanneredge, draw=red]";
1087 } else {
1088 const size_t a = 63 - 63 * static_cast<double>(label) / (T::get_size() - 1);
1089 file << "\n \\definecolor{color}{RGB}{" << static_cast<int>(details::colormap[a][0]) << ", "
1090 << static_cast<int>(details::colormap[a][1]) << ", "
1091 << static_cast<int>(details::colormap[a][2]) << "}\\path[tanneredge, draw=color]";
1092 }
1093 file << " (v" << checks[i][e].var_id << ") -- (c" << i << ");";
1094 }
1095 }
1096
1097 if constexpr (T::get_size() == 2) {
1098 if (ws && ws->v.has_value()) {
1099 for (size_t j = 0; j < n; ++j) {
1100 file << "\n \\node[anchor=south, font=\\tiny] at ($(v" << j << ")+(0,0.3)$) {$" << std::fixed
1101 << std::setprecision(2) << (*(ws->v))(0, j) << "$};";
1102 }
1103 }
1104 }
1105
1106 file << "\n\\end{tikzpicture}\n";
1107 }
1109 /**
1110 * @brief Export a standalone TikZ fragment with workspace beliefs
1111 *
1112 * @param filename Output filename
1113 * @param ws Optional decoding workspace; if non-null, posterior beliefs and decisions are shown
1114 *
1115 * Available only for finite fields of size at most 64.
1116 */
1117 void export_as_tikz(const std::string& filename, const BP_Workspace* ws) const
1118 requires FiniteFieldType<T> && (T::get_size() <= 64)
1119 {
1120 std::ofstream file;
1124 file.close();
1125 }
1126
1127 /**
1128 * @brief Export a standalone TikZ fragment without workspace beliefs
1129 *
1130 * @param filename Output filename
1131 *
1132 * Available only for finite fields of size at most 64.
1133 */
1134 void export_as_tikz(const std::string& filename) const
1135 requires FiniteFieldType<T> && (T::get_size() <= 64)
1136 {
1137 export_as_tikz(filename, nullptr);
1138 }
1139
1140 /// @brief Number of variable nodes
1141 size_t n;
1142 /// @brief Incident edges by check node; `checks[i]` lists the variables in parity check `i`
1143 std::vector<std::vector<details::CheckEdge<T>>> checks;
1144};
1145
1146} // namespace CECCO
1147
1148#endif
Contains implementation details not to be exposed to the user. Functions and classes here may change ...
Definition blocks.hpp:59
std::array< double, T::get_size()> symbol_costs_from_llrs(const Matrix< double > &llrs, size_t s)
Convert one column of symbol-level LLRs to trellis costs.
Definition graphs.hpp:129
double max_star(double a, double b) noexcept
Definition graphs.hpp:139
Provides a framework for error correcting codes.
Definition blocks.hpp:57
Tanner graph of a parity-check matrix.
Definition graphs.hpp:938
Workspace for BCJR forward-backward decoding.
Definition graphs.hpp:345
std::vector< std::vector< double > > alpha
Forward metrics by layer and vertex id.
Definition graphs.hpp:369
void calculate_edge_costs(const Trellis &trellis, const Matrix< double > &llrs)
Set BCJR edge costs from symbol-level LLRs.
Definition graphs.hpp:655
std::vector< std::vector< double > > beta
Backward metrics by layer and vertex id.
Definition graphs.hpp:371
BCJR_Workspace(const Trellis &tr)
Allocate metric arrays for tr.
Definition graphs.hpp:643
std::vector< std::vector< double > > edge_costs
Edge cost for each segment and edge index.
Definition graphs.hpp:373
static constexpr bool is_soft
BCJR uses floating-point soft metrics.
Definition graphs.hpp:347
Layered trellis with field-labelled edges.
Definition graphs.hpp:167
void add_parallel_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value)
Add one edge in segment segment, allowing a parallel edge.
Definition graphs.hpp:490
std::vector< std::vector< details::Edge< T > > > E
Edges by segment; E[s] connects V[s] to V[s + 1].
Definition graphs.hpp:472
void tikz_header(std::ostream &file) const
Write TikZ style definitions.
Definition graphs.hpp:696
void add_edge(size_t segment, uint32_t id_from, uint32_t id_to, T value)
Add one edge in segment segment.
Definition graphs.hpp:481
std::vector< std::vector< details::Vertex< T > > > V
Vertices by layer; V[s][i].id == i.
Definition graphs.hpp:470
Trellis operator*(const Trellis &other) const
Product trellis with componentwise added edge labels.
Definition graphs.hpp:514
std::ostream & print(std::ostream &os, const WS *ws) const
Write a segment-by-segment text representation.
Definition graphs.hpp:669
Trellis()
Construct the one-vertex source trellis.
Definition graphs.hpp:476
size_t get_maximum_depth() const noexcept
Maximum number of vertices in any layer.
Definition graphs.hpp:543
std::ostream & print(std::ostream &os) const
Write a segment-by-segment text representation without edge costs.
Definition graphs.hpp:685
void export_as_tikz(const std::string &filename) const
Export a standalone TikZ fragment without workspace metrics.
Definition graphs.hpp:874
void export_as_tikz(const std::string &filename, const WS *ws) const
Export a standalone TikZ fragment with workspace metrics.
Definition graphs.hpp:863
void tikz_picture(std::ostream &file, const WS *ws, size_t frontier) const
Write one TikZ picture of the trellis.
Definition graphs.hpp:720
Edge from a check node to a variable node in a Tanner graph.
Definition graphs.hpp:104
CheckEdge(uint32_t var_id, T value)
Construct an edge to variable node var_id with label value.
Definition graphs.hpp:111
T value
Parity-check coefficient (label).
Definition graphs.hpp:116
uint32_t var_id
Incident variable-node id.
Definition graphs.hpp:114
Edge between two adjacent trellis layers.
Definition graphs.hpp:80
uint32_t from_id
Source vertex id.
Definition graphs.hpp:91
Edge(uint32_t from_id, uint32_t to_id, T value)
Construct an edge from_id --value--> to_id.
Definition graphs.hpp:88
T value
Edge label.
Definition graphs.hpp:95
uint32_t to_id
Target vertex id.
Definition graphs.hpp:93
Vertex in one trellis layer.
Definition graphs.hpp:66
uint32_t id
Layer-local vertex id.
Definition graphs.hpp:71
Vertex(uint32_t id) noexcept
Construct a vertex with layer-local id id.
Definition graphs.hpp:68