ldpc_decoder.f90 Source File


This file depends on

sourcefile~~ldpc_decoder.f90~~EfferentGraph sourcefile~ldpc_decoder.f90 ldpc_decoder.f90 sourcefile~ldpc_edge_list.f90 ldpc_edge_list.f90 sourcefile~ldpc_decoder.f90->sourcefile~ldpc_edge_list.f90

Source Code

! SPDX-License-Identifier: GPL-3.0-or-later
! Copyright (C) 2024  Marco Origlia

!    This program is free software: you can redistribute it and/or modify
!    it under the terms of the GNU General Public License as published by
!    the Free Software Foundation, either version 3 of the License, or
!    (at your option) any later version.

!    This program is distributed in the hope that it will be useful,
!    but WITHOUT ANY WARRANTY; without even the implied warranty of
!    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
!    GNU General Public License for more details.

!    You should have received a copy of the GNU General Public License
!    along with this program.  If not, see <https://www.gnu.org/licenses/>.
module ldpc_decoder
  !! author: Marco Origlia
  !! license: GPL-3.0-or-later
  !!
  !! This module provides the TDecoder class, capable of
  !! performing Gallager's SPA decoding.
  use ldpc_edge_list, only: TEdgeList
  use iso_c_binding, only: wp => c_double
  implicit none

  public :: TDecoder

  type, public :: TDecoder
     !! LDPC Decoder object
     integer :: cnum
     !! Number of Check Nodes
     integer :: vnum
     !! Number of variable nodes
     integer :: Ne
     !! Number of edges
     type(TEdgeList), allocatable :: c_to_e(:)
     !! For each check node, the list of its connected edges
     type(TEdgeList), allocatable :: v_to_e(:)
     !! For each variable node, the list of its connected edges
     type(TEdgeList), allocatable :: c_to_v(:)
     !! For each check node, the list of its connected variable nodes
     type(TEdgeList), allocatable :: v_to_c(:)
     !! For each variable node, the list of its connected check nodes

     real(wp), allocatable :: B_buffer(:)
     !! Support buffer for node processing (backwards partial results)
     real(wp), allocatable :: F_buffer(:)
     !! Support buffer for node processing (forward partial results)
   contains
     final :: destructor
     procedure, pass(this), public :: print => print_decoder
     procedure, pass(this), public :: check_llr
     procedure, pass(this), public :: word_to_synd
     procedure, pass(this), public :: decode
  end type TDecoder

  interface TDecoder
     module procedure TDecoderConstructor
  end interface TDecoder

