packages feed

reedsolomon-0.0.1.0: src/Data/ReedSolomon/Matrix.lhs

> {-# LANGUAGE DeriveDataTypeable #-}
> module Data.ReedSolomon.Matrix (
>       Matrix
>     , identityMatrix
>     , multiply
>     , subMatrix
>     , invert
>     , vandermonde
>     ) where
>
> import Prelude hiding (break)
>
> import Control.Exception.Base (Exception)
> import Control.Monad (unless, when)
> import Control.Monad.ST (ST)
> import Control.Monad.Trans (lift)
> import Data.Bits (xor)
> import Data.STRef (newSTRef, readSTRef, writeSTRef)
> import Data.Typeable (Typeable)
> import Data.Word (Word8)
>
> import Control.Monad.Catch (MonadThrow, throwM)
>
> import qualified Data.Vector as V (Vector, MVector)
> import qualified Data.Vector.Generic as V hiding (Vector)
> import qualified Data.Vector.Generic.Mutable as MV
> import qualified Data.Vector.Storable as SV
>
> import Control.Loop (numLoop)
>
> import qualified Data.ReedSolomon.Galois as Galois
> import Data.Vector.Generic.Exceptions (CatchST)
> import qualified Data.Vector.Generic.Exceptions as VE
> import qualified Data.Vector.Generic.Lifted as VL

/**
 * Matrix Algebra over an 8-bit Galois Field
 *
 * Copyright 2015, Klaus Post
 * Copyright 2015, Backblaze, Inc.
 */

package reedsolomon

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
)

// byte[row][col]
type matrix [][]byte

> -- | A row-major 'Matrix' of bytes.
> type Matrix = V.Vector (SV.Vector Word8)
>
> matrixSize :: Matrix -> (Int, Int)
> matrixSize m = (V.length m, V.length (V.head m))

// newMatrix returns a matrix of zeros.
func newMatrix(rows, cols int) (matrix, error) {
	if rows <= 0 {
		return nil, errInvalidRowSize
	}
	if cols <= 0 {
		return nil, errInvalidColSize
	}

	m := matrix(make([][]byte, rows))
	for i := range m {
		m[i] = make([]byte, cols)
	}
	return m, nil
}

// NewMatrixData initializes a matrix with the given row-major data.
// Note that data is not copied from input.
func newMatrixData(data [][]byte) (matrix, error) {
	m := matrix(data)
	err := m.Check()
	if err != nil {
		return nil, err
	}
	return m, nil
}

// IdentityMatrix returns an identity matrix of the given size.
func identityMatrix(size int) (matrix, error) {
	m, err := newMatrix(size, size)
	if err != nil {
		return nil, err
	}
	for i := range m {
		m[i][i] = 1
	}
	return m, nil
}

> identityMatrix :: Int -> Matrix
> identityMatrix size =
>     V.generate size $ \i ->
>         V.generate size $ \j ->
>             if i == j
>             then 1
>             else 0

// errInvalidRowSize will be returned if attempting to create a matrix with negative or zero row number.
var errInvalidRowSize = errors.New("invalid row size")

// errInvalidColSize will be returned if attempting to create a matrix with negative or zero column number.
var errInvalidColSize = errors.New("invalid column size")

// errColSizeMismatch is returned if the size of matrix columns mismatch.
var errColSizeMismatch = errors.New("column size is not the same for all rows")

> data DimensionMismatch = DimensionMismatch String
>   deriving (Show, Eq, Typeable)
> instance Exception DimensionMismatch
>
> throwDimensionMismatch :: MonadThrow m => String -> Matrix -> Matrix -> m a
> throwDimensionMismatch f m1 m2 = throwM $ DimensionMismatch message
>   where
>     message = unwords [ "Can't", f
>                       , "matrix of size", show (matrixSize m1)
>                       , "with matrix of size", show (matrixSize m2)
>                       ]

func (m matrix) Check() error {
	rows := len(m)
	if rows <= 0 {
		return errInvalidRowSize
	}
	cols := len(m[0])
	if cols <= 0 {
		return errInvalidColSize
	}

	for _, col := range m {
		if len(col) != cols {
			return errColSizeMismatch
		}
	}
	return nil
}

// String returns a human-readable string of the matrix contents.
//
// Example: [[1, 2], [3, 4]]
func (m matrix) String() string {
	var rowOut []string
	for _, row := range m {
		var colOut []string
		for _, col := range row {
			colOut = append(colOut, strconv.Itoa(int(col)))
		}
		rowOut = append(rowOut, "["+strings.Join(colOut, ", ")+"]")
	}
	return "[" + strings.Join(rowOut, ", ") + "]"
}

