This notebook will describe a Reed-Solomon CODEC library for OCaml.
$ make
$ make install
Ops
- interface for basic mathematical operations.Poly, Matrix
- polynomial and matrix operationsGalois
- galois field arithmetic operations, including extension fieldsRs
- high level Reed-Solomon CODEC implementation
In [1]:
#require "reedsolomon,iocaml.notebook"
The Rs
module defines a few pre-built standards. For now we will use BBCTest
which describes the RS code from this excellent whitepaper.
In [2]:
module X = Rs.BBCTest
Out[2]:
There are 4 modules defined therein;
Gp
- parameters for the Galois fieldG
- Galois field implementationRp
- parameters for the RS CODECR
- Reed-Solomon CODEC implementionLets look at some of those parameters.
In [3]:
let pp, pe = X.Gp.pp, X.Gp.pe
Out[3]:
In [4]:
let k, t = X.Rp.k, X.Rp.t
let n = k + (2*t)
Out[4]:
Out[4]:
k
- number of symbols in a messaget
- error correction capabilityn
- code word size
In [5]:
let number_of_field_elements = X.G.n_elems
Out[5]:
This tells us something about the Galois field - specifically that it has 16 elements. They are represented by integers in the range 0 to 15.
We can do arithmetic operations over the field using the functions in X.G
.
In [6]:
(* utility function to display a html table *)
let table rows cols f =
let tag x s = "<" ^ x ^ ">" ^ s ^ "</" ^ x ^ ">" in
let concat x s = s |> Array.map (tag x) |> Array.to_list |> String.concat "" in
let init n x f = Array.init n f |> concat x in
Iocaml.display "text/html"
("<table>" ^ (init rows "tr" (fun row -> init cols "td" (f row))) ^ "</table>")
Out[6]:
In [7]:
table 16 16 (fun i j -> X.G.(to_string (i +: j)))
Out[7]:
In [8]:
table 16 16 (fun i j -> X.G.(to_string (i *: j)))
Out[8]:
In [9]:
table 1 16 (fun _ i -> X.G.(to_string (inv i)))
Out[9]:
The Poly
module represents polynomials as arrays of coefficients. The coefficient type is generic and provided as an argument to the Poly.Make functor.
The indices of the array give the power of the indeterminate. For example
In [10]:
module IPoly = Poly.Make(Ops.Int)
Out[10]:
In [11]:
(* display poly *)
let display_poly poly =
(* modifiy printing a little to suit mathjax *)
let str = X.R.R.(string_format true
{ poly_format with
indet = function 0 -> "" | 1 -> "x" | _ as n -> "x^{" ^ string_of_int n ^ "}"; }) in
Iocaml.display "text/html" ("$$" ^ X.R.R.to_string ~str:str poly ^ "$$")
Out[11]:
In [12]:
let f,g = [| 7; 3 |], [| 4; 0; 2 |]
Out[12]:
In [13]:
display_poly f;;
display_poly g;;
Out[13]:
Out[13]:
In [14]:
display_poly IPoly.(f *: g)
Out[14]:
The Galois
module is actually quite general and can implement the fields in different ways. Indeed the integer representation above is actually derived from a lower level version based on polynomials over GF2.
In [15]:
table 2 2 (fun i j -> Galois.Primitive.GF2.(to_string (i +: j)));;
table 2 2 (fun i j -> Galois.Primitive.GF2.(to_string (i *: j)));;
Out[15]:
Out[15]:
In [16]:
module GFPoly = Galois.Extension.Make(struct
module Poly = Poly.Make(Galois.Primitive.GF2)
let pp = [| 1; 1; 0; 0; 1 |]
end)
Out[16]:
Using this representation we can see the underlying structure of $GF(2^4)$ fields as polynomials.
In [17]:
let x : GFPoly.t = [|0; 1; 0; 0|]
Out[17]:
In [18]:
GFPoly.to_string x
Out[18]:
In [19]:
GFPoly.(to_string (x *: x))
Out[19]:
In [20]:
let rec pow = GFPoly.(function
| 0 -> one
| 1 -> x
| _ as n -> x *: pow (n-1))
Out[20]:
In [21]:
table 15 3 (fun i -> function
| 0 -> "$x^{" ^ string_of_int i ^ "}$"
| 1 -> "$" ^ GFPoly.to_string (pow i) ^ "$"
| _ -> X.G.(to_string (X.Gp.pe **: i)))
Out[21]:
The encoding process takes a message of k symbols and produces a codeword of n symbols.
In [22]:
let message = [| 11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1 |]
let () = display_poly message
Out[22]:
In [23]:
let codeword = X.R.encode message
let () = display_poly codeword
Out[23]:
The encoding process is rather simple
$$codeword = message.x^{2t} + (message.x^{2t} \mod generator)$$We can implement it in a few lines of polynomial arithmetic.
In [24]:
let myencode message =
let open X.R.R in
let (%:) a b = snd (a /: b) in (* mod; the remainder of division *)
let message = message *: (x **: (2*t)) in (* shift message *)
message +: (message %: X.R.generator)
Out[24]:
In [25]:
let mycodeword = myencode message
let () = display_poly mycodeword
Out[25]:
It is worth noting that the RS encoder is systematic - that is the orginal message is included in the code word. The symbols added by the encoder are known as parity symbols.
During transmission or storage the codeword may become corrupted by some errors.
$$received = codeword + errors$$It is the purpose of the Reed-Solomon decoder to calculate the error polynomial from the received polynomial then
$$corrected = received - errors$$For a codeword with $2t$ parity symbols the Reed-Solomon decoder will be able to correct upto $t$ symbol errors.
In [26]:
let error = [| 0; 0; 2; 0; 0; 0; 0; 0; 0; 13; 0; 0; 0; 0; 0 |]
let () = display_poly error
Out[26]:
In [27]:
let received = X.R.R.(codeword +: error)
let () = display_poly received
Out[27]:
We start by running the decoder on our example.
In [28]:
let corrected = X.R.decode received
let () = display_poly corrected
let okay = corrected = codeword
Out[28]:
Out[28]:
As we see the decoder has taken the (corrupt) received poly and recovered the original codeword.
Although I shall avoid a technical description of the maths involved in decoding (if you're interested check out that BBC white paper), I will show how the decoder is put together from the functions in the Rs
module.
The first step is to calculate the syndrome vector. This is done by evalulating the received polynomial (here we use horners method) at each of the roots of the generator polynomial (of which there are $2t$).
In [29]:
let syndromes = Array.init (2*t) X.R.(fun i -> horner received (root i))
Out[29]:
Because the roots of the generator are also the roots of the codeword, if any of these values are non-zero then we know there are errors to be corrected, as is the case here.
We now have a choice on how we will solve the key equations - Petersons' method (matrix inversion), Berlekamp-Massey (black magic) or Euclids algorithm (polynomial factorisation). We'll choose the later as it's easiest to understand, though implementations of all methods are provided. From the syndromes this will produce the error locator and error evaluator polynomials.
In [30]:
let v, l = X.R.euclid syndromes
Out[30]:
Chien search finds the error locations.
In [31]:
let el = X.R.chien l
let el' = List.map X.R.error_location el (* compute actual locations *)
Out[31]:
Out[31]:
Forneys algorithm solves for the error values.
In [32]:
let ev = List.map (X.R.forney v l) el
Out[32]:
Using el
and ev
we form the error polynomial
In [33]:
let e = X.R.error ev el
Out[33]:
And finally it can be added to the received polynomial to form the corrected polynomial.
In [34]:
let corrected = X.R.R.(received +: e)
let () = display_poly corrected
Out[34]: