Page Contents

2020-10-09 #codes #unicon

This is the first in what I plan to be a series of posts about coding theory, looking at different techniques.

I am working through two books:

  • Raymond Hill, A First Course in Coding Theory, 1986.

  • Arkadii Slinko, Algebra for Applications, 2020.

What is coding theory?

Coding theory attempts to solve the problem of reliably transmitting information from one location (or person), the transmitter, to a second location (or person), the receiver. The information is transmitted across a channel. The challenge is that this channel does not reliably transmit information, but may introduce noise, or other distortions.

For example, the transmitter may want to send the message "HELLO WORLD". The channel may introduce some noise, and the receiver then gets the message "HFLLP VOPLD".

A typical way of tackling this problem is to recode the message to include some redundancy. In this way, the channel probably cannot alter all the redundant parts of the message, and so the receiver can reconstruct what has been sent. This idea leads us to our first code.

Binary Repetition Code

For the time being, I will simplify the type of messages that can be transmitted. These messages will be sequences of binary digits, e.g. "100110". The noise that the channel can introduce will mean that a bit can be flipped, e.g. "101010" is a noisy version of the first binary message.

As we are talking about the channel, let us define how noise is added to a message. We make the following assumptions to define a binary symmetric channel:

  • every binary digit in the message has the same probability, p, of being received in error;

  • p is assumed to be less than 0.5 (this means we can assume the occurrence of one error is more likely than the occurrence of two errors); and

  • the chance of a transmitted 0 being received as an erroneous 1 is the same as that of a 1 being received as a 0.

As an example, if we want to transmit the message "100110", what is the probability that it will be received without errors? As p is the probability that an error will occur in any bit, there is a (1-p) probability that any individual bit will be received without error.

For the six bits in the message, that means a (1-p)6 probability that the message is received correctly.

Therefore, for a message of length 6 bits, and p = 0.01 (i.e. noise affects 1 in 100 of the transmitted bits):

  • Probability that the message is received correctly is 0.94

  • Or, probability that the message is received with one or more errors is 0.06

The binary repetition code aims to reduce the error rate by sending each bit several times. For example, instead of transmitting the bit 1, we will transmit the bits 111, and instead of transmitting the bit 0, we will transmit the bits 000: the 111 and 000 are called the codewords.

The binary repetition code looks like this:

  • Message bit 1 → 1 1 1

  • Message bit 0 → 0 0 0

So the message "100110" is encoded as: "111000000111111000".

What is the advantage of this? The main one is that we can correct a certain amount of errors.

For example, if the message "101" is received, we can see that it is an error (because "101" is not either of our codewords: "000" or "111"). However, it is closer to "111" than it is to "000", and so we can decode the message into the codeword "111" with a reasonable expectation that we have decoded it correctly.

Notice how we rely on p being less than 0.5 here:

  • "101" is one error from "111" with probability (1-p)p(1-p), which is 0.0098 for p=0.01.

  • "101" is two errors from "000" with probability p(1-p)p, which is 0.000099 for p=0.01.

The "closeness" of the codeword to either all 1s or all 0s is defined as its weight, simply the sum of non-zero bits. If the weight is greater than half of the number of bits, then it is closer to being all 1s than to all 0s.

The weight of "101" is 2, and 2 is greater than 3/2=1.5, so "101" is closer to "111".

Word Error Probability

The word error probability defines the probability that a given codeword is received in error.

In the case without any coding, we were transmitting single bits: in effect, the 1 and 0 bits are their own codewords. We could call this the redundant case when the repetition code is of length 1. The word error probability in this case is p, the probability that a given bit will be received in error.

In the case when the repetition code is of length 3, as above, the word error can be calculated as follows. First, consider "000": which received messages will be closer to "000" than "111"? These are "000", "001", "010", "100". So the probability that "000" is received correctly can be calculated from the sum of the probabilities that each of these would be received:

  • P(000) = (1-p)(1-p)(1-p)

  • P(001) = (1-p)(1-p)p

  • P(010) = (1-p)p(1-p)

  • P(100) = p(1-p)(1-p)

