#set page(paper: "a4", margin: 2cm, numbering: "1/1")
#set text(font: "Linux Libertine", lang: "fr")

#set math.mat(delim: "[")

#align(center)[
  #text(size: 24pt, weight: "bold")[Codes LDPC] \
  #v(5pt)
  #text(size: 14pt, style: "italic")[Notes pour TIPE]
]


= Introduction à la théorie de l'information
#v(5pt)

Problème principal : il y a du bruit dans les transmissions mais on veut pas d'erreurs.\
Au lieu de trouver des modifications physiques on va creer des solutions pour corriger les erreurs.\
Il faut un *encodeur* qui ajoute de la redondance et un *décodeur*.\

#v(5pt)

#import "@preview/cetz:0.4.2"
#align(center)[
  #cetz.canvas(length: 1cm, {
    import cetz.draw: *

    content((0, 0), name: "src")[*Source*]

    rect((2, -0.75), (5, 0.75), name: "enc")
    content("enc")[Encodeur]

    rect((7, -0.75), (10, 0.75), name: "chan")
    content("chan")[Canal _(Bruité)_]

    rect((12, -0.75), (15, 0.75), name: "dec")
    content("dec")[Décodeur]

    content((17, 0), name: "dest")[*Dest*]

    line("src", "enc", mark: (end: ">"))
    line("enc", "chan", mark: (end: ">"))
    line("chan", "dec", mark: (end: ">"))
    line("dec", "dest", mark: (end: ">"))

    content((1, 0), anchor: "south", padding: 5pt)[$ s $]
    content((6, 0), anchor: "south", padding: 5pt)[$ t $]
    content((11, 0), anchor: "south", padding: 5pt)[$ r $]
    content((16, 0), anchor: "south", padding: 5pt)[$ hat(s) $]
  })
  Code correcteur d'erreurs pour un cannal binaire symetrice @mackay
]

#v(5pt)

Le but est de transformer un cannal bruité en cannal fiable avec un coups de calculs en plus (encodeur / décodeur).\
On vas chercher la meilleur performance de correction d'erreurs. Ce sont les limites théoriques que cherchent à trouver la _Théorie de l'information_.
Pas de retransmissions.

#v(5pt)

= Codes de répétition
#pad(left: 1cm)[

  == Définitions

  Il s'agit ici de répter tous les bits. Un message source *$s$*, un message transmit *$t$*, un vecteur de bruit *$n$* et un message recu *$r$*. $space$ *$r = t + n$*.
  #pad(left: 1cm)[
    $s = space space 0 space space space space space 1$\
    $t = overbrace(000) space overbrace(111)$ \
    $n = 001 space space space 010$ \
    $r = 001 space space space 101$\
  ]
  On décode en choisissant le bit le plus présent dans un bloc. \
  C'est ce que représente le _Likelihood ratios_ : $P(r | s = 1) / P(r | s = 0)$ \
  Ici $001 -> 0$ et $101 -> 1$ donc $hat(s) = 0 space 1$.\
  Preuve de l'optimalité (dans le sens la plus faible probabilité d'erreurs) @mackay p6. \
  *Problème* : trois fois plus de bande passante...
]

#v(5pt)

