Рейтинг@Mail.ru

Программная реализация кодера свёрточного кода и декодера по алгоритму Витерби

автор:
Be the first to comment! Программирование
Print Friendly, PDF & Email
Рассмотрим программную реализацию свёрточного кодера, а также декодера свёрточного кода по алгоритму Витерби.

Продолжим тему, начатую в двух предыдущих статьях:

  1. Кодирование и декодирование свёрточного кода по алгоритму Витерби
  2. Аппаратная реализация декодера свёрточного кода по алгоритму Витерби

Что такое свёрточные коды? Это дополнительная информация, которую добавляют в информационное сообщение, которая позволяет в определённых пределах восстанавливать сообщение при ошибках. Ошибки могут возникать при передаче в канале связи или частичном повреждении носителя данных (например, поцараапанный компакт-диск). В данной статье будет описана программная реализация на языке VB.NET сначала кодера свёрточного кода, а затем декодера свёрточного кода по описанному в первой статье алгоритму Витерби.

1 Кодер свёрточного кода

Сделаем кодер свёрточного кода параметризуемым. В конструкторе будем задавать образующие многочлены с возможностью выбора системы счисления. Кроме того, после внимательного изучения кодера и декодера, обнаруживается, что у них много общего. И это общее перенесём в базовый класс, от которого будут наследовать и кодер, и декодер.

Базовый класс для кодера и декодера Витерби (разворачивается)
Namespace Viterbi

    ''' <summary>
    ''' Базовый класс для кодера и декодера Витерби.
    ''' </summary>
    Public MustInherit Class ViterbiBase

#Region "CTOR"

        ''' <summary>
        ''' Конструктор кодера с указанием порождающих многочленов.
        ''' </summary>
        ''' <param name="polynoms">Массив порождающих многочленов (по умолчанию в ВОСЬМЕРИЧНОЙ системе счисления).</param>
        ''' <param name="base">Основание системы счисления, в которой указаны полиномы. Обычно 8 или 10.</param>
        Public Sub New(polynoms() As Integer, base As Integer)
            Select Case base
                Case 10
                    Me.Polynoms = polynoms
                Case Else
                    ReDim Me.Polynoms(polynoms.Length - 1)
                    For i As Integer = 0 To polynoms.Length - 1
                        Me.Polynoms(i) = ConvertFromTo(polynoms(i), base, 10)
                    Next
            End Select
        End Sub

#End Region '/CTOR

#Region "PROPS"

        ''' <summary>
        ''' Массив порождающих многочленов.
        ''' </summary>
        Public ReadOnly Property Polynoms As Integer()

        ''' <summary>
        ''' Разрядность кодера.
        ''' </summary>
        ''' <remarks>Соответствует числу полиномов.</remarks>
        Public ReadOnly Property CoderBitness As Integer
            Get
                Return Polynoms.Length
            End Get
        End Property

        ''' <summary>
        ''' Разрядность сдвигового регистра кодера.
        ''' </summary>
        Public ReadOnly Property RegisterLength As Integer
            Get
                If (_RegisterLength = -1) Then
                    Dim maxPolynom As Integer = Polynoms.Max() 'определяем разрядность регистра по наибольшему числу
                    Dim tmp As Integer = ConvertFromTo(maxPolynom, 10, 2, _RegisterLength) 'для определения разрядности регистра
                End If
                Return _RegisterLength
            End Get
        End Property
        Private _RegisterLength As Integer = -1

        ''' <summary>
        ''' Таблица возможных состояний кодера.
        ''' </summary>
        Public ReadOnly Property CoderStates As Dictionary(Of Integer, Integer)
            Get
                If (_CoderStates.Count = 0) Then
                    _CoderStates = CalculateCoderStates()
                End If
                Return _CoderStates
            End Get
        End Property
        Private _CoderStates As New Dictionary(Of Integer, Integer)

        ''' <summary>
        ''' Таблица переходов кодера.
        ''' </summary>
        ''' <remarks>
        ''' Пользуясь решёткой, легко проводить кодирование, не высчитывая каждый раз суммы по модулю два. 
        ''' Для этого нужно взять за исходное состояние узел 000 и далее перемещаться по стрелкам в один из двух узлов. 
        ''' Выбор следующего узла определяется кодируемым битом данных – если бит данных «1», то первый слева бит следующего узла должен быть «1», 
        ''' если кодируемый бит – «0», то первый слева бит следующего узла должен быть «0». 
        ''' Физически переход соответствует сдвигу в регистре кодера. 
        ''' Выходным значением кодера будет (<see cref="Transition.CoderOut"/>).
        ''' </remarks>
        Public ReadOnly Property Transitions As Lookup(Of Integer, Transition)
            Get
                If (_Transitions Is Nothing) Then
                    _Transitions = CalculateTransitionsGrid()
                End If
                Return _Transitions
            End Get
        End Property
        Private _Transitions As Lookup(Of Integer, Transition) = Nothing

        ''' <summary>
        ''' Число возможных состояний регистра.
        ''' </summary>
        ''' <remarks>Всего 2^k ветвей (число заваисит только от длины регистра).</remarks>
        Protected ReadOnly Property StatesCount As Integer
            Get
                If (_StatesCount = 0) Then
                    _StatesCount = CInt(Math.Pow(2, RegisterLength))
                End If
                Return _StatesCount
            End Get
        End Property
        Private _StatesCount As Integer = 0

        ''' <summary>
        ''' Число возможных переходов из узла в узел / число узлов в решётке переходов
        ''' </summary>
        ''' <remarks>Всего 2^(k-1) переходов (число заваисит только от длины регистра).</remarks>
        Protected ReadOnly Property TransitionsCount As Integer
            Get
                If (_TransitionNodesCount = 0) Then
                    _TransitionNodesCount = CInt(Math.Pow(2, RegisterLength - 1))
                End If
                Return _TransitionNodesCount
            End Get
        End Property
        Private _TransitionNodesCount As Integer = 0