// Multiply multiplies this matrix (the one on the left) by another
// matrix (the one on the right) and returns a new matrix with the result.
func (m matrix) Multiply(right matrix) (matrix, error) {
	if len(m[0]) != len(right) {
		return nil, fmt.Errorf("columns on left (%d) is different than rows on right (%d)", len(m[0]), len(right))
	}
	result, _ := newMatrix(len(m), len(right[0]))
	for r, row := range result {
		for c := range row {
			var value byte
			for i := range m[0] {
				value ^= galMultiply(m[r][i], right[i][c])
			}
			result[r][c] = value
		}
	}
	return result, nil
}

> multiply :: MonadThrow m => Matrix -> Matrix -> m Matrix
> multiply m right
>     | V.length (V.head m) /= V.length right = throwDimensionMismatch "multiply" m right
>     | otherwise = return $
>         V.generate (V.length m) $ \r ->
>             V.generate (V.length (V.head right)) $ \c ->
>                 foldr
>                     (\i value ->
>                         let mri = V.unsafeIndex (V.unsafeIndex m r) i in
>                         let rightic = V.unsafeIndex (V.unsafeIndex right i) c in
>                         value `xor` Galois.galMultiply mri rightic)
>                     0
>                     [0 .. V.length (V.head m) - 1]

// Augment returns the concatenation of this matrix and the matrix on the right.
func (m matrix) Augment(right matrix) (matrix, error) {
	if len(m) != len(right) {
		return nil, errMatrixSize
	}

	result, _ := newMatrix(len(m), len(m[0])+len(right[0]))
	for r, row := range m {
		for c := range row {
			result[r][c] = m[r][c]
		}
		cols := len(m[0])
		for c := range right[0] {
			result[r][cols+c] = right[r][c]
		}
	}
	return result, nil
}

> augment :: MonadThrow m => Matrix -> Matrix -> m Matrix
> augment m right
>     | V.length m /= V.length right = throwDimensionMismatch "augment" m right
>     | otherwise = return $ V.zipWith (V.++) m right

// errMatrixSize is returned if matrix dimensions are doesn't match.
var errMatrixSize = errors.New("matrix sizes does not match")

func (m matrix) SameSize(n matrix) error {
	if len(m) != len(n) {
		return errMatrixSize
	}
	for i := range m {
		if len(m[i]) != len(n[i]) {
			return errMatrixSize
		}
	}
	return nil
}

// Returns a part of this matrix. Data is copied.
func (m matrix) SubMatrix(rmin, cmin, rmax, cmax int) (matrix, error) {
	result, err := newMatrix(rmax-rmin, cmax-cmin)
	if err != nil {
		return nil, err
	}
	// OPTME: If used heavily, use copy function to copy slice
	for r := rmin; r < rmax; r++ {
		for c := cmin; c < cmax; c++ {
			result[r-rmin][c-cmin] = m[r][c]
		}
	}
	return result, nil
}

> subMatrix :: Matrix -> Int -> Int -> Int -> Int -> Matrix
> subMatrix m rmin cmin rmax cmax =
>     V.generate (rmax - rmin) $ \r ->
>         V.generate (cmax - cmin) $ \c ->
>             V.unsafeIndex (V.unsafeIndex m (rmin + r)) (cmin + c)

// SwapRows Exchanges two rows in the matrix.
func (m matrix) SwapRows(r1, r2 int) error {
	if r1 < 0 || len(m) <= r1 || r2 < 0 || len(m) <= r2 {
		return errInvalidRowSize
	}
	m[r2], m[r1] = m[r1], m[r2]
	return nil
}

> swapRows :: V.MVector s a -> Int -> Int -> ST s ()
> swapRows = MV.swap


// IsSquare will return true if the matrix is square
// and nil if the matrix is square
func (m matrix) IsSquare() bool {
	if len(m) != len(m[0]) {
		return false
	}
	return true
}

> isSquare :: Matrix -> Bool
> isSquare m
>     | V.length m /= V.length (V.head m) = False
>     | otherwise = True

// errSingular is returned if the matrix is singular and cannot be inversed
var errSingular = errors.New("matrix is singular")

> data SingularMatrix = SingularMatrix
>   deriving (Show, Eq, Typeable)
> instance Exception SingularMatrix

// errNotSquare is returned if attempting to inverse a non-square matrix.
var errNotSquare = errors.New("only square matrices can be inverted")

// Invert returns the inverse of this matrix.
// Returns ErrSingular when the matrix is singular and doesn't have an inverse.
// The matrix must be square, otherwise ErrNotSquare is returned.
func (m matrix) Invert() (matrix, error) {
	if !m.IsSquare() {
		return nil, errNotSquare
	}

	size := len(m)
	work, _ := identityMatrix(size)
	work, _ = m.Augment(work)

	err := work.gaussianElimination()
	if err != nil {
		return nil, err
	}

	return work.SubMatrix(0, size, size, size*2)
}

> invert :: MonadThrow m => Matrix -> m Matrix
> invert m
>     | not (isSquare m) = throwM $ DimensionMismatch
>                                 $ unwords [ "Can't invert non-square matrix of size"
>                                           , show (matrixSize m)
>                                           ]
>     | otherwise = do
>         let size = V.length m
>         let work = identityMatrix size
>         work' <- augment m work
>         work'' <- gaussianElimination work'
>         return $ subMatrix work'' 0 size size (size * 2)