= _Single parity check code_ et définitions algébriques
#pad(left: 1cm)[
  Ajout d'uniquement 1 bit d'information à la fin sur la parité du nombre de 1. \
  Un message *$s$* est de la forme
  $
    s = [s_1 space s_2 space s_3 space s_4 space s_5 space s_6]
  $
  où $c_i in {0,1}$ et le _codeword_ vérifie la contraine si

  #math.equation(
    numbering: _ => "E",
    block: true,
  )[$s_1 plus.o s_2 plus.o s_3 plus.o s_4 plus.o s_5 plus.o s_6 = 0$]

  _parity-check equation_.\
  Inversion d'un nombre bit paire $=>$ E = 0 donc aucune erreur détécté.\
  C'est donc pas assez puissant pour savoir quel bit à changé.\

  On écrit sous forme matricielle.

  $ H s^T = mat(1, 1, 0, 1, 0, 0; 0, 1, 1, 0, 1, 0; 1, 1, 1, 0, 0, 1;) mat(s_1; s_2; s_3; s_4; s_5; s_6) = mat(0; 0; 0) $ avec $H$ la matrice _parity-check_ où chaque ligne de $H$ correspond à l'équation de parité et chaque colonne de $H$ correspond à un bit du _codeword_.\

  Les contraines sont alors les suivantes
  $
    s_4 & = s_1 plus.o s_2
          s_5 & = s_2 plus.o s_3
                s_6 & = s_1 plus.o s_2 plus.o s_3
  $

  De plus $s_4, s_5, s_6$ sont les bits de parités et :
  $ s = [s_1 s_2 s_3 s_4 s_5 s_6] = [s_1, s_2, s_3]
  underbrace(
    mat(
      1, 0, 0, 1, 0, 1;
      0, 1, 0, 1, 1, 1;
      0, 0, 1, 0, 1, 1
    ), G
  ) $ où *$G$* est la matrice génératrice du code.\

  On note $u = [u_1, ..., u_k]$ où $u$ contient les $k$ bits du message, ici $u = [u_1, u_2, u_2]$
  $ s = u G $ Pour un message de longueur $k$ et $n$ _codewords_, $G in M_(k times n) (ZZ \/ 2ZZ)$. \

  De plus $k / n$ est le _rate_ du code.\

  Un code de taille $k$ contient $2^k$ _codewords_. Ces _codewords_ sont des sous-ensembles avec $2^n$ vecteurs de taille $n$ possibles. \

  On peut obtenir $H$ sout la forme

  $ H = [A, I_(n-k)] $ avec $A in M_((n-k) times k) (ZZ \/ 2 ZZ)$ et donc
  $ G = [I_k, A^T] $ De plus si $G$ est la matrice génératrice pour un code avec matrice de parité $H$ alors

  $ G H^T = 0 $ $G$ est orthogonal à $H$.\

  Un code peut avoir autant de contraites _parity-check_ qu'il veut mais seulement $n - k$ d'entre elle seront linéairement independantes. C'est à dire :

  $
    n - k = op("rg")(H)
  $

  Voir @johnson

  == Comment détécter et corriger les erreurs

  Supposon qu'on envoie $s = [1 space 0 space 1 space 1 space 1 space 0]$ et qu'on recois $r = [1 space 0 space 1 space 0 space 1 space 0]$ alors

  $
    H r^T = mat(1, 1, 0, 1, 0, 0; 0, 1, 1, 0, 1, 0; 1, 1, 1, 0, 0, 1;) mat(1; 0; 1; 0; 1; 0) = mat(1; 0; 0)
  $

  Le vecteur $s = H r^T$ est le *syndrome* de $r$, il indique quel contraine de _parity-check_ ne sont pas satisfaites par $r$.\

  Ici $s = mat(1; 0; 0)$ et l'équation de parité associé est $s_4 = s_1 plus.o s_2$.\

  Un _block code_ ne peut détecter des erreurs que si ces dernières ne transforment pas un _codeword_ valide en un autre _codeword_ valide. (voir #link(<hamming>)[Code de _Hamming_])
  \

  - Distance de Hamming : Nombre de positions où les bits diffèrent entre deux _codewords_.\
    Exemple : $[1 space 0 space 1 space 0 space 0 space 1 space 1 space 0]$ et $[1 space 0 space 0 space 0 space 0 space 1 space 1 space 1]$ diffèrent aux positions 3 et 8 \
    $=>$ Distance de Hamming = $2$.

  - Distance minimale ($d_min$) : La plus petite distance de Hamming mesurée entre n'importe quelle paire de _codewords_ appartenant au code.

  Un code avec une distance minimale $d_min$ peut garantir la détection de $t$ erreurs si et seulement si :
  $
    t < d_min
  $

  Exemple :\
  Pour _Hamming_ (7,4) vu #link(<hamming>)[après], on a $d_min = 3$. \
  $=>$ Il garantit la détection de 1 ou 2 erreurs ($t < 3$).\
  $=>$ Si 3 bits (ou plus) s'inversent, le message peut correspondre à un autre _codeword_ valide. (Exemple 1.8 @mackay).

  Pour corriger l'erreur, le décodeur cherche le _codeword_ le plus probable.\

  Principe (_maximum-likelihood_ (ML) Decoder) : Il choisit le _codeword_ $s$ valide qui a la plus petite distance de Hamming avec le message reçu $r$. (Si égalité alors le choix est aléatoire).

  $
    hat(s) = min_(c in C) d_H (r, s)
  $
  Avec $C$ l'ensemble des _codewords_ valides.
]

#v(5pt)