#End Region '/PROPS

#Region "METHODS"

        ''' <summary>
        ''' Возвращает список образующих многочленов в восьмеричной форме.
        ''' </summary>
        Public Overloads Function ToString() As String
            Dim sb As New Text.StringBuilder()
            sb.Append("(")
            For Each p As Integer In Polynoms
                sb.Append($"{p.ToOctal()},")
            Next
            sb = sb.Remove(sb.Length - 1, 1)
            sb.Append(")")
            Return sb.ToString()
        End Function

#End Region '/METHODS

#Region "CLOSED METHODS"

        ''' <summary>
        ''' Рассчитывает таблицу всех возможных состояний кодера.
        ''' </summary>
        ''' <remarks>Значение состояния определяется по его индексу.</remarks>
        Private Function CalculateCoderStates() As Dictionary(Of Integer, Integer)
            Dim states As New Dictionary(Of Integer, Integer) 'ключ словаря = номер состояния, значение = само состояние
            states.Add(0, 0) 'исходное состояние

            For stateNumber As Integer = 1 To StatesCount - 1 'начинаем с 1-го, т.к. 0-ое добавили выше
                Dim adderOuts As New List(Of Boolean)(Polynoms.Length) 'возможные значения выхода - по количеству сумматоров (или полиномов)
                For Each polynom As Integer In Polynoms 'рассчитываем каждое значение выхода: XORим между собой те биты содержимого регистра, на которые указывают "1" полинома
                    Dim adderOut As Boolean = False 'состояние выхода сумматора
                    Dim register As New BitArray({stateNumber}) 'содержимое регистра кодера = значение номера состояния
                    For i As Integer = 0 To RegisterLength - 1 'идём по всем битам регистра кодера
                        Dim polynomBit As Boolean = (((polynom >> i) And 1) > 0) 'i-ый бит полинома
                        If polynomBit Then 'если бит "1", то XORим значение i-го бита регистра с предыдущим значением выхода сумматора
                            adderOut = adderOut Xor register(i)
                        End If
                    Next
                    adderOuts.Add(adderOut)
                Next

                'Объединяем все выходы в одно состояние:
                Dim state As Integer = 0
                For Each adderOut In adderOuts
                    Dim currentBit As Integer = (CInt(adderOut) And 1)
                    state = (state << 1) Or currentBit
                Next

                states.Add(stateNumber, state)
            Next
            Return states
        End Function

        ''' <summary>
        ''' Рассчитывает решётку переходов.
        ''' </summary>
        Private Function CalculateTransitionsGrid() As Lookup(Of Integer, Transition)
            Dim transitions As New List(Of Transition)(TransitionsCount)
            For node As Integer = 0 To TransitionsCount - 1 'все числа от 0 до 2^(k-1)-1 - это значения узлов
                For mostSignificantBit As Integer = 0 To 1
                    Dim reg As Integer = node Or (mostSignificantBit << (RegisterLength - 1)) 'старший бит заменяем "0" или "1"
                    Dim coderOut As Integer = CoderStates(reg)
                    Dim destinationNode As Integer = reg >> 1
                    transitions.Add(New Transition(RegisterLength, Polynoms.Count, node, destinationNode, coderOut))
                Next
            Next
            Dim ilu As ILookup(Of Integer, Transition) = transitions.ToLookup(Of Integer, Transition)(
                Function(trans) trans.SourceNode, 'ключ - значение стартового узла
                Function(trans) trans 'значение - параметры (узел назначения, выход кодера)
                )
            Dim lu As Lookup(Of Integer, Transition) = CType(ilu, Lookup(Of Integer, Transition))
            Return lu
        End Function

        ''' <summary>
        ''' Выбирает узел решётки, в который нужно идти при кодировании бита <paramref name="bitToEncode"/>.
        ''' </summary>
        ''' <param name="bitToEncode">Очередной кодируемый бит сообщения.</param>
        ''' <param name="currentTransitions">Массив возможных переходов из текущего узла.</param>
        Protected Function GetNextTransition(bitToEncode As Boolean, currentTransitions As IEnumerable(Of Transition)) As Transition
            For Each t In currentTransitions
                Select Case bitToEncode
                    Case True 'если бит "1", то переход в узел со старшим битом "1"
                        If t.DestinationNodeMsb Then
                            Return t
                        End If
                    Case False 'если бит "0", то переход в узел со старшим битом "0"
                        If (Not t.DestinationNodeMsb) Then
                            Return t
                        End If
                End Select
            Next
            Return Nothing
        End Function

#End Region '/CLOSED METHODS

    End Class '/ViterbiBase

    ''' <summary>
    ''' Параметры перехода кодера.
    ''' </summary>
    Public Class Transition

#Region "CTOR"

        ''' <summary>
        ''' 
        ''' </summary>
        ''' <param name="registerBitness">Разрядность регистра.</param>
        ''' <param name="coderBitness">Разрядность кодера. Совпадает с числом порождающих многочленов кодера.</param>
        ''' <param name="srcNode">Исходящий узел.</param>
        ''' <param name="destNode">Входящий узел.</param>
        ''' <param name="coderOut">Значение на выходе кодера.</param>
        Public Sub New(registerBitness As Integer, coderBitness As Integer, srcNode As Integer, destNode As Integer, coderOut As Integer)
            Me.RegisterBitness = registerBitness
            Me.CoderBitness = coderBitness
            Me.SourceNode = srcNode
            Me.DestinationNode = destNode
            Me.CoderOut = coderOut
        End Sub

#End Region '/CTOR

