Reed-solomon CODEC

This notebook will describe a Reed-Solomon CODEC library for OCaml.

Building

$ make
$ make install

Library structure

  • Ops - interface for basic mathematical operations.
  • Poly, Matrix - polynomial and matrix operations
  • Galois - galois field arithmetic operations, including extension fields
  • Rs - high level Reed-Solomon CODEC implementation

Using the library


In [1]:
#require "reedsolomon,iocaml.notebook"


	Camlp4 Parsing version 4.01.0

/home/andyman/.opam/4.01.0-test/lib/reedsolomon: added to search path
/home/andyman/.opam/4.01.0-test/lib/reedsolomon/reedsolomon.cma: loaded
/home/andyman/.opam/4.01.0-test/lib/iocaml: added to search path

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]:
module X :
  sig
    module Gp : sig val pp : int val pe : int end
    module G :
      sig
        type t = int
        val zero : t
        val one : t
        val ( +: ) : t -> t -> t
        val ( -: ) : t -> t -> t
        val to_string : t -> string
        val alpha : t
        val n_elems : int
        val log : t -> int
        val antilog : int -> t
        val ( *: ) : t -> t -> t
        val ( /: ) : t -> t -> t
        val ( **: ) : t -> int -> t
        val inv : t -> t
      end
    module Rp : sig val k : int val t : int val b : int end
    module R :
      sig
        type elt = int
        module M :
          sig
            type t = elt
            type matrix = t array array
            val rows : matrix -> int
            val cols : matrix -> int
            val init : int -> int -> (int -> int -> t) -> matrix
            val create : int -> int -> matrix
            val copy : matrix -> matrix
            val identity : int -> matrix
            val transpose : matrix -> matrix
            val map : (t -> t) -> matrix -> matrix
            val map2 : (t -> t -> t) -> matrix -> matrix -> matrix
            val row_vector : t array -> matrix
            val col_vector : t array -> matrix
            val ( >> ) : matrix -> matrix -> matrix
            val ( ^^ ) : matrix -> matrix -> matrix
            val sub : int -> int -> int -> int -> matrix -> matrix
            val ( +: ) : matrix -> matrix -> matrix
            val ( -: ) : matrix -> matrix -> matrix
            val ( *: ) : matrix -> matrix -> matrix
            val ( *:. ) : matrix -> t -> matrix
            val minor : int -> int -> matrix -> matrix
            val det : matrix -> t
            val adjoint_inverse : matrix -> t * matrix
            val gauss_jordan : matrix -> matrix
            val gauss_jordan_inverse : matrix -> matrix
            module Row :
              sig
                val swap : int -> int -> int -> matrix
                val mult : int -> int -> t -> matrix
                val madd : int -> int -> int -> t -> matrix
              end
          end
        module R :
          sig
            type elt = elt
            type t = elt array
            val degree : t -> int
            val zero : t
            val one : t
            val x : t
            val to_poly : elt array -> t
            val of_poly : t -> elt array
            val copy : t -> t
            type poly_format =
              Rs.BBCTest.R.R.poly_format = {
              coef : elt -> string;
              indet : int -> string;
            }
            val poly_format : poly_format
            val string_format : bool -> poly_format -> int -> elt -> string
            val to_string :
              ?down:bool -> ?str:(int -> elt -> string) -> t -> string
            val trim : t -> t
            val slice : t -> int -> t
            val ( +: ) : t -> t -> t
            val ( -: ) : t -> t -> t
            val ( *: ) : t -> t -> t
            val ( /: ) : t -> t -> t * t
            val ( *:. ) : t -> elt -> t
            val ( /:. ) : t -> elt -> t
            val ( ^: ) : t -> int -> t
            val ( **: ) : t -> int -> t
            val ext_gcd : t -> t -> t * t
            val eval : t -> elt -> elt
          end
        type poly = R.t
        type loc = int
        val root : int -> elt
        val generator : poly
        val xn : int -> poly
        val x2t : poly
        val parity : poly -> poly
        val encode : poly -> poly
        val horner : poly -> elt -> elt
        val syndromes : poly -> poly
        val key_equations : poly -> int -> M.matrix * M.matrix
        val solve_key_equations : M.matrix * M.matrix -> M.matrix
        val peterson : poly -> poly
        val euclid_inner : poly * poly -> poly * poly -> poly * poly
        val euclid : ?norm:bool -> ?lim:int -> poly -> poly * poly
        val berlekamp_massey_iter :
          poly -> int -> poly * poly * int -> poly * poly * int
        val berlekamp_massey : poly -> poly
        module Sarwate :
          sig
            val iBM : poly -> poly
            val riBM : poly -> poly * poly
            val rriBM : poly -> poly * poly
            val forney : poly -> poly -> loc -> elt
          end
        val chien : poly -> loc list
        val error_location : loc -> int
        val error_magnitude : int -> poly -> poly -> poly
        val deriv : poly -> poly
        val forney : poly -> poly -> loc -> elt
        val error : elt list -> loc list -> poly
        val correct : poly -> poly -> poly
        val decode_euclid : poly -> poly
        val decode_berlekamp_massey : poly -> poly
        val decode_peterson : poly -> poly
        val decode : poly -> poly
        val erasure_locator : int list -> poly
        val zero_erasures : poly -> int list -> poly
        val error_and_erasure :
          elt list -> loc list -> elt list -> loc list -> poly
        val decode_erasures_euclid : poly -> int list -> poly
        val decode_erasures : poly -> int list -> poly
        val decode_errors_and_erasures_euclid : poly -> int list -> poly
        val decode_errors_and_erasures_berlekamp_massey :
          poly -> int list -> poly
        val decode_errors_and_erasures : poly -> int list -> poly
      end
  end