= Code de _Hamming_ <hamming>
#pad(left: 1cm)[
  But : Ajouter de la redondance à des bloques de données.\
  _Block code_ : règle de conversion d'un sequence de bits *$s$* de longueur $K$ dans une séquencce *$t$* de $N$ bits. (Redondance $=>$ $N > K$). \
  Dans un code linéaire les $N - K$ bits réstant sont linéaire en fonction des $K$ bits originaux, ce sont les _parity-check bits_.\
  - _Hamming_ (7,4)
  #pad(left: 1cm)[
    L'encodage se visualise via 3 cercles sécants (Diagramme de Venn).
    Les 7 bits sont placés de sorte que la parité de chaque cercle soit paire (somme = 0).

    - Bits de Source ($s_1, s_2, s_3, s_4$) : Copiés directement dans le message transmis ($t_1..t_4$).
    - Bits de Parité ($t_5, t_6, t_7$) : Calculés pour valider les cercles.

    $
      t_5 & = s_1 plus.o s_2 plus.o s_3 & "(Cercle 1)" \
      t_6 & = s_2 plus.o s_3 plus.o s_4 & "(Cercle 2)" \
      t_7 & = s_1 plus.o s_3 plus.o s_4 & "(Cercle 3)"
    $

    Le *Syndrome* *$z$* :
    On vérifie la parité des cercles à l'arrivée.
    - 1 cercle faux $->$ Erreur sur le bit de parité.
    - 2 ou 3 cercles faux $->$ Erreur à l'intersection unique des cercles fautifs.
  ]
  On peut le voir sous forme de matrice.\
  Message transmit *$t$* (_codeword_) :
  $
    t = G^T s
  $

  avec *$G$* la matrice génératrice du code.

  // $
  //   G^T = mat(
  //     1, 0, 0, 0;
  //     0, 1, 0, 0;
  //     0, 0, 1, 0;
  //     0, 0, 0, 1;
  //     1, 1, 1, 0;
  //     0, 1, 1, 1;
  //     1, 0, 1, 1;
  //     column-gap: #1.5em,
  //   )
  // $
  Visualisation de la solution avec diagramme de Venn.\
  Trouver $P$ tel que
  $ G^T = mat(
    I_n;
    P;
    row-gap: #0.75em
  ) $ avec $z = H r$ et $H$ la mtrice _parity-check_ $H = mat(-P, I_(n-1)) = mat(P, I_(n-1))$ \
  Et donc tous les _codewords_ satisfont $t = G^T s$,
  $
    H t = mat(
      0;
      0;
      0;
    )
  $
  Mais $r = G^T s + n$ on doit trouver *$n$* tel que $H n = z$. C'est le probleme _maximum-likelihood decoder_.

  Voir exemple @mackay p9. | #link("https://www.youtube.com/watch?v=X8jsijhllIA")[3Blue1Brown Hamming codes]
]


= Low-density parity-check codes (LDPC) @johnson