#Region "PROPS"

        ''' <summary>
        ''' Значение узла, из которого осуществляется переход.
        ''' </summary>
        Public ReadOnly Property SourceNode As Integer

        ''' <summary>
        ''' Значение узла, в который возможен переход.
        ''' </summary>
        Public ReadOnly Property DestinationNode As Integer

        ''' <summary>
        ''' Выход кодера в виде битов.
        ''' </summary>
        Public ReadOnly Property CoderOutBits As Boolean()
            Get
                If (_CoderOutBits.Length = 0) Then
                    ReDim _CoderOutBits(CoderBitness - 1)
                    For i As Integer = 0 To CoderBitness - 1
                        Dim shift As Integer = CoderBitness - 1 - i
                        _CoderOutBits(i) = CBool((CoderOut >> shift) And 1)
                    Next
                End If
                Return _CoderOutBits
            End Get
        End Property
        Private _CoderOutBits() As Boolean = {}

        ''' <summary>
        ''' Старший бит узла назначения.
        ''' </summary>
        ''' <remarks>Используется при принятии решения о переходе.</remarks>
        Public ReadOnly Property DestinationNodeMsb As Boolean
            Get
                If (_DestinationNodeMsb Is Nothing) Then
                    Dim firstBit As Integer = DestinationNode >> (RegisterBitness - 2)
                    _DestinationNodeMsb = (firstBit > 0)
                End If
                Return _DestinationNodeMsb.Value
            End Get
        End Property
        Private _DestinationNodeMsb As Boolean? = Nothing

        ''' <summary>
        ''' Метрика пути.
        ''' </summary>
        ''' <remarks>Используется декодером.</remarks>
        Public Property Metrics As Integer = 0

#End Region '/PROPS

#Region "FIELDS"

        ''' <summary>
        ''' Разрядность кодера. Соответствует числу образующих многочленов.
        ''' </summary>
        Private ReadOnly CoderBitness As Integer

        ''' <summary>
        ''' Разрядность регистра.
        ''' </summary>
        Private ReadOnly RegisterBitness As Integer

        ''' <summary>
        ''' Значение на выходе кодера на данном переходе.
        ''' </summary>
        Private ReadOnly CoderOut As Integer

#End Region '/FIELDS

#Region "METHODS"

        ''' <summary>
        ''' Клонирует переход.
        ''' </summary>
        Public Function Clone() As Transition
            Dim cloned As Transition = CType(Me.MemberwiseClone(), Transition)
            cloned.Metrics = Me.Metrics
            Return cloned
        End Function

        Public Overloads Function ToString() As String
            Dim sb As New Text.StringBuilder()
            sb.Append(Convert.ToString(SourceNode, 2).PadLeft(CoderBitness + 1, "0"c))
            sb.Append("; ")
            sb.Append(Convert.ToString(DestinationNode, 2).PadLeft(CoderBitness + 1, "0"c))
            sb.Append("; ")
            sb.Append($"({Metrics})")
            Return sb.ToString()
        End Function

#End Region '/METHODS

    End Class '/Transition

    ''' <summary>
    ''' Вспомогательные методы.
    ''' </summary>
    <Runtime.CompilerServices.Extension()>
    Public Module Helpers

        ''' <summary>
        ''' Преобразует число из десятичной системы счисления в число в двоичной системе счисления.
        ''' </summary>
        <Runtime.CompilerServices.Extension()>
        Public Function ToBinary(dec As Integer) As Integer
            Return ConvertFromTo(dec, 10, 2)
        End Function

        ''' <summary>
        ''' Преобразует число из десятичной системы счисления в число в восьмеричной системе счисления.
        ''' </summary>
        <Runtime.CompilerServices.Extension()>
        Public Function ToOctal(dec As Integer) As Integer
            Return ConvertFromTo(dec, 10, 8)
        End Function

        ''' <summary>
        ''' Преобразует число из восьмеричной системы счисления в число в десятичной системе счисления.
        ''' </summary>
        <Runtime.CompilerServices.Extension()>
        Public Function ToDecimal(oct As Integer) As Integer
            Return ConvertFromTo(oct, 8, 10)
        End Function

        ''' <summary>
        ''' Преобразует число из одной системы счисления в другую.
        ''' </summary>
        ''' <param name="number">Число в исходной системе счисления.</param>
        ''' <param name="fromBase">Исходная система счисления.</param>
        ''' <param name="toBase">Целевая система счисления.</param>
        ''' <param name="bitness">Выходное значение - разрядность в целевой системе счисления. Если не требуется, можно не передавать.</param>
        ''' <remarks>
        ''' Будьте внимательны: при переводе в двоичную систему может возникнуть ошибка переполнения, т.к. двоичные числа очень длинные.
        ''' Перевод в 16-ную систему и обратно не обрабатывается.
        ''' </remarks>
        Public Function ConvertFromTo(number As Integer, fromBase As Integer, toBase As Integer, Optional ByRef bitness As Integer = 0) As Integer
            Dim result As Integer = 0
            Dim i As Integer = 0
            Do While (number <> 0)
                result += (number Mod toBase) * CInt(Math.Pow(fromBase, i))
                number = CInt(Math.Truncate(number / toBase))
                i += 1
            Loop
            bitness = i
            Return result
        End Function

        ''' <summary>
        ''' Возвращает строку, представляющую двоичную последовательность переданной строки.
        ''' </summary>
        ''' <param name="s">Произвольная строка.</param>
        <Runtime.CompilerServices.Extension()>
        Public Function ToBinString(s As String) As String
            Dim messageBytes As Byte() = Text.Encoding.UTF8.GetBytes(s)
            Dim ba As New BitArray(messageBytes)
            Dim sb As New Text.StringBuilder()
            For Each b As Boolean In ba
                sb.Append(CInt(b) And 1)
            Next
            Return sb.ToString()
        End Function

        ''' <summary>
        ''' Возвращает строку, представляющую двоичную последовательность переданного массива битов.
        ''' </summary>
        ''' <param name="bits">Произвольная битовая последовательность.</param>
        <Runtime.CompilerServices.Extension()>
        Public Function ToBinString(bits As IEnumerable(Of Boolean)) As String
            Dim sb As New Text.StringBuilder()
            For Each b As Boolean In bits
                sb.Append(CInt(b) And 1)
            Next
            Return sb.ToString()
        End Function

        ''' <summary>
        ''' Преобразует строку битов в битовую последовательность.
        ''' </summary>
        ''' <param name="binaryString">Строка, составленная из "1" и "0".</param>
        <Runtime.CompilerServices.Extension()>
        Public Function FromBinString(binaryString As String) As List(Of Boolean)
            Dim bits As New List(Of Boolean)
            For Each c As Char In binaryString.ToCharArray()
                bits.Add(Boolean.Parse(c))
            Next
            Return bits
        End Function

        ''' <summary>
        ''' Преобразует битовую последовательность в строку ASCII.
        ''' </summary>
        ''' <param name="bits"></param>
        <Runtime.CompilerServices.Extension()>
        Public Function ToUtf8String(bits As IEnumerable(Of Boolean)) As String
            Dim bytes As New List(Of Byte)
            For i As Integer = 0 To bits.Count - 8 Step 8
                Dim int As Integer = 0
                For j As Integer = 0 To 7
                    int = int Or ((CInt(bits(i + j)) And 1) << j)
                Next
                bytes.Add(CByte(int))
            Next
            Dim s As String = Text.Encoding.UTF8.GetString(bytes.ToArray())
            Return s
        End Function

        ''' <summary>
        ''' Побитово сравнивает 2 массива.
        ''' </summary>
        ''' <param name="ar1"></param>
        ''' <param name="ar2"></param>
        <Runtime.CompilerServices.Extension()>
        Public Function BinaryEquals(ar1 As IEnumerable(Of Boolean), ar2 As IEnumerable(Of Boolean)) As Boolean
            If (ar1.Count <> ar2.Count) Then
                Return False
            End If
            For i As Integer = 0 To ar1.Count - 1
                If (ar1(i) <> ar2(i)) Then
                    Return False
                End If
            Next
            Return True
        End Function

         ''' <summary>
        ''' Добавляет в битовый поток <paramref name="message"/> заданный процент случайных ошибок.
        ''' </summary>
        ''' <param name="message">Исходное сообщение.</param>
        ''' <param name="errorsPercent">Процент ошибок, от 0 до 100.</param>
        Public Function GetMessageWithErrors(message As IEnumerable(Of Boolean), errorsPercent As Integer) As List(Of Boolean)
            Dim numErrors As Integer = CInt(Math.Truncate(message.Count * errorsPercent / 100))
            Dim errorsIndeces As New List(Of Integer)
            Dim rnd As New Random()
            For i As Integer = 0 To numErrors - 1
                Dim r As Integer = rnd.Next(0, message.Count)
                errorsIndeces.Add(r)
            Next
            Dim messageWithErrors As New List(Of Boolean)
            For i As Integer = 0 To message.Count - 1
                If errorsIndeces.Contains(i) Then
                    messageWithErrors.Add(Not message(i))
                Else
                    messageWithErrors.Add(message(i))
                End If
            Next
            Return messageWithErrors
        End Function

    End Module '/Helpers