func (m matrix) gaussianElimination() error {
	rows := len(m)
	columns := len(m[0])
	// Clear out the part below the main diagonal and scale the main
	// diagonal to be 1.
	for r := 0; r < rows; r++ {
		// If the element on the diagonal is 0, find a row below
		// that has a non-zero and swap them.
		if m[r][r] == 0 {
			for rowBelow := r + 1; rowBelow < rows; rowBelow++ {
				if m[rowBelow][r] != 0 {
					m.SwapRows(r, rowBelow)
					break
				}
			}
		}
		// If we couldn't find one, the matrix is singular.
		if m[r][r] == 0 {
			return errSingular
		}
		// Scale to 1.
		if m[r][r] != 1 {
			scale := galDivide(1, m[r][r])
			for c := 0; c < columns; c++ {
				m[r][c] = galMultiply(m[r][c], scale)
			}
		}
		// Make everything below the 1 be a 0 by subtracting
		// a multiple of it.  (Subtraction and addition are
		// both exclusive or in the Galois field.)
		for rowBelow := r + 1; rowBelow < rows; rowBelow++ {
			if m[rowBelow][r] != 0 {
				scale := m[rowBelow][r]
				for c := 0; c < columns; c++ {
					m[rowBelow][c] ^= galMultiply(scale, m[r][c])
				}
			}
		}
	}

	// Now clear the part above the main diagonal.
	for d := 0; d < rows; d++ {
		for rowAbove := 0; rowAbove < d; rowAbove++ {
			if m[rowAbove][d] != 0 {
				scale := m[rowAbove][d]
				for c := 0; c < columns; c++ {
					m[rowAbove][c] ^= galMultiply(scale, m[d][c])
				}

			}
		}
	}
	return nil
}

> gaussianElimination :: MonadThrow m => Matrix -> m Matrix
> gaussianElimination = VE.modify $ \m -> do
>     let rows = MV.length m
>
>     numLoop 0 (rows - 1) $ \r -> do
>         mrr <- load m r r
>         when (mrr == 0) $ do
>             break <- lift $ newSTRef False
>             numLoop (r + 1) (rows - 1) $ \rowBelow -> do
>                 doBreak <- lift $ readSTRef break
>                 unless doBreak $ do
>                     mrowBelowr <- load m rowBelow r
>                     when (mrowBelowr /= 0) $ do
>                         lift $ swapRows m r rowBelow
>                         lift $ writeSTRef break True
>
>         mrr' <- load m r r
>         when (mrr' == 0) $ do
>             throwM SingularMatrix
>
>         when (mrr' /= 1) $ do
>             scale <- Galois.galDivide 1 mrr'
>             mr <- VL.read m r
>             let mr' = V.map (\mrc -> Galois.galMultiply mrc scale) mr
>             VL.write m r mr'
>
>         when (rows > r + 1) $ numLoop (r + 1) (rows - 1) $ \rowBelow -> do
>             mrowBelowr <- load m rowBelow r
>             when (mrowBelowr /= 0) $ do
>                 let scale = mrowBelowr
>                 mr <- VL.read m r
>                 mrowBelow <- VL.read m rowBelow
>                 let mrowBelow' = V.zipWith xor mrowBelow (V.map (\mrc -> Galois.galMultiply scale mrc) mr)
>                 VL.write m rowBelow mrowBelow'
>
>     numLoop 0 (rows - 1) $ \d ->
>         when (d > 0) $ numLoop 0 (d - 1) $ \rowAbove -> do
>             mrowAboved <- load m rowAbove d
>             when (mrowAboved /= 0) $ do
>                 let scale = mrowAboved
>                 mrowAbove <- VL.read m rowAbove
>                 md <- VL.read m d
>                 let mrowAbove' = V.zipWith xor mrowAbove (V.map (\mdc -> Galois.galMultiply scale mdc) md)
>                 VL.write m rowAbove mrowAbove'
>   where
>     load :: V.MVector s (SV.Vector Word8) -> Int -> Int -> CatchST s Word8
>     load m row col = do
>         r <- VL.read m row
>         return $ (V.!) r col

// Create a Vandermonde matrix, which is guaranteed to have the
// property that any subset of rows that forms a square matrix
// is invertible.
func vandermonde(rows, cols int) (matrix, error) {
	result, err := newMatrix(rows, cols)
	if err != nil {
		return nil, err
	}
	for r, row := range result {
		for c := range row {
			result[r][c] = galExp(byte(r), c)
		}
	}
	return result, nil
}

> vandermonde :: Int -> Int -> Matrix
> vandermonde rows cols =
>     V.generate rows $ \r ->
>         V.generate cols $ \c ->
>             Galois.galExp (fromIntegral r) c