#pad(left: 1cm)[

  _Block codes_ définis par une matrice _parity-check_ *$H$* très *creuse* (beaucoup de 0).\
  $=>$ Décodage en $cal(O)(n)$

  == Défintions et Propriétés

  *Création* : On construit d'abord la matrice creuse *$H$*, puis on en déduit la matrice génératrice *$G$* (contrairement aux codes classiques).\

  *Décodage* : Décodage itératif basé sur une représentation graphique de *$H$* (Graphe de Tanner) (au lieu du décodage ML habituel).

  *Régularité* : Un code LDPC est dit $(w_c, w_r)$-régulier si :
  - Chaque bit de code appartient à $w_c$ équations de parité.
  - Chaque équation de parité contient $w_r$ bits de code.

  *Irréguliers*
  Le nombre de $1$ varie selon les lignes et les colonnes de *$H$*.\
  On définit la *distribution des degrés* $(v, h)$ :
  - *$v_i$* : la fraction des colonnes (bits) ayant un poids de $i$.
  - *$h_i$* : la fraction des lignes (équations de parité) ayant un poids de $i$.

  #pad(left: 1cm)[
    *Propriété : nombre total de $1$ dans $H$ :* \
    Pour une matrice de $m$ lignes et $n$ colonnes :
    - Code régulier : $m dot w_r = n dot w_c$
    - Code irrégulier : $m sum_i (h_i dot i) = n sum_i (v_i dot i)$
  ]

  == LDPC constructions

  *Principe * : On part d'une matrice remplie de zéros et on y place un petit nombre de $1$ pour respecter la distribution de degrés.

  === Gallager

  Il faut imaginer que la matrice *$H$* (de *$m$* lignes) est découpée horizontalement en *$w_c$* "bandes" de même taille (chacune a donc *$m / w_c$* lignes).

  - *Création de la 1ère bande* : On place *$w_r$* $1$ consécutifs sur chaque ligne. À chaque fois qu'on descend d'une ligne, on décale ces $1$ vers la droite.

  - *Création des autres bandes* : On prend simplement la 1ère bande et on mélange aléatoirement l'ordre de ses colonnes.

  - *Résultat* : Comme on a superposé *$w_c$* bandes, et que dans chaque bande il y a exactement un $1$ par colonne, on est certain que chaque colonne de *$H$* aura exactement un poids de *$w_c$*.

  - Exemple :


  #pad(left: 1cm)[
    Code régulier avec $n = 12$ colonnes, $w_c = 3$ et $w_r = 4$.\

    *$H$* aura $m = (12 dot 3) / 4 = 9$ lignes.\

    On divise ces 9 lignes en $w_c = 3$ blocs horizontaux de 3 lignes.

    *Bloc 1* (Escalier) :
    $
      B_1 = mat(
        1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0;
        0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0;
        0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1;
      )
    $

    *Bloc 2* (Permutation aléatoire des colonnes du Bloc 1) :
    $
      B_2 = mat(
        1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0;
        0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1;
        0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0
      )
    $

    *Bloc 3* (Permutation aléatoire des colonnes du Bloc 1) :
    $
      B_3 = mat(
        1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0;
        0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1;
        0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0
      )
    $

    Et donc

    $
      H = mat(B_1; B_2; B_3; row-gap: #0.75em)
    $

    Finalement dans n'importe quel bloc en regardant une colonne il n'y à qu'un $1$.
  ]

  === MacKay et Neal

  - La matrice *$H$* est remplie une colonne à la fois, de gauche à droite.
  - Les $1$ sont placés aléatoirement dans les lignes qui ne sont pas encore pleines.
  - Si à un moment, il y a plus de lignes incomplètes que de colonnes restantes à ajouter, la distribution des lignes ne sera pas exacte. On peut alors revenir de quelques colonnes en arrière ou recommencer le processus jusqu'à obtenir le bon résultat.

  - Exemple :

  #pad(left: 1cm)[
    Code régulier $(3,4)$ de longueur $12$.\
    Quand on ajoute la $11$ème colonne, les lignes non remplies étaient les lignes $2, 4, 5, 6$ et $9$.\
    L'algorithme a choisi d'y placer un $1$ sur les lignes $2, 4$ et $6$.

    $
      H = mat(
        1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0;
        1, 0, 0, 1, 1, 0, 0, 0, 0, 0, bold(1), 0;
        0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1;
        0, 0, 1, 0, 0, 0, 1, 1, 0, 0, bold(1), 1;
        0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0;
        1, 0, 1, 0, 0, 0, 0, 0, 0, 0, bold(1), 1;
        0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0;
        0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0;
        0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1
      )
    $
  ]


  === Codes Repeat-Accumulate (RA)

  - Les *$m$* dernières colonnes de la matrice *$H$* ont toutes un poids de $2$ et forment un motif "en escalier".
  - Encodage rapide.

  - Exemple :

  #pad(left: 1cm)[
    Code RA de longueur $12$ avec un ratio de $1/4$.

    $
      H = mat(
        1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0;
        1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0;
        0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0;
        0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0;
        0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0;
        0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0;
        1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0;
        0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0;
        0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1
      )
    $

    Les trois premières colonnes de *$H$* correspondent aux bits du message initial.\
    Les bits de parité (à partir de la 4ème colonne) se calculent ensuite en cascade :\
    $c_4 = c_1$\
    $c_5 = c_4 plus.o c_1$\
    $c_6 = c_5 plus.o c_2$\
    ...\
    Chaque bit de parité peut être calculé un par un en utilisant seulement les bits du message et le bit de parité juste avant lui.
  ]

  == Graphe de Tanner

  Deux ensembles de sommets :
  - $G = (X union.sq Y, A)$ biparti.
  - $X = {text("bits du codeword")}$ et $|X| = n$
  - $Y = {text("équations de parité")}$ (_check-nodes_) et $|Y| = m$
  - Soit $x in X, y in Y$, alors il existe $(x, y) in A$ si le bit est inclu dans l'équation de parité corerspondante.
  - $|A| =$ nombre de $1$ dans la matrice de parité.

  *Exemple :*
  Pour la matrice _parity-check_ suivante (régulière $w_c=2$, $w_r=3$)
  $
    H = mat(
      1, 1, 0, 1, 0, 0;
      0, 1, 1, 0, 1, 0;
      1, 0, 0, 0, 1, 1;
      0, 0, 1, 1, 0, 1
    )
  $

  Le graphe de Tanner correspondant

  #align(center)[
    #cetz.canvas(length: 1.2cm, {
      import cetz.draw: *

      content((0, 6.5), [*Noeuds de bits* \ (Variables $s_i$)], anchor: "south")
      content(
        (4, 6.5),
        [*Noeuds de parité* \ (Contraintes $f_j$)],
        anchor: "south",
      )

      for i in range(1, 7) {
        circle((0, 6.5 - i), radius: 0.3, name: "b" + str(i), fill: white)
        content("b" + str(i), [$s_#i$])
      }

      for j in range(1, 5) {
        let y = 6.5 - j * 1.2
        rect(
          (4 - 0.3, y - 0.3),
          (4 + 0.3, y + 0.3),
          name: "f" + str(j),
          fill: rgb("e0e0e0"),
        )
        content("f" + str(j), [$f_#j$])
      }

      line("b1", "f1", stroke: (paint: gray, thickness: 2pt))
      line("b2", "f1", stroke: (paint: gray, thickness: 2pt))
      line("b4", "f1")

      line("b2", "f2", stroke: (paint: gray, thickness: 2pt))
      line("b3", "f2")
      line("b5", "f2", stroke: (paint: gray, thickness: 2pt))

      line("b1", "f3", stroke: (paint: gray, thickness: 2pt))
      line("b5", "f3", stroke: (paint: gray, thickness: 2pt))
      line("b6", "f3")

      line("b3", "f4")
      line("b4", "f4")
      line("b6", "f4")
    })
  ]


  - *_Girth_* : La taille du plus petit cycle présent dans tout le graphe. (biparti $=>$ cycle de taille pair donc plus petit cycle de taille $4$).

  - *Exemple :*
  Dans notre $H$ le plus petit cycle est de taille 6 en gris.
  $c = s_1 -> f_1 -> s_2 -> f_2 -> s_5 -> f_3 -> s_1$.

  On veut éviter les cycles courts, il existe un algorithme de Mackay Neal qui permet de d'éviter les $4$-cycles. algorithme @johnson p14. D'autre methode vont être vu (algébriques) pour des court codes.


  == Encodage

  Matrice _parity-check_
  $ H = [A, I_(n-k)] $ avec $A in M_((n-k) times k)(ZZ\/2ZZ)$ on trouve alors $G$ par réducation de Gausse-Jordan sur $H$.

  $
    G = [I_k, A^T]
  $

  1. Mettre $H$ sous forme échelonée puis sous forme ligne-cheloné réduite puis sous forme standard (avec des permutation de colonnes) puis construire $G$ voir @johnson p15. Cette méthode ne rend pas la matrice creuse donc complexité nul.
  2. Transformer $H$ en matrice triangulaire inférieur aproximative. Voir @johnson p17.


  == Décodage

  Voir les exemple du papier @johnson p21.
]

#pagebreak()

= QC-LDPC

= Code Rust
#pad(left: 1cm)[
  - Voir la strucutre du projet en amont
  - Implémentation des codes ldpc (avec un peu toutes les méthodes possible pour faire des test etc)
    - Encodage Implémentation avec $H$ aléatoire faire Gausse-Jordan pour trouver $G$ et $s = u G$
    - Decodage par bit-flipping
    - ---
    - Decodage sum product
    - Hashmap pour ne pas avoir à recalculer $G$ pour toutes les longueurs de message. (à $H$ fixé)
    - Implémenter le "repeat-accumulate" (RA) compliqué
  - Et faire un export pour visualiser les matrice et le graphe dans un fichier typst par exemple ou autre
  - Implementation en rust https://github.com/daniestevez/ldpc-toolbox (lire le code pour voir)
  - Voir SIMD (`std::simd`) pour le calcul de LLR sur plusieur bits en même temps.
  - Voir le multi-threadage possible (après l'implementation)
]

#pagebreak()
#bibliography("sources.yml", style: "ieee")