End Namespace
    

Теперь напишем программный код кодера. Его листинг ниже:

Программный код кодера свёрточного кода (разворачивается)
Namespace Viterbi

    ''' <summary>
    ''' Кодер свёрточного кода.
    ''' </summary>
    Public Class Encoder
        Inherits ViterbiBase

#Region "CTOR"

        ''' <summary>
        ''' Конструктор кодера с указанием порождающих многочленов.
        ''' </summary>
        ''' <param name="polynoms">Массив порождающих многочленов (по умолчанию в ВОСЬМЕРИЧНОЙ системе счисления).</param>
        ''' <param name="base">Основание системы счисления, в которой указаны полиномы. Обычно 8 или 10.</param>
        Public Sub New(polynoms() As Integer, Optional base As Integer = 8)
            MyBase.New(polynoms, base)
        End Sub

#End Region '/CTOR

#Region "METHODS"

        ''' <summary>
        ''' Кодирует сообщение <paramref name="message"/>, заданное массивом битов.
        ''' </summary>
        ''' <remarks>
        ''' Пользуясь решёткой, легко проводить кодирование, не высчитывая каждый раз суммы по модулю два. 
        ''' Для этого нужно взять за исходное состояние узел 000 и далее перемещаться по стрелкам в один из двух узлов. 
        ''' Выбор следующего узла определяется кодируемым битом данных: если бит данных «1», то первый слева бит следующего узла должен быть «1», 
        ''' если кодируемый бит «0», то первый слева бит следующего узла должен быть «0». 
        ''' Физически операция перемещения по стрелке соответствует сдвигу в регистре кодера. 
        ''' Выходным значением кодера будут цифры, соответствующие стрелке.
        ''' </remarks>
        Public Function Encode(message As BitArray) As List(Of Boolean)
            Dim encodedMessage As New List(Of Boolean)
            Dim currentNode As Integer = 0 'начинаем с узла "0"
            For stepNum As Integer = 0 To message.Length - 1
                Dim nextTransition As Transition = GetNextTransition(message(stepNum), Transitions(currentNode))
                currentNode = nextTransition.DestinationNode
                encodedMessage.AddRange(nextTransition.CoderOutBits)
            Next
            Return encodedMessage
        End Function

        ''' <summary>
        ''' Кодирует сообщение, заданное строкой символов.
        ''' </summary>
        ''' <param name="message">Сообщение в виде текстовой строки.</param>
        ''' <remarks>Для преобразования в байты применяется кодировка UTF8.</remarks>
        Public Function Encode(message As String) As List(Of Boolean)
            Dim messageBytes As Byte() = Text.Encoding.UTF8.GetBytes(message)
            Dim ba As New BitArray(messageBytes)
            Dim encoded As List(Of Boolean) = Encode(ba)
            Return encoded
        End Function

        ''' <summary>
        ''' Кодирует поток <paramref name="streamToEncode"/> и сохраняет в поток <paramref name="encodedOutputStream"/>.
        ''' </summary>
        ''' <param name="streamToEncode">Двоичный поток, который будет побайтово прочитан и закодирован по алгоритму Витерби.</param>
        ''' <param name="encodedOutputStream">В этот поток (в текстовом!) виде будет сохранён результат.</param>
        Public Sub Encode(streamToEncode As IO.BinaryReader, encodedOutputStream As IO.StreamWriter)
            Do While (streamToEncode.PeekChar <> -1)
                Dim b As Byte = streamToEncode.ReadByte()
                Dim ba As New BitArray({b})
                Dim encodedBits As IEnumerable(Of Boolean) = Encode(ba)
                For Each bit As Boolean In encodedBits
                    encodedOutputStream.Write(CInt(bit) And 1)
                Next
            Loop
        End Sub

        ''' <summary>
        ''' Кодирует сообщение <paramref name="message"/>, заданное массивом битов.
        ''' </summary>
        Public Function Encode(message As IEnumerable(Of Boolean)) As List(Of Boolean)
            Dim b As New BitArray(message.ToArray())
            Return Encode(b)
        End Function