There are 4 modules defined therein;

  • Gp - parameters for the Galois field
  • G - Galois field implementation
  • Rp - parameters for the RS CODEC
  • R - Reed-Solomon CODEC implemention

Lets look at some of those parameters.


In [3]:
let pp, pe = X.Gp.pp, X.Gp.pe


Out[3]:
val pp : int = 19
val pe : int = 2
  • pp - primtive polynomial of galois field
  • pe - primtive element of galois field

In [4]:
let k, t = X.Rp.k, X.Rp.t
let n = k + (2*t)


Out[4]:
val k : int = 11
val t : int = 2
Out[4]:
val n : int = 15
  • k - number of symbols in a message
  • t - error correction capability
  • n - code word size

Galois field elements


In [5]:
let number_of_field_elements = X.G.n_elems


Out[5]:
val number_of_field_elements : int = 16

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]:
val table : int -> int -> (int -> int -> string) -> unit = <fun>

In [7]:
table 16 16 (fun i j -> X.G.(to_string (i +: j)))


0123456789101112131415
1032547698111013121514
2301674510118914151213
3210765411109815141312
4567012312131415891011
5476103213121514981110
6745230114151213101189
7654321015141312111098
8910111213141501234567
9811101312151410325476
1011891415121323016745
1110981514131232107654
1213141589101145670123
1312151498111054761032
1415121310118967452301
1514131211109876543210
Out[7]:
- : unit = ()

In [8]:
table 16 16 (fun i j -> X.G.(to_string (i *: j)))


0000000000000000
0123456789101112131415
0246810121431751191513
0365121510911813147412
0481237111562141051139
0510157213814114191236
0612101113715391514824
0714915816131034251211
0831161451312415710291
0918211310413512615714
0107131449315582111612
0115141011547122913683
0121175914210611315348
0139411285215116314107
0141511332129768410115
0151329641111412387510
Out[8]:
- : unit = ()

In [9]:
table 1 16 (fun _ i -> X.G.(to_string (inv i)))


0191413117615212510438
Out[9]:
- : unit = ()

Polynomials

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]:
module IPoly :
  sig
    type elt = Ops.Int.t
    type t = Ops.Int.t array
    val degree : t -> int
    val zero : t
    val one : t
    val x : t
    val to_poly : elt array -> t
    val of_poly : t -> elt array
    val copy : t -> t
    type poly_format =
      Poly.Make(Ops.Int).poly_format = {
      coef : elt -> string;
      indet : int -> string;
    }
    val poly_format : poly_format
    val string_format : bool -> poly_format -> int -> elt -> string
    val to_string : ?down:bool -> ?str:(int -> elt -> string) -> t -> string
    val trim : t -> t
    val slice : t -> int -> t
    val ( +: ) : t -> t -> t
    val ( -: ) : t -> t -> t
    val ( *: ) : t -> t -> t
    val ( /: ) : t -> t -> t * t
    val ( *:. ) : t -> elt -> t
    val ( /:. ) : t -> elt -> t
    val ( ^: ) : t -> int -> t
    val ( **: ) : t -> int -> t
    val ext_gcd : t -> t -> t * t
    val eval : t -> elt -> elt
  end

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]:
val display_poly : X.R.R.t -> unit = <fun>

In [12]:
let f,g = [| 7; 3 |], [| 4; 0; 2 |]


Out[12]:
val f : int array = [|7; 3|]
val g : int array = [|4; 0; 2|]

In [13]:
display_poly f;;
display_poly g;;


$$3.x + 7$$
$$2.x^{2} + 4$$
Out[13]:
- : unit = ()
Out[13]:
- : unit = ()

In [14]:
display_poly IPoly.(f *: g)


$$6.x^{3} + 14.x^{2} + 12.x + 28$$
Out[14]:
- : unit = ()

More on Galois fields

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)));;


01
10
00
01
Out[15]:
- : unit = ()
Out[15]:
- : unit = ()

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]:
module GFPoly :
  sig
    type t = Galois.Primitive.GF2.t array
    val zero : t
    val one : t
    val ( +: ) : t -> t -> t
    val ( -: ) : t -> t -> t
    val ( *: ) : t -> t -> t
    val ( /: ) : t -> t -> t
    val to_string : t -> string
  end

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]:
val x : GFPoly.t = [|0; 1; 0; 0|]

