In [1]:
using PyPlot

Homework #6 Programming Assignment Solution

Problem #1

In this homework, we examine the properties of binary convolutional codes and implement Viterbi decoders. Note: in the following, we always assume the input is a single sequence of bits.

Basic functions

We will represent each state by string. For example, all zero state with memory length 2 corresponds to "00".

The following basic operations will be useful.

In [2]:
str2int(x) = (x=="0"?0:1)

array2str(A) = string(A...)

str2array(parsetype, s) = [parse(parsetype, x) for x in split(s,"")]'
Out[2]:
str2array (generic function with 1 method)

In Viterbi algorithm, when we find the best path, we need to track back the possible previous states.

In [3]:
prev(s, bit) = s[2:end]*bit
Out[3]:
prev (generic function with 1 method)
In [4]:
hammdist(s1,s2) = sum((str2array(Int, s1)+str2array(Int, s2))%2)
Out[4]:
hammdist (generic function with 1 method)

Functions related to channels:

In [5]:
bern(p) = rand(1)[1]<p ? 1 : 0

genRandBitVec(k) = [rand([0,1]) for x in 1:k]
Out[5]:
genRandBitVec (generic function with 1 method)
In [6]:
function BSC(c, p)
    # input: code vector c
    # output: noisy vector y
    y = str2array(Int, c)
    for i in 1:length(c)
        y[i] += bern(p)
    end
    return array2str(y % 2)
end

function AWGN(c, SNRdB)
    # input: code vector c
    # output: noisy vector y
    y = 2*str2array(Float64, c)-1  # maps (0,1) to (-1,1)
    N = 10^(-SNRdB/10)
    y += sqrt(N)*randn(length(y))'
    return y
end
Out[6]:
AWGN (generic function with 1 method)

(a)

Write a program for a function $\texttt{convEncode}$($\texttt{connections},$ $\texttt{inputbits}$) that takes an array of octal tap connections $\texttt{connections}$ and a sequence of bits $\texttt{inputbits}$ as inputs and outputs the encoded bit sequence. For example, for $\texttt{connections} = [\begin{matrix}4 & 5\end{matrix}]$ and $\texttt{inputbits} = 1001010001110,$ your function should output the bit sequence [ 11000111001000010011111001. ] Use this function to encode the bit sequence $111111111111111111111111111$ using

  • the convolutional code with generator matrix $G(D) = [\begin{matrix}1+D^2 & 1+D+D^2 \end{matrix}],$ i.e., the code with octal tap connections $[\begin{matrix}5 & 7 \end{matrix}].$
  • the convolutional code with generator matrix $G(D) = [\begin{matrix}1 & 1+D \end{matrix}],$ i.e., the code with octal tap connections $[\begin{matrix}2 & 3 \end{matrix}].$

First, given a tap-connections configuration, we first translate it into a matrix form.

In [7]:
octal2bin(oct) = *([bin(parse(Int, string(oct)[ii]), 3) for ii in 1:length(string(oct))]...)
Out[7]:
octal2bin (generic function with 1 method)
In [8]:
function connmatrix(connections)
    nummemory = Int(ceil(log2(maximum(connections))))-1
    connections_matrix = zeros(Int64, length(connections), nummemory+1)
    for ii in 1:length(connections)
        connections_matrix[ii, :] = str2array(Int, octal2bin(connections[ii]))[end-nummemory:end]
    end
    return connections_matrix
end
Out[8]:
connmatrix (generic function with 1 method)

For example,

In [9]:
connmatrix([12 13])
Out[9]:
2×4 Array{Int64,2}:
 1  0  1  0
 1  0  1  1

Using this function, we can easily implement the encoder.