#End Region '/METHODS

    End Class '/Encoder

End Namespace
    

Использовать данный кодер предельно просто:

Dim coder As New Viterbi.Encoder({15, 17})
Dim encMessage = coder.Encode({True, False, True, False}) '1010 как пример в статье
Console.WriteLine($"Encoded ""1010"": {encMessage.ToBinString()}")

В конструкторе можно задавать любые полиномы в любом количестве:

Dim coder As New Viterbi.Encoder({7, 11, 13})

2 Декодер свёрточного кода по алгоритму Витерби

Теперь, когда есть кодер, можно взяться и за декодер.

Код декодера свёрточного кода по алгортиму Витерби (разворачивается)
Namespace Viterbi

    ''' <summary>
    ''' Декодер свёрточного кода по алгоритму Витерби.
    ''' </summary>
    Public Class Decoder
        Inherits ViterbiBase

#Region "CTOR"

        ''' <summary>
        ''' Конструктор с указанием порождающих многочленов.
        ''' </summary>
        ''' <param name="polynoms">Массив порождающих многочленов (по умолчанию в ВОСЬМЕРИЧНОЙ системе счисления).</param>
        ''' <param name="base">Основание системы счисления, в которой указаны полиномы. Обычно 8 или 10.</param>
        Public Sub New(polynoms() As Integer, Optional base As Integer = 8)
            MyBase.New(polynoms, base)
        End Sub

#End Region '/CTOR