In [18]:
GFPoly.to_string x


Out[18]:
- : string = "x"

In [19]:
GFPoly.(to_string (x *: x))


Out[19]:
- : string = "x^2"

In [20]:
let rec pow = GFPoly.(function
    | 0 -> one
    | 1 -> x
    | _ as n -> x *: pow (n-1))


Out[20]:
val pow : int -> GFPoly.t = <fun>

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)))


$x^{0}$$1$1
$x^{1}$$x$2
$x^{2}$$x^2$4
$x^{3}$$x^3$8
$x^{4}$$x + 1$3
$x^{5}$$x^2 + x$6
$x^{6}$$x^3 + x^2$12
$x^{7}$$x^3 + x + 1$11
$x^{8}$$x^2 + 1$5
$x^{9}$$x^3 + x$10
$x^{10}$$x^2 + x + 1$7
$x^{11}$$x^3 + x^2 + x$14
$x^{12}$$x^3 + x^2 + x + 1$15
$x^{13}$$x^3 + x^2 + 1$13
$x^{14}$$x^3 + 1$9
Out[21]:
- : unit = ()

Encoding

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


$$x^{10} + 2.x^{9} + 3.x^{8} + 4.x^{7} + 5.x^{6} + 6.x^{5} + 7.x^{4} + 8.x^{3} + 9.x^{2} + 10.x + 11$$
Out[22]:
val message : int array = [|11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1|]

In [23]:
let codeword = X.R.encode message
let () = display_poly codeword


$$x^{14} + 2.x^{13} + 3.x^{12} + 4.x^{11} + 5.x^{10} + 6.x^{9} + 7.x^{8} + 8.x^{7} + 9.x^{6} + 10.x^{5} + 11.x^{4} + 3.x^{3} + 3.x^{2} + 12.x + 12$$
Out[23]:
val codeword : X.R.poly = [|12; 12; 3; 3; 11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1|]

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]:
val myencode : X.R.R.t -> X.R.R.t = <fun>

In [25]:
let mycodeword = myencode message
let () = display_poly mycodeword


$$x^{14} + 2.x^{13} + 3.x^{12} + 4.x^{11} + 5.x^{10} + 6.x^{9} + 7.x^{8} + 8.x^{7} + 9.x^{6} + 10.x^{5} + 11.x^{4} + 3.x^{3} + 3.x^{2} + 12.x + 12$$
Out[25]:
val mycodeword : X.R.R.t =
  [|12; 12; 3; 3; 11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1|]

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.

Errors

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


$$13.x^{9} + 2.x^{2}$$
Out[26]:
val error : int array = [|0; 0; 2; 0; 0; 0; 0; 0; 0; 13; 0; 0; 0; 0; 0|]

In [27]:
let received = X.R.R.(codeword +: error)
let () = display_poly received


$$x^{14} + 2.x^{13} + 3.x^{12} + 4.x^{11} + 5.x^{10} + 11.x^{9} + 7.x^{8} + 8.x^{7} + 9.x^{6} + 10.x^{5} + 11.x^{4} + 3.x^{3} + x^{2} + 12.x + 12$$
Out[27]:
val received : X.R.R.t = [|12; 12; 1; 3; 11; 10; 9; 8; 7; 11; 5; 4; 3; 2; 1|]

Decoding

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


$$x^{14} + 2.x^{13} + 3.x^{12} + 4.x^{11} + 5.x^{10} + 6.x^{9} + 7.x^{8} + 8.x^{7} + 9.x^{6} + 10.x^{5} + 11.x^{4} + 3.x^{3} + 3.x^{2} + 12.x + 12$$
Out[28]:
val corrected : X.R.poly =
  [|12; 12; 3; 3; 11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1|]
Out[28]:
val okay : bool = true

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]:
val syndromes : X.R.elt array = [|15; 3; 4; 12|]

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]:
val v : X.R.poly = [|14; 3|]
val l : X.R.poly = [|9; 7; 7|]

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]:
val el : X.R.loc list = [6; 13]
Out[31]:
val el' : int list = [9; 2]

Forneys algorithm solves for the error values.


In [32]:
let ev = List.map (X.R.forney v l) el


Out[32]:
val ev : X.R.elt list = [13; 2]

Using el and ev we form the error polynomial


In [33]:
let e = X.R.error ev el


Out[33]:
val e : X.R.poly = [|0; 0; 2; 0; 0; 0; 0; 0; 0; 13|]

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


$$x^{14} + 2.x^{13} + 3.x^{12} + 4.x^{11} + 5.x^{10} + 6.x^{9} + 7.x^{8} + 8.x^{7} + 9.x^{6} + 10.x^{5} + 11.x^{4} + 3.x^{3} + 3.x^{2} + 12.x + 12$$
Out[34]:
val corrected : X.R.R.t = [|12; 12; 3; 3; 11; 10; 9; 8; 7; 6; 5; 4; 3; 2; 1|]