contains
  ! ------------------------------
  ! --- Construction and setup ---
  ! ------------------------------
  function TDecoderConstructor(N, e_to_v, e_to_c) result(decoder)
    type(TDecoder) :: decoder
    !! The resulting Decoder object
    integer, intent(in) :: N
    !! Number of edges
    integer, intent(in) :: e_to_c(N)
    !! each location contains the index of the check node
    !! connected to the edge corresponding to that location
    integer, intent(in) :: e_to_v(N)
    !! each location contains the index of the variable node
    !! connected to the edge corresponding to that location

    integer :: j, i, max_buff

    decoder%cnum = maxval(e_to_c) + 1
    decoder%vnum = maxval(e_to_v) + 1
    decoder%Ne   = N

    allocate(decoder%c_to_e(decoder%cnum))
    allocate(decoder%v_to_e(decoder%vnum))
    call invert_table(N, e_to_c, decoder%cnum, decoder%c_to_e)
    call invert_table(N, e_to_v, decoder%vnum, decoder%v_to_e)

    allocate(decoder%c_to_v(decoder%cnum))
    allocate(decoder%v_to_c(decoder%vnum))
    call edge_to_node_convert_table(decoder%cnum, decoder%c_to_e, N, e_to_v, decoder%c_to_v)
    call edge_to_node_convert_table(decoder%vnum, decoder%v_to_e, N, e_to_c, decoder%v_to_c)

    max_buff = 0
    do i = 1, decoder%cnum
       max_buff = max(max_buff, decoder%c_to_e(i)%N)
    end do
    do i = 1, decoder%vnum
       max_buff = max(max_buff, decoder%v_to_e(i)%N)
    end do
    allocate(decoder%B_buffer(2:max_buff))
    allocate(decoder%F_buffer(max_buff-1))
  end function TDecoderConstructor

  subroutine invert_table(N, e_to_x, xnum, x_to_e)
    !! Create an array which contains, in each location, the list of edges
    !! connected to the node corresponding to that location
    integer, intent(in) :: N
    !! Number of edges
    integer, intent(in) :: e_to_x(N)
    !! Array of nodes to which each edge is connected
    integer, intent(in) :: xnum
    !! Total number of nodes
    type(TEdgeList), intent(inout) :: x_to_e(xnum)
    !! Array of list of edges, one list for each node

    integer :: xcount(xnum)
    integer :: i, j

    xcount(:) = 0
    do i = 1, N
       xcount(e_to_x(i)+1) = xcount(e_to_x(i)+1) + 1
    end do

    do i = 1, xnum
       x_to_e(i) = TEdgeList(xcount(i))
    end do

    do i = N, 1, -1
       j = e_to_x(i) + 1
       x_to_e(j)%data(xcount(j)) = i
       xcount(j) = xcount(j) - 1
    end do

  end subroutine invert_table


  subroutine edge_to_node_convert_table(N, x_to_e, Ne, e_to_y, x_to_y)
    !! Build node-to-node connection list from node-to-edge and edge-to-other-node
    integer, intent(in) :: N
    !! Number of nodes
    type(TEdgeList), intent(in) :: x_to_e(N)
    !! Array with the list of connected edges for each node
    integer, intent(in) :: Ne
    !! Total number of edges
    integer, intent(in) :: e_to_y(Ne)
    !! Array with nodes (of the other kind) connected to each edge
    type(TEdgeList), intent(inout) :: x_to_y(N)
    !! Array with the list of connected nodes from the original nodes
    !! i.e. from variable- to check-nodes or vice versa

    integer :: i, j

    do i = 1, N
       x_to_y(i) = TEdgeList(x_to_e(i)%N)
       do j = 1, x_to_y(i)%N
          x_to_y(i)%data(j) = 1 + e_to_y(x_to_e(i)%data(j))
       end do
    end do
  end subroutine edge_to_node_convert_table


  subroutine destructor(decoder)
    !! Destructor method for the TDecoder object
    !! It will just deallocate whatever was allocated.
    !!
    !! It should be called automatically.
    type(TDecoder) :: decoder

    if (allocated(decoder%c_to_e)  ) deallocate(decoder%c_to_e)
    if (allocated(decoder%v_to_e)  ) deallocate(decoder%v_to_e)
    if (allocated(decoder%v_to_c)  ) deallocate(decoder%v_to_c)
    if (allocated(decoder%c_to_v)  ) deallocate(decoder%c_to_v)
    if (allocated(decoder%F_buffer)) deallocate(decoder%F_buffer)
    if (allocated(decoder%B_buffer)) deallocate(decoder%B_buffer)
  end subroutine destructor

  subroutine print_decoder(this)
    class(TDecoder) :: this
    integer :: i
    print *, "TDecoder"
    print *, "    CNum =", this%cnum
    print *, "    VNum =", this%vnum
    print *, "    ENum =", this%Ne
  end subroutine print_decoder


  ! --------------------------------
  ! --- Message passing routines ---
  ! --------------------------------

  subroutine process_xnode(buffer_x_to_y, Ne, m_inout, f_plus_kind, total, B_buffer, F_buffer)
    !! Generic processing function, it works both for variable- and for check- nodes
    !! - for variable nodes, f_plus_kind is a simple addition routine
    !! - for check nodes, f_plus_kind can be:
    !!     + the "box plus" for exact message passing
    !!     + the min abs with sign product for the min-sum algorithm
    !!     + whatever you want for your approximation
    type(TEdgeList), intent(in) :: buffer_x_to_y
    !! List of edges "y" connected to node "x"
    integer, intent(in)       :: Ne
    !! Number of edges
    real(wp), intent(inout) :: m_inout(Ne)
    !! Input messages, updated with the output messages
    interface
       pure function f_real_real(x, y) result (z)
         import wp
         real(wp), intent(in) :: x
         real(wp), intent(in) :: y
         real(wp)             :: z
       end function f_real_real
    end interface
    procedure(f_real_real) :: f_plus_kind
    !! Function with 2 real inputs and one real output
    !! used to perform the combination of input messages
    !! to output messages
    real(wp), intent(out), optional  :: total
    !! f_plus_kind applied to all input messages
    real(wp), intent(inout), target, optional  :: B_buffer(2:buffer_x_to_y%N)
    !! pre-allocated backwords buffer for partial message processing results
    real(wp), intent(inout), target, optional  :: F_buffer(buffer_x_to_y%N-1)
    !! pre-allocated forward buffer for partial message processing results


    real(wp), pointer :: buffer_fwd(:)
    real(wp), pointer :: buffer_bwd(:)

    integer :: i, j, N

    N = buffer_x_to_y%N

    ! Initialize buffers
    if (present(B_buffer)) then
       buffer_bwd => B_buffer
    else
       allocate(buffer_bwd(2:N))
    end if

    if (present(F_buffer)) then
       buffer_fwd => F_buffer
    else
       allocate(buffer_fwd(N-1))
    end if

    ! Fill the forward buffer
    buffer_fwd(1) = m_inout(buffer_x_to_y%data(1))
    j = 1
    do i=2, N - 1
       buffer_fwd(i) = f_plus_kind(buffer_fwd(j), m_inout(buffer_x_to_y%data(i)))
       j = i
    end do

    ! Fill the total "sum"
    if (present(total)) then
       total = f_plus_kind(buffer_fwd(N-1), m_inout(buffer_x_to_y%data(N)))
    end if

    ! Fill the backwards buffer
    buffer_bwd(N) = m_inout(buffer_x_to_y%data(N))
    j = N
    do i = N - 1, 2, -1
       buffer_bwd(i) = f_plus_kind(buffer_bwd(j), m_inout(buffer_x_to_y%data(i)))
       j = i
    end do

    ! Update the messages over the edges connected to this node
    m_inout(buffer_x_to_y%data(1)) = buffer_bwd(2)
    do i = 2, buffer_x_to_y%N-1
       m_inout(buffer_x_to_y%data(i)) = f_plus_kind(buffer_fwd(i-1), buffer_bwd(i+1))
    end do
    m_inout(buffer_x_to_y%data(N)) = buffer_fwd(N-1)

    ! Cleanup
    if (.not. present(B_buffer)) then
       deallocate(buffer_bwd)
    end if
    if (.not. present(F_buffer)) then
       deallocate(buffer_fwd)
    end if
  end subroutine process_xnode

  pure function f_plus_add(x, y) result (z)
    !! Real sum
    real(wp), intent(in) :: x
    real(wp), intent(in) :: y
    real(wp)             :: z

    z = x + y
  end function f_plus_add


  pure function f_plus_box(x, y) result (z)
    !! Box-plus sum \(z = 2\tanh^{-1}(\tanh(x/2) \cdot \tanh(y/2))\)
    real(wp), intent(in) :: x
    real(wp), intent(in) :: y
    real(wp)             :: z

    z = sign(min(abs(x), abs(y)), x*y) + &
         log(1 + exp(-abs(x+y))) - &
         log(1 + exp(-abs(x-y)))
  end function f_plus_box

  subroutine process_vnode(buffer_v_to_e, llr_channel, llr_updated, Ne, m_inout, B_buffer, F_buffer)
    !! Variable node A Posteriori Probability (APP) processing function
    type(TEdgeList), intent(in) :: buffer_v_to_e
    !! List of edges connected to the variable node being currently processed
    real(wp), intent(in)  :: llr_channel
    !! Log-Likelihood ratio (or Log-A Posteriori Probability Ratio)
    !! of the variable node being currently processed
    real(wp), intent(out) :: llr_updated
    !! Log-Likelihood ratio (or Log-A Posteriori Probability Ratio)
    !! of the variable node after processing
    integer, intent(in)   :: Ne
    !! Total number of edges in the LDPC code
    real(wp), intent(inout) :: m_inout(Ne)
    !! Array of input messages, to be updated with output messages, one per edge
    real(wp), intent(inout), target, optional :: B_buffer(2:buffer_v_to_e%N)
    !! pre-allocated backwords buffer for partial message processing results
    real(wp), intent(inout), target, optional :: F_buffer(buffer_v_to_e%N-1)
    !! pre-allocated forward buffer for partial message processing results

    real(wp) :: total
    integer :: i

    if (buffer_v_to_e%N > 1) then
       if (present(B_buffer) .and. present(F_buffer)) then
          call process_xnode(buffer_v_to_e, Ne, m_inout, f_plus_add, total, B_buffer, F_buffer)
       else
          call process_xnode(buffer_v_to_e, Ne, m_inout, f_plus_add, total)
       end if
    else
       total = m_inout(buffer_v_to_e%data(1))
       m_inout(buffer_v_to_e%data(1)) = 0
    end if

    do i = 1, buffer_v_to_e%N
       m_inout(buffer_v_to_e%data(i)) = m_inout(buffer_v_to_e%data(i)) + llr_channel
    end do

    llr_updated = total + llr_channel
  end subroutine process_vnode


  subroutine process_cnode(buffer_c_to_e, s, Ne, m_inout, B_buffer, F_buffer)
    !! Variable node A Posteriori Probability (APP) processing function
    type(TEdgeList), intent(in) :: buffer_c_to_e
    !! List of edges connected to the variable node being currently processed
    logical, intent(in)   :: s
    !! Syndrome bit value associated to the checknode being currently processed
    integer, intent(in)   :: Ne
    !! Total number of edges in the LDPC code
    real(wp), intent(inout) :: m_inout(Ne)
    !! Message buffer
    real(wp), intent(inout), target, optional :: B_buffer(2:buffer_c_to_e%N)
    !! pre-allocated backwords buffer for partial message processing results
    real(wp), intent(inout), target, optional :: F_buffer(buffer_c_to_e%N-1)
    !! pre-allocated forward buffer for partial message processing results

    integer :: i
    real(wp) :: sign_factor

    if (s) then
       sign_factor = -1.0_wp
    else
       sign_factor = 1.0_wp
    end if

    if (buffer_c_to_e%N < 2) then
       ! Very unlikely code design: it means a specific bit is known through the syndrome
       m_inout(buffer_c_to_e%data(1)) = sign_factor * 1e300_wp
       return
    end if

    if (present(B_buffer) .and. present(F_buffer)) then
       call process_xnode(buffer_c_to_e, Ne, m_inout, f_plus_box, B_buffer=B_buffer, F_buffer=F_buffer)
    else
       call process_xnode(buffer_c_to_e, Ne, m_inout, f_plus_box)
    end if

    do i = 1, buffer_c_to_e%N
       m_inout(buffer_c_to_e%data(i)) = m_inout(buffer_c_to_e%data(i)) * sign_factor
    end do
  end subroutine process_cnode


  logical function check_llr(this, llr, synd)
    !! Check the array of LLRs (or LAPPRs) against the syndrome
    class(TDecoder) :: this
    !! Decoder
    real(wp), intent(in) :: llr(this%vnum)
    !! Array of LLR (or LAPPR)
    logical, intent(in) :: synd(this%cnum)
    !! Syndrome

    logical :: p

    integer :: i, j

    check_llr = .true.

    checknode_loop : do i = 1, this%cnum
       p = .false.
       do j = 1, this%c_to_v(i)%N
          p = p .xor. (llr(this%c_to_v(i)%data(j)) < 0.0d0)
       end do

       if (xor(p, synd(i))) then
          check_llr = .false.
          exit checknode_loop
       end if
    end do checknode_loop
  end function check_llr

  ! --------------------------------------------------
  ! --- Helpers for orther programs and simulators ---
  ! --------------------------------------------------

  function word_to_synd(this, word) result(synd)
    !! Evaluate the syndrome from a word
    class(TDecoder) :: this
    !! Decoder
    logical, intent(in) :: word(this%vnum)
    !! Word
    logical :: synd(this%cnum)
    !! Syndrome

    integer :: i, j

    do i = 1, this%cnum
       synd(i) = .false.
       do j = 1, this%c_to_v(i)%N
          synd(i) = xor(synd(i), word(this%c_to_v(i)%data(j)))
       end do
    end do
  end function word_to_synd


  function llr_to_word(N, llr) result(word)
    !! Binarize the LLRs (or LAPPRs)
    integer, intent(in) :: N
    !! Word size
    real(wp), intent(in):: llr(N)
    !! LLR (or LAPPR)
    logical :: word(N)
    !! Word

    word = (llr < 0.0d0)
  end function llr_to_word


  ! ------------------------
  ! --- Decoding routine ---
  ! ------------------------
  subroutine decode(this, llr_channel, llr_updated, synd, N_iterations)
    !! Iteratively decode array of LLR (or LAPPRs) based on syndrome
    class(TDecoder)        :: this
    !! Decoder
    real(wp), intent(in)   :: llr_channel(this%vnum)
    !! Array of LLRs (or LAPPRs) obtained from the channel
    real(wp), intent(out)  :: llr_updated(this%vnum)
    !! Array of processed LLRs (or LAPPRs)
    logical, intent(in)    :: synd(this%cnum)
    !! Syndrome
    integer, intent(inout) :: N_iterations
    !! Maximum number of iterations (in), actual number of iterations (out)
    ! N_iterations is used to return the actual number of iterations

    real(wp) :: edge_messages(this%Ne)
    integer :: it, i

    if (this%check_llr(llr_channel, synd)) then
       llr_updated = llr_channel
       N_iterations = 0
       return
    end if

    edge_messages(:) = 0
    do i = 1, this%vnum
       ! First round to propagate the channel LLRs to the checknodes inputs
       call process_vnode(this%v_to_e(i), llr_channel(i), llr_updated(i), this%Ne, edge_messages)
    end do

    decoding_loop: do it = 1, N_iterations
       do i= 1, this%cnum
          call process_cnode(this%c_to_e(i), synd(i), this%Ne, edge_messages, &
               this%B_buffer, this%F_buffer)
       end do

       do i = 1, this%vnum
          call process_vnode(this%v_to_e(i), llr_channel(i), llr_updated(i), this%Ne, edge_messages, &
               this%B_buffer, this%F_buffer)
       end do

       if (this%check_llr(llr_updated, synd)) then
          ! Early stop if the current llr already satisfies the syndrome
          N_iterations = it
          exit decoding_loop
       end if
    end do decoding_loop
  end subroutine decode
end module ldpc_decoder