#Region "METHODS"

        ''' <summary>
        ''' Декодирует битовый поток <paramref name="encodedMessage"/>.
        ''' </summary>
        ''' <param name="encodedMessage"></param>
        Public Function Decode(encodedMessage As IEnumerable(Of Boolean)) As Boolean()
            If (encodedMessage.Count Mod CoderBitness = 0) Then
                Dim paths As New PathHolder(StatesCount)
                'Посимвольно вычитываем из закодированного потока и декодируем:
                For symbol As Integer = 0 To encodedMessage.Count - CoderBitness Step CoderBitness
                    Dim encoderOutput(CoderBitness - 1) As Boolean
                    For bit As Integer = 0 To CoderBitness - 1
                        encoderOutput(bit) = encodedMessage(symbol + bit)
                    Next
                    UpdatePaths(encoderOutput, paths)
                Next
                Dim decodedMessage As Boolean() = ChooseFinalMessage(paths)
                Return decodedMessage
            End If
            Throw New ArgumentException($"Число бит в сообщении ({encodedMessage.Count}) не согласуется с параметрами декодера ({Me.ToString()}).")
        End Function

        ''' <summary>
        ''' Декодирует битовый поток <paramref name="encodedMessage"/>.
        ''' </summary>
        ''' <param name="encodedMessage"></param>
        Public Function Decode(encodedMessage As BitArray) As Boolean()
            Dim bools As New List(Of Boolean)
            For Each b As Boolean In encodedMessage
                bools.Add(b)
            Next
            Return Decode(bools)
        End Function

        ''' <summary>
        ''' Декодирует закодированную строку, состоящую из "1" и "0".
        ''' </summary>
        ''' <param name="binaryString">Строка из битов "1" и "0".</param>
        Public Function Decode(binaryString As String) As List(Of Boolean)
            Dim resBits As New List(Of Boolean)
            For i As Integer = 0 To binaryString.Length - CoderBitness Step CoderBitness
                Dim symbol(CoderBitness - 1) As Boolean
                For j As Integer = 0 To CoderBitness - 1
                    symbol(j) = Integer.Parse(binaryString(i + j)) > 0
                Next
                Dim decoded As Boolean() = Decode(symbol)
                resBits.AddRange(decoded)
            Next
            Return resBits
        End Function

        ''' <summary>
        ''' Декодирует битовый поток, прочитанный из текстового файла битового потока.
        ''' </summary>
        ''' <param name="streamToDecode"></param>
        ''' <param name="decodedOutputStream"></param>
        Public Sub Decode(streamToDecode As IO.TextReader, decodedOutputStream As IO.BinaryWriter)
            Dim byteSymbolLength As Integer = CoderBitness * 8
            Dim ar(0) As Byte
            Do While (streamToDecode.Peek <> -1)

                'Вычитываем по одному закодированному байту (в битах длина закодированного байта = 8 * разрядность_кодера):
                Dim encodedByte(byteSymbolLength - 1) As Boolean
                For i As Integer = 0 To byteSymbolLength - 1
                    Dim ch As Integer = streamToDecode.Read()
                    Dim bitc As Char = ChrW(ch)
                    Dim bit As Integer = Integer.Parse(bitc)
                    encodedByte(i) = CBool(bit)
                Next

                Dim decodedByte As Boolean() = Decode(encodedByte)
                Dim ba As New BitArray(decodedByte)
                ba.CopyTo(ar, 0)
                Dim b As Byte = ar(0)
                decodedOutputStream.Write(b)
            Loop
        End Sub

#End Region '/METHODS

#Region "CLOSED METHODS"

        ''' <summary>
        ''' Восстанавливает сообщение, выбирая его из финальных выживших путей кодера.
        ''' </summary>
        ''' <param name="encoderPaths"></param>
        Private Function ChooseFinalMessage(encoderPaths As PathHolder) As Boolean()
            Dim paths As IEnumerable(Of DecoderPath) = From p As DecoderPath In encoderPaths.Paths
                                                       Where (p.SurvivorPath.Count > 0)
                                                       Select p Order By p.Metrics 'путь с минимальной метрикой окажется вверху списка
            For Each path In paths
                Dim decoded(path.Count - 1) As Boolean
                For i As Integer = 0 To path.Count - 1
                    decoded(i) = path.SurvivorPath(i).DestinationNodeMsb
                Next
                Return decoded 'Если имеется несколько путей с одинаковой минимальной метрикой, выбирается первый, потом выходим из цикла.
            Next
            Return {}
        End Function

        ''' <summary>
        ''' Находит очередные выжившие пути декодера, отбрасывает невыжившие.
        ''' </summary>
        ''' <param name="encoderOut">Очередной символ закодированного сообщения на выходе кодера.</param>
        ''' <param name="paths">Пути декодера. Обновляемое значение.</param>
        Private Sub UpdatePaths(encoderOut As IEnumerable(Of Boolean), ByRef paths As PathHolder)
            Dim currentPaths As Transition() = CalculatePathMetrics(encoderOut, paths)
            Dim survivedPaths As Transition() = SelectSurvivedPaths(currentPaths)
            paths.AddSurvivorPath(survivedPaths)
        End Sub

        ''' <summary>
        ''' Рассчитывает метрики для всех путей.
        ''' </summary>
        ''' <param name="encoderOut">Очередной символ закодированного сообщения на выходе кодера.</param>
        ''' <param name="paths">Вычислитель и селектор путей декодера.</param>
        Private Function CalculatePathMetrics(encoderOut As IEnumerable(Of Boolean), ByRef paths As PathHolder) As Transition()
            Dim transitionsWithMetrics(StatesCount - 1) As Transition
            For i As Integer = 0 To StatesCount - 1
                Dim transCount As Integer = Transitions(i).Count()
                For j As Integer = 0 To transCount - 1
                    Dim trans As Transition = Transitions(i)(j)
                    Dim branchMetric As Integer = CalculateHammingDistance(trans.CoderOutBits, encoderOut)
                    Dim pathMetric As Integer = paths.GetPathMetricByNode(trans.SourceNode)
                    Dim t As Transition = trans.Clone()
                    t.Metrics = branchMetric + pathMetric
                    transitionsWithMetrics(i * transCount + j) = t
                Next
            Next
            Return transitionsWithMetrics
        End Function

        ''' <summary>
        ''' Выбирает и возвращает выжившие пути.
        ''' </summary>
        ''' <remarks>
        ''' Выбирает из 2-х конкурирующих путей, входящих в один узел, путь с минимальной метрикой.
        ''' Также здесь можно просмотреть невыживший путь, если это для чего-то нужно.
        ''' </remarks>
        Private Function SelectSurvivedPaths(currentPaths As Transition()) As Transition()
            Dim survivors(TransitionsCount - 1) As Transition
            For i As Integer = 0 To TransitionsCount - 1
                Dim prevDestNode As Integer = i
                Dim sameDestPaths As IEnumerable(Of Transition) = From t As Transition In currentPaths
                                                                  Where (t.DestinationNode = prevDestNode) Select t 'пути с одним узлом назначения
                Dim minMetricTransition As Transition = sameDestPaths.First() 'примем за минимальную метрику произвольный путь (например, 1-ый)
                For Each t As Transition In sameDestPaths
                    If (t.Metrics < minMetricTransition.Metrics) Then
                        minMetricTransition = t
                    End If
                Next
                survivors(i) = minMetricTransition
            Next
            Return survivors
        End Function

        ''' <summary>
        ''' Рассчитывает Хэмингово расстояние между несколькими битовыми комбинациями.
        ''' </summary>
        ''' <param name="bitCombination">Набор битовых комбинаций. Например, {0,1} и {1,1}. Разрядности комбинаций должны быть одинаковыми.</param>
        ''' <remarks>
        ''' Хэмингово расстояние рассчитывается следующим образом: 
        ''' принятая комбинация складывается по модулю два (ПОРАЗРЯДНО!) со сгенерированной комбинацией и далее рассчитывается количество единиц в получившемся значении. 
        ''' Например, хэмингово расстояние для двух комбинаций 01 и 00 рассчитывается следующим образом: 
        ''' количество единиц (0 xor 0; 1 xor 0) = количество единиц (01) = 1.
        ''' </remarks>
        Private Shared Function CalculateHammingDistance(ParamArray bitCombination() As IEnumerable(Of Boolean)) As Integer
            Dim sumOfOnes As Integer = 0
            For bit As Integer = 0 To bitCombination(0).Count - 1 'поразрядно 
                Dim resultBit As Boolean = False
                For comb As Integer = 0 To bitCombination.Count - 1 'для всех комбинаций
                    resultBit = resultBit Xor bitCombination(comb)(bit) 'считаем сумму по модуля 2
                Next
                If resultBit Then 'если 1, то инкрементируем сумму
                    sumOfOnes += 1
                End If
            Next
            Return sumOfOnes
        End Function

