Программная реализация кодера свёрточного кода и декодера по алгоритму Витерби
Продолжим тему, начатую в двух предыдущих статьях:
- Кодирование и декодирование свёрточного кода по алгоритму Витерби
- Аппаратная реализация декодера свёрточного кода по алгоритму Витерби
Что такое свёрточные коды? Это дополнительная информация, которую добавляют в информационное сообщение, которая позволяет в определённых пределах восстанавливать сообщение при ошибках. Ошибки могут возникать при передаче в канале связи или частичном повреждении носителя данных (например, поцараапанный компакт-диск). В данной статье будет описана программная реализация на языке 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
Восстанавливающая способность свёрточного кода зависит от образующих многочленов, а также от расположения ошибок в битовом потоке, а точнее от их количества и битового расстояния между ошибками.
Иными словами, чем ближе ошибки друг к другу и чем больше их количество, тем меньше вероятность восстановить исходное сообщение. Так как наш генератор добавляет ошибки в случайные позиции сообщения, то при последовательном запуске с одинаковым процентом битовых ошибок можно получить разные результаты.