Which, with some rearranging, means the probability that "000" is received correctly = (1-p)2 (1+2p)

Thus, the word error probability is 1 - (1-p)2 (1+2p) = 3p2 - 2p3

(The same value is reached when considering "111", by symmetry.)

Some predictions, with p=0.01:

  • repetition = 1, word error = 0.01

  • repetition = 3, word error = 0.000298

  • repetition = 5, word error = 0.000010

Notice how the probability of receiving an error reduces as the amount of repetition increases; the more redundancy there is in the message, the easier it is to correct for noise.

Simulation

We can now write some code to implement this repetition code scheme, and try to reproduce the predicted word error rates. Of course, any language can be used here, but I’m using unicon.

Messages will be stored as lists of integers, 0 or 1 as appropriate. We begin with a procedure to calculate the weight of a given list of integers:

procedure weight (lst)
  total := 0
  every total +:= !lst
  return total
end

Oddly, I cannot find a standard way to compare two lists in unicon, so I added:

procedure equal_lists (lst1, lst2)
  if *lst1 === *lst2 then {
    every i := key(lst1) do {
      if lst1[i] ~=== lst2[i] then fail()
    }
  } else
    fail()

  return
end

The following two important procedures perform the actual encoding or decoding. The first produces the repetition codeword, and the second uses the weight to convert a codeword to its likely original value:

procedure encode (val, size)
  result := []

  every !size do {
    result := push(result, val)
  }

  return result
end

procedure decode (val, size)
  return if weight(val) <= size/2 then
    0
  else
    1
end

The following two procedures will encode or decode an entire message. These are probably not needed, given our simulation only tests individual codewords, but they complete the code.

The decoding of a message means dividing a perhaps long stream of binary digits into blocks of the same size as our repetition code. e.g. with a code of "111000000111111000", it must be divided into "111", "000", "000", "111", "111" and "000", and then each individual block decoded.

procedure encode_msg (msg, size)
  result := []

  every bit := !msg do {
    result := result ||| encode (bit, size)
  }

  return result
end

procedure decode_msg (msg, size)
  result := []

  every i:= 1 to *msg by size do {
    result := put (result, decode (msg[i+:size], size))
  }

  return result
end

The transmit procedure takes the message and randomly inverts each bit, using the given p:

procedure transmit (msg)
  p := 0.01 # symbol error probability

  every i := key(msg) do {
    if ?0 < p then msg[i] := 1-msg[i] # add noise
  }

  return msg
end

Finally, the main procedure runs the simulation. It loops through the three repetition sizes and transmits a random codeword as the message for N iterations, checking the accuracy of the decoded message. The procedure reports on the overall word error probability for each repetition size:

procedure main (arglist)
  N := 1000000 # number of iterations

  every size := ![1,3,5] do {
    correct := 0

    every !N do {
      msg := [ ?[0, 1] ]  # msg is a single bit
      received := decode_msg (transmit (encode_msg (msg, size)), size)
      if equal_lists(msg, received) then correct +:= 1
    }

    write ("Size           = ", size)
    write ("N              = ", N)
    write ("Number correct = ", correct)
    write ("Perr(C)        = ", 1-real(correct)/N)
    write ()
  }
end

Results are reasonably close, for 1 million iterations (comment added to show expected value):

$ unicon -C codes.icn
$ ./codes
Size           = 1
N              = 1000000
Number correct = 990027
Perr(C)        = 0.00997300000000001    # expected 0.01

Size           = 3
N              = 1000000
Number correct = 999710
Perr(C)        = 0.0002900000000000125  # expected 0.000298

Size           = 5
N              = 1000000
Number correct = 999992
Perr(C)        = 8.000000000008001e-06  # =0.000008 expected 0.000010