#End Region '/CLOSED METHODS

#Region "NESTED TYPES"

        ''' <summary>
        ''' Содержит полный набор выживших и невыживших путей декодера.
        ''' </summary>
        Private Class PathHolder

            ''' <summary>
            ''' Создаёт объект для хранения путей при декодировании.
            ''' </summary>
            ''' <param name="decoderStatesCount">Число состояний декодера. Число выживших и невыживших путей будет в 2 раза меньше.</param>
            Public Sub New(decoderStatesCount As Integer)
                Dim count As Integer = decoderStatesCount \ 2
                ReDim Paths(count - 1)
                For i As Integer = 0 To count - 1
                    Paths(i) = New DecoderPath()
                Next
            End Sub

#Region "PROPS"

            ''' <summary>
            ''' Массив путей декодера.
            ''' </summary>
            Public ReadOnly Property Paths As DecoderPath()

            ''' <summary>
            ''' Путь уже имеет узлы или ещё нет (флаги для выжившией и невыжевшей ветвей).
            ''' </summary>
            Private IsFirstNode As Boolean() = {True, True}

#End Region '/PROPS

#Region "METHODS"

            ''' <summary>
            ''' Добавляет выживший путь в <see cref="Paths"/>.
            ''' </summary>
            ''' <param name="survivor">Массив выживших переходов.</param>
            Public Sub AddSurvivorPath(ByRef survivor As Transition())
                If IsFirstNode(0) Then
                    AddSurvivorPathFirstly(survivor)
                Else
                    'Новый путь продолжает предыдущий => его начальный узел должен выходить из конечного узла предыдущего узла.
                    'Запоминаем соотвтествие индексов новых выживших уже существующим путям
                    '(если сразу вставлять элемент пути, коллекция Path изменялась бы, что привело бы к ошибкам):
                    Dim survivorIndeces(survivor.Length - 1) As Integer
                    Dim prevPathIndeces As New List(Of Integer)
                    For i As Integer = 0 To survivor.Length - 1
                        survivorIndeces(i) = i
                        For j As Integer = 0 To Paths.Count - 1
                            If (Paths(j).DestinationNode = survivor(i).SourceNode) Then
                                prevPathIndeces.Add(j)
                            End If
                        Next
                    Next

                    'Ключ - индекс текущего пути в Path, значения - новые выжившие пути:
                    Dim pathIndeces As ILookup(Of Integer, Integer) = survivorIndeces.ToLookup(Of Integer, Integer)(
                        Function(key) prevPathIndeces(key),
                        Function(key) key
                        )

                    'Запомним индексы путей, которые можно удалить:
                    Dim survivorsToRemove As New Queue(Of Integer)
                    For i As Integer = 0 To survivorIndeces.Length - 1
                        If (Not pathIndeces.Contains(i)) Then
                            survivorsToRemove.Enqueue(i)
                        End If
                    Next

                    'По сохранённым индексам добавим выжившие пути в Path:
                    For Each survIndeces In pathIndeces
                        Dim pathIndex As Integer = survIndeces.Key

                        'Если по ключу находится 2 узла, 2-ой вставляем по индексу невыжившего пути
                        If (survIndeces.Count > 1) Then
                            Dim survivorToRemove As Integer = survivorsToRemove.Dequeue()
                            Paths(survivorToRemove).SurvivorPath.Clear() 'удаляем невыживший путь
                            For Each node In Paths(pathIndex).SurvivorPath 'копируем текущий путь вместо удалённого
                                Paths(survivorToRemove).AddSurvivorNode(node)
                            Next
                            Paths(survivorToRemove).AddSurvivorNode(survivor(survIndeces(1)))
                        End If

                        '1-ый элемент добавляем как обычно:
                        If (survIndeces.Count >= 1) Then
                            Paths(pathIndex).AddSurvivorNode(survivor(survIndeces(0)))
                        End If
                    Next
                End If
            End Sub

            Private Sub AddSurvivorPathFirstly(ByRef survivors As Transition())
                For i As Integer = 0 To survivors.Length - 1
                    Paths(i).AddSurvivorNode(survivors(i)) 'первый узел добавляем без привязки к предыдущим в произвольный путь (пусть i-ый)
                Next
                IsFirstNode(0) = False
            End Sub

            ''' <summary>
            ''' Возвращает метрику пути по узлу назначения.
            ''' </summary>
            ''' <param name="node">Узел назначения, для пути которого нужна метрика.</param>
            Public Function GetPathMetricByNode(node As Integer) As Integer
                If (Paths(0).SurvivorPath.Count > 0) Then
                    For Each path As DecoderPath In Paths
                        Dim count As Integer = path.SurvivorPath.Count
                        If (path.SurvivorPath(count - 1).DestinationNode = node) Then
                            Return path.Metrics
                        End If
                    Next
                End If
                Return 0
            End Function

#End Region '/METHODS

        End Class '/PathHolder

        ''' <summary>
        ''' Полный путь декодирования.
        ''' </summary>
        Private Class DecoderPath