In [10]:
function convEncode(connections, inputbits)
    nummemory = Int(ceil(log2(maximum(connections))))-1
    connections_matrix = connmatrix(connections)
    outputbits = []
    memory = zeros(Int64, nummemory+1)'
    for j = 1:length(inputbits)
        memory = [inputbits[j] memory[1:end-1]...]
        output = (memory*connections_matrix')%2
        append!(outputbits, output)
    end
    return array2str(outputbits)
end
Out[10]:
convEncode (generic function with 1 method)
In [11]:
connections = [4 5]
inputbits = str2array(Int, "1001010001110")
codebits = convEncode(connections, inputbits)
Out[11]:
"11000111001000010011111001"
In [12]:
connections = [5 7]
inputbits = str2array(Int, "111111111111111111111111111")
codebits = convEncode(connections, inputbits)
Out[12]:
"111001010101010101010101010101010101010101010101010101"
In [13]:
connections = [2 3]
inputbits = str2array(Int, "111111111111111111111111111")
codebits = convEncode(connections, inputbits)
Out[13]:
"111010101010101010101010101010101010101010101010101010"

(c)

Write a program for a function $\texttt{ViterbiBSC}$($\texttt{connections},$ $\texttt{receivedsequence}$) that takes an array of octal tap connections $\texttt{connections}$ of a convolutional code, as well as the received sequence when the code is used over a BSC with $p<1/2,$ and outputs the most likely input sequence using Viterbi decoding. Verify that this function works as expected for the code with connections $[\begin{matrix}5 & 7 \end{matrix}].$ You should initialize your decoder at the all-zero state.

(e)

Write a program for a function $\texttt{ViterbiAWGN}$($\texttt{connections},$ $\texttt{receivedsequence}$) that takes an array of octal tap connections $\texttt{connections}$ of a convolutional code, as well as the received sequence when the code is used over an AWGN, and outputs the most likely input sequence using Viterbi decoding. Here, we assume that BPSK with unit power is used for transmission, i.e., the symbol $0$ is mapped to $-1$ and $1$ is mapped to $1$ while transmitting over the channel. Verify that this function works as expected for the code with connections $[\begin{matrix}5 & 7 \end{matrix}].$ You should initialize your decoder at the all-zero state.

First we define one-step codebook for a given encoder to ease the decoding process.

In [14]:
function convEncodeOneStep(connections, state, input)
    s = [parse(Int, ss) for ss in split(state,"")]'
    connections_matrix = connmatrix(connections)
    memory = [input, s...]'
    output = (memory*connections_matrix')%2
    return array2str(output)
end
Out[14]:
convEncodeOneStep (generic function with 1 method)

For example, given a tap-connection [5 7], if the state was "00" and the input bit was 1, then the output of the encoder is

In [15]:
convEncodeOneStep([5 7], "00", 1)
Out[15]:
"11"

It would be helpful if we have a lookup table of the encoder.

In [16]:
function onestep(connections)
    nummemory = Int(ceil(log2(maximum(connections))))-1
    numoutput = length(connections)
    state2idx = Dict(bin(i-1, nummemory) => i for i in 1:2^nummemory)
    states = [bin(i-1,nummemory) for i in 1:2^nummemory]
    onestep_codebook_index = Dict()
    i = 0
    onestep_codebook = zeros(Int64, 2^(nummemory+1), numoutput)
    onestep_codebook = ["" for i in 1:2^(nummemory+1)]
    for state in states 
        for input in [0 1]
            i += 1
            onestep_codebook_index[(state, input)] = i
            onestep_codebook[i] = convEncodeOneStep(connections, state, input)
        end
    end
    return (onestep_codebook, onestep_codebook_index)
end
Out[16]:
onestep (generic function with 1 method)

For example, given a tap-connection [5 7],

In [17]:
onestep_codebook, onestep_codebook_index = onestep([5 7]);
In [18]:
onestep_codebook_index
Out[18]:
Dict{Any,Any} with 8 entries:
  ("01",0) => 3
  ("10",1) => 6
  ("11",1) => 8
  ("11",0) => 7
  ("00",0) => 1
  ("10",0) => 5
  ("01",1) => 4
  ("00",1) => 2
In [19]:
onestep_codebook
Out[19]:
8-element Array{String,1}:
 "00"
 "11"
 "11"
 "00"
 "01"
 "10"
 "10"
 "01"

You can read off the information as follows: for example, if the state was "01" and the input bit was 0, then the output of the encoder is stored as the third element of onestep_codebook.

Now, we implement the Viterbi algorithm. The only difference between BSC and AWGN is the metric we use.

In [20]:
function Viterbi(channel, connections, receivedsequence)
    # Choose a metric.
    if channel == "BSC"
        dist = hammdist
    elseif channel == "AWGN"
        sqdist(code, received) = sum(((2*str2array(Float64, code)-1)'-received).^2)
        dist = sqdist
    else
        print("ERROR: Channel\n")
        return false
    end
    # Set parameters
    nummemory = Int(ceil(log2(maximum(connections))))-1
    numoutput = length(connections)
    numinputbits = Int(length(receivedsequence)/numoutput)
    state2idx = Dict(bin(i-1, nummemory) => i for i in 1:2^nummemory)
    states = [bin(i-1,nummemory) for i in 1:2^nummemory]
    # Initialize survivor metrics and paths
    survivormetrics = zeros(2^(nummemory))
    survivormetrics[2:end] = Inf
    survivorpaths = ["" for i in 1:2^nummemory]
    survivorinputpaths = ["" for i in 1:2^nummemory]
    # One step codebook
    onestep_codebook, onestep_codebook_index = onestep(connections)
    # For each step, we do the following:
    for ii in 1:numinputbits
        received = receivedsequence[numoutput*(ii-1)+1:numoutput*ii]  
        distances = [dist(code, received) for code in onestep_codebook]
        next_survivorpaths = copy(survivorpaths)
        next_survivorinputpaths = copy(survivorinputpaths)
        next_survivormetrics = copy(survivormetrics)
        # For each state, we find the optimal path:
        for state in states
            state_idx = state2idx[state]
            # Add
            new_survmetric = [survivormetrics[state2idx[prev(state, s)]] + distances[onestep_codebook_index[(prev(state, s), parse(Int, state[1]))]] for s in ["0" "1"]]
            # Compare & Select
            optidx = new_survmetric[1]<new_survmetric[2]?1:2
            opts = optidx==1?"0":"1"
            optprev = prev(state, opts)
            # Update
            next_survivormetrics[state_idx] = new_survmetric[optidx]
            opt_path = onestep_codebook[onestep_codebook_index[(optprev, parse(Int, state[1]))]]
            next_survivorpaths[state_idx] = *(survivorpaths[state2idx[optprev]], opt_path)
            next_survivorinputpaths[state_idx] = *(survivorinputpaths[state2idx[optprev]], string(state[1]))
        end
        # Update the survivor paths and metrics
        survivorpaths = copy(next_survivorpaths)
        survivorinputpaths = copy(next_survivorinputpaths)
        survivormetrics = copy(next_survivormetrics)
#         print(survivormetrics)
    end
#     print(survivormetrics, "\n")
    # Pick the first minimum survivor metric
    return survivorpaths[indmin(survivormetrics)], survivorinputpaths[indmin(survivormetrics)]
end
Out[20]:
Viterbi (generic function with 1 method)

(d)

Use the functions in parts (a) and (c) to plot the bit error rate (BER) performance of the codes $[\begin{matrix}2 & 3\end{matrix}]$ and $[\begin{matrix}5 & 7\end{matrix}]$ over a BSC($p$) with $p\in [0\colon0.05\colon0.5]$. For each value of $p$, you should encode and decode a block of at least 2,000 symbols and calculate the average bit error rate. Please make sure that the encoder always start from the all-zero state. (No termination is required.)

In [30]:
iterN = 10
leninputbits = 2000
plist = 0:0.05:0.5
connectionslist = [[2 3], [5 7], [155, 117]]
BERlist = zeros(size(connectionslist,1),length(plist))
for ii in 1:size(connectionslist, 1)
    connections = connectionslist[ii]
    onestep_codebook, onestep_codebook_index = onestep(connections)
    for jj in 1:length(plist)
        print(jj, " ")
        p = plist[jj]
        BER = 0
        for kk = 1:iterN
            inputbits = genRandBitVec(leninputbits)
            codebits = convEncode(connections, inputbits)
            receivedbits = BSC(codebits, p)
            decodedbits, decodedinputbits = Viterbi("BSC", connections, receivedbits)
            BER += (hammdist(string(inputbits...), decodedinputbits))/length(inputbits)
        end
        BERlist[ii, jj] = BER/iterN
    end
end
1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 
In [31]:
for ii = 1:length(connectionslist)
    plot(plist, BERlist[ii,:], marker = "*", label = "Taps "*string(connectionslist[ii]))
end
plot(plist, plist)
grid("on")
title("BSC(p)")
xlabel("Bit flip probability p")
ylabel("Average bit error rate")
legend(loc = "upper left", fancybox = "true")
Out[31]:
PyObject <matplotlib.legend.Legend object at 0x7fa8194e5450>

(f)

Use the functions in parts (a) and (e) to plot the bit error rate (BER) performance of the codes $[\begin{matrix}2 & 3\end{matrix}]$ and $[\begin{matrix}5 & 7\end{matrix}]$ over an AWGN with SNRdB ranging from $-1$ dB to $10$ dB. For each value of the SNRdB, you should encode and decode a block of at least 2,000 symbols and calculate the average bit error rate. Please make sure that the encoder always start from the all-zero state. (No termination is required.)

In [23]:
# Experiemnts: AWGN
iterN = 15
leninputbits = 2000
SNRdBlist = -1:1:10
connectionslist = [[2 3], [5 7], [155 117]]
BERlist = zeros(size(connectionslist,1),length(SNRdBlist))
for ii in 1:size(connectionslist, 1)
    connections = connectionslist[ii]
    for jj in 1:length(SNRdBlist)
        print(jj, " ")
        SNRdB = SNRdBlist[jj]
        BER = 0
        for kk = 1:iterN
            inputbits = genRandBitVec(leninputbits)
            codebits = convEncode(connections, inputbits)
            receivedbits = AWGN(codebits, SNRdB)
            decodedbits, decodedinputbits = Viterbi("AWGN", connections, receivedbits)
            BER += (hammdist(string(inputbits...), decodedinputbits))/length(inputbits)
        end
        BERlist[ii, jj] = BER/iterN
    end
end
1 2 3 4 5 6 7 8 9 10 11 12 1 2 3 4 5 6 7 8 9 10 11 12 1 2 3 4 5 6 7 8 9 10 11 
In [24]:
for ii = 1:length(connectionslist)
    plot(SNRdBlist, log10(BERlist[ii,:]), marker = "*", label = "Taps "*string(connectionslist[ii]))
end
grid("on")
title("AWGN(SNRdB)")
xlabel("SNR (dB)")
ylabel("log10(Average bit error rate)")
legend(loc = "upper right", fancybox = "true")
Out[24]:
PyObject <matplotlib.legend.Legend object at 0x7fa8195e0b10>

(b)

Write a program for a function $\texttt{freeDistance}$($\texttt{connections}$) that takes an array of octal tap connections $\texttt{connections}$ as input and outputs the free distance $d_{\textrm{free}}$ of the code. For example, for $\texttt{connections} = [\begin{matrix}4 & 5\end{matrix}],$ your function should output $$ d_{\textrm{free}} = 3. $$ Use this function to find $d_{\mathrm{free}}$ for the two codes in part (a), as well as for the code with octal tap connections $[\begin{matrix}155 & 117 \end{matrix}].$

We can easily modify the Viterbi algorithm.

In [25]:
function freeDistance(connections)
    # Set parameters
    nummemory = Int(ceil(log2(maximum(connections))))-1
    numoutput = length(connections)
    state2idx = Dict(bin(i-1, nummemory) => i for i in 1:2^nummemory)
    states = [bin(i-1,nummemory) for i in 1:2^nummemory]
    # Initialize survivor metrics and paths
    survivormetrics = zeros(2^(nummemory))
    survivormetrics[2:end] = Inf
    survivorpaths = ["" for i in 1:2^nummemory]
    # One step codebook
    onestep_codebook, onestep_codebook_index = onestep(connections)
    # Initialize the minimum distance and path.
    mindist = Inf
    minpath = ""
    while true
        received = ^("0", numoutput)
        distances = [hammdist(code, received) for code in onestep_codebook]
        next_survivorpaths = copy(survivorpaths)
        next_survivormetrics = copy(survivormetrics)
        for state in states
            state_idx = state2idx[state]
            if state == ^("0", nummemory)  # For all-zero state, the optimal path is always from "00...01".
                # Add
                new_survmetric = [survivormetrics[state2idx[prev(state, "1")]] + distances[onestep_codebook_index[(prev(state, "1"), parse(Int, state[1]))]] for s in ["0" "1"]]
                # Update
                optidx = new_survmetric[1]<new_survmetric[2]?1:2
                optprev = prev(state, "1")
            else
                new_survmetric = [survivormetrics[state2idx[prev(state, s)]] + distances[onestep_codebook_index[(prev(state, s), parse(Int, state[1]))]] for s in ["0" "1"]]
                # Compare & Select
                optidx = new_survmetric[1]<new_survmetric[2]?1:2
                opts = optidx==1?"0":"1"
                optprev = prev(state, opts)
            end
            # Update
            next_survivormetrics[state_idx] = new_survmetric[optidx]
            optpath = onestep_codebook[onestep_codebook_index[(optprev, parse(Int, state[1]))]]
            next_survivorpaths[state_idx] = *(survivorpaths[state2idx[optprev]], optpath)            
        end
        survivorpaths = copy(next_survivorpaths)
        survivormetrics = copy(next_survivormetrics)
        # Update the minimum distance
        if mindist > survivormetrics[state2idx[^("0", nummemory)]]
            mindist = survivormetrics[state2idx[^("0", nummemory)]]
            minpath = survivorpaths[state2idx[^("0", nummemory)]]
        end
        if mindist < minimum(survivormetrics)
            break
        end
    end
    return Int(mindist), minpath
end
Out[25]:
freeDistance (generic function with 1 method)
In [26]:
freeDistance([2 3])
Out[26]:
(3,"1101")
In [27]:
freeDistance([5 7])
Out[27]:
(5,"110111")
In [28]:
freeDistance([4 5])
Out[28]:
(3,"110001")
In [29]:
freeDistance([155 117])
Out[29]:
(10,"0011100011110111")