#Region "PROPS"

            ''' <summary>
            ''' Список выживших переходов при декодировании.
            ''' </summary>
            Public ReadOnly Property SurvivorPath As New List(Of Transition)

            ''' <summary>
            ''' Метрика последнего перехода всего выжившего пути.
            ''' </summary>
            Public ReadOnly Property Metrics As Integer
                Get
                    Return SurvivorPath(SurvivorPath.Count - 1).Metrics
                End Get
            End Property

            ''' <summary>
            ''' Узел назначения последнего перехода всего выжившего пути.
            ''' </summary>
            Public ReadOnly Property DestinationNode As Integer
                Get
                    Return SurvivorPath(SurvivorPath.Count - 1).DestinationNode
                End Get
            End Property

            Public ReadOnly Property Count As Integer
                Get
                    Return SurvivorPath.Count()
                End Get
            End Property

#End Region '/PROPS

#Region "METHODS"

            ''' <summary>
            ''' Добавляет узел к выжившему пути.
            ''' </summary>
            ''' <param name="node"></param>
            Public Sub AddSurvivorNode(node As Transition)
                SurvivorPath.Add(node)
            End Sub

            ''' <summary>
            ''' Возвращает список переходов. В конце - метрика пути.
            ''' </summary>
            Public Overloads Function ToString() As String
                Dim sb As New Text.StringBuilder()
                If (SurvivorPath.Count > 0) Then
                    sb.Append(Convert.ToString(SurvivorPath(0).SourceNode, 2).PadLeft(3, "0"c))
                    sb.Append("; ")
                    For Each t As Transition In SurvivorPath
                        sb.Append(Convert.ToString(t.DestinationNode, 2).PadLeft(3, "0"c))
                        sb.Append("; ")
                    Next
                    sb = sb.Remove(sb.Length - 2, 2)
                End If
                sb.Append($" ({Metrics})")
                Return sb.ToString()
            End Function

#End Region '/METHODS

        End Class '/DecoderPath

#End Region '/NESTED TYPES

    End Class '/Decoder

End Namespace
    

Использовать декодер тоже очень просто:

Dim decod As New Viterbi.Decoder({15, 17})
Dim restored1 = decod.Decode({True, False, True, False})
Dim restored2 = decod.Decode("1010")
Console.WriteLine($"Restored 1: {restored1.ToBinString()}")
Console.WriteLine($"Restored 2: {restored2.ToBinString()}")

Параметры декодера должны быть такими же, как параметры кодера при кодировании сообщения, естественно.

Последние обновления данного кода можно найти на Гитхаб автора.

3 Проверка восстанавливающей способности декодера свёрточного кода Витерби

Как уже упоминалось, свёрточные коды нужны для того чтобы восстанавливать битовые ошибки в сообщении. Давайте проверим восстанавливающую способность декодера с образующими многочленами (158, 178). Для этого в закодированное сообщение будем добавлять случайные битовые ошибки. Для генерации ошибок напишем такой метод:

''' <summary>
''' Добавляет в битовый поток <paramref name="message"/> заданный процент случайных ошибок.
''' </summary>
''' <param name="message">Исходное сообщение.</param>
''' <param name="errorsPercent">Процент ошибок, от 0 до 100.</param>
Public Function GetMessageWithErrors(message As IEnumerable(Of Boolean), errorsPercent As Integer) As List(Of Boolean)
    Dim numErrors As Integer = CInt(Math.Truncate(message.Count * errorsPercent / 100)) 'абсолютное кол-во ошибок, которое нужно добавить в сообщение
    Dim errorsIndeces As New List(Of Integer) 'случайные индексы сбойных битов
    Dim rnd As New Random()
    For i As Integer = 0 To numErrors - 1
        Dim r As Integer = rnd.Next(0, message.Count)
        errorsIndeces.Add(r) 'тут можно добавить проверку, чтобы не было повторяющихся индексов
    Next
    Dim messageWithErrors As New List(Of Boolean)
    For i As Integer = 0 To message.Count - 1
        If errorsIndeces.Contains(i) Then
            messageWithErrors.Add(Not message(i)) 'инвертируем бит - вводим ошибку в сообщение
        Else
            messageWithErrors.Add(message(i))
        End If
    Next
    Return messageWithErrors
End Function

Собственно, теперь проверим работу декодера:

Dim polys As Integer() = {15, 17}
Dim enc As New Viterbi.Encoder(polys)
Dim decod As New Viterbi.Decoder(polys)
Console.WriteLine($"Кодер, декодер {enc.ToString()}")    
    
Dim message2 As String = "Hello, Я Soltau.Ru! :)"
Console.WriteLine($"Message: {message2}")
encoded = enc.Encode(message2)

For percent As Integer = 0 To 16 Step 2
    Console.ForegroundColor = ConsoleColor.Yellow
    Console.WriteLine($"Errors: {percent}%".Insert(0, vbNewLine)) 'выводим текущий процент ошибок в сообщении
    Console.ForegroundColor = ConsoleColor.Gray
    Dim errEncoded = GetMessageWithErrors(encoded, percent) 'добавляем ошибки в закодированное сообщение
    Dim restored As IEnumerable(Of Boolean) = decod.Decode(errEncoded) 
    Console.WriteLine($"Encoded: {errEncoded.ToBinString()}") 'выводим закодированное сообщение с ошибками
    If (restored.ToUtf8String() = message2) Then 'зелёным цветом отметим успешно восстановленное сообщение
        Console.ForegroundColor = ConsoleColor.Green
    Else
        Console.ForegroundColor = ConsoleColor.Red 'красным - ошибки не парированы полностью
    End If
    Console.WriteLine($"Restord: {restored.ToUtf8String()}") 'выводим восстановленное сообщение
    Console.ForegroundColor = ConsoleColor.Gray
Next

Восстанавливающая способность свёрточного кода зависит от образующих многочленов, а также от расположения ошибок в битовом потоке, а точнее от их количества и битового расстояния между ошибками.

Иными словами, чем ближе ошибки друг к другу и чем больше их количество, тем меньше вероятность восстановить исходное сообщение. Так как наш генератор добавляет ошибки в случайные позиции сообщения, то при последовательном запуске с одинаковым процентом битовых ошибок можно получить разные результаты.

Демонстрация работы восстанавливающей способности декодера Витерби
Демонстрация работы восстанавливающей способности декодера Витерби
Last modified onВторник, 05 Апрель 2022 20:09 Read 6518 times
Ключевые слова: :

Поблагодарить автора:

Поделиться

Print Friendly, PDF & Email

Leave a comment