/*
* Reed-Solomon error-correcting code decoder (Java)
*
* Copyright (c) 2017 Project Nayuki
* All rights reserved. Contact Nayuki for licensing.
* https://www.nayuki.io/page/reed-solomon-error-correcting-code-decoder
*/
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Objects;
/**
* Performs Reed-Solomon encoding and decoding. This object can encode a message into a codeword.
* The codeword can have some values modified by external code. Then this object can try
* to decode the codeword, and under some circumstances can reproduce the original message.
*
This class is immutable and thread-safe, but the argument arrays passed into methods are not thread-safe.
*/
public final class ReedSolomon {
/*---- Fields ----*/
/** The number of values in each message. Always at least 1. */
public final int messageLen;
/** The number of error correction values to expand the message by. Always at least 1. */
public final int eccLen;
/** The number of values in each codeword, equal to messageLen + eccLen. Always at least 2. */
public final int codewordLen;
// The field for message and codeword values, and for performing arithmetic operations on values. Not null.
private final Field f;
// An element of the field whose powers generate all the non-zero elements of the field. Not null.
private final E generator;
// The class object for the actual type parameter E, which is used in newArray(). Not null.
private Class elementType;
/*---- Constructor ----*/
/**
* Constructs a Reed-Solomon encoder-decoder with the specified field, lengths, and other parameters.
* Note: The class argument is used like this:
* {@code ReedSolomon rs = new ReedSolomon<>(f, gen, Integer.class, msgLen, eccLen);}
* @param f the field for all values and operations (not {@code null})
* @param gen a generator of the field {@code f} (not {@code null})
* @param elemType the class object for the type parameter {@code E} (not {@code null})
* @param msgLen the length of message arrays, which must be positive
* @param eccLen the number of values to expand each message by, which must be positive
* @throws NullPointerException if any of the object arguments is null
* @throws IllegalArgumentException if msgLen ≤ 0, eccLen ≤ 0, or mlgLen + eccLen > Integer.MAX_VALUE
*/
public ReedSolomon(Field f, E gen, Class elemType, int msgLen, int eccLen) {
// Check arguments
Objects.requireNonNull(f);
Objects.requireNonNull(gen);
Objects.requireNonNull(elemType);
if (msgLen <= 0 || eccLen <= 0 || Integer.MAX_VALUE - msgLen < eccLen)
throw new IllegalArgumentException("Invalid message or ECC length");
// Assign fields
this.f = f;
this.generator = gen;
this.elementType = elemType;
this.messageLen = msgLen;
this.eccLen = eccLen;
this.codewordLen = msgLen + eccLen;
}
/*---- Encoder methods ----*/
/**
* Returns a new array representing the codeword produced by encoding the specified message.
* If the message has the correct length and all its values are
* valid in the field, then this method is guaranteed to succeed.
* @param message the message to encode, whose length must equal {@code this.messageLen}
* @return a new array representing the codeword values
* @throws NullPointerException if the message array or any of its elements are {@code null}
* @throws IllegalArgumentException if the message array has the wrong length
*/
public E[] encode(E[] message) {
// Check arguments
Objects.requireNonNull(message);
if (message.length != messageLen)
throw new IllegalArgumentException("Invalid message length");
// Make the generator polynomial (this doesn't depend on the message)
E[] genPoly = makeGeneratorPolynomial();
// Compute the remainder ((message(x) * x^eccLen) mod genPoly(x)) by performing polynomial division.
// Process message bytes (polynomial coefficients) from the highest monomial power to the lowest power
E[] eccPoly = newArray(eccLen);
Arrays.fill(eccPoly, f.zero());
for (int i = messageLen - 1; i >= 0; i--) {
E factor = f.add(message[i], eccPoly[eccLen - 1]);
System.arraycopy(eccPoly, 0, eccPoly, 1, eccLen - 1);
eccPoly[0] = f.zero();
for (int j = 0; j < eccLen; j++)
eccPoly[j] = f.subtract(eccPoly[j], f.multiply(genPoly[j], factor));
}
// Negate the remainder
for (int i = 0; i < eccPoly.length; i++)
eccPoly[i] = f.negate(eccPoly[i]);
// Concatenate the message and ECC polynomials
E[] result = newArray(codewordLen);
System.arraycopy(eccPoly, 0, result, 0, eccLen);
System.arraycopy(message, 0, result, eccLen, messageLen);
return result;
}
// Computes the generator polynomial by multiplying powers of the generator value:
// genPoly(x) = (x - gen^0) * (x - gen^1) * ... * (x - gen^(eccLen-1)).
// The resulting array of coefficients is in little endian, i.e. from lowest to highest power, except
// that the very highest power (the coefficient for the x^eccLen term) is omitted because it's always 1.
// The result of this method can be pre-computed because it doesn't depend on the message to be encoded.
private E[] makeGeneratorPolynomial() {
// Start with the polynomial of 1*x^0, which is the multiplicative identity
E[] result = newArray(eccLen);
Arrays.fill(result, f.zero());
result[0] = f.one();
E genPow = f.one();
for (int i = 0; i < eccLen; i++) {
// At this point, genPow == generator^i.
// Multiply the current genPoly by (x - generator^i)
for (int j = eccLen - 1; j >= 0; j--) {
result[j] = f.multiply(f.negate(genPow), result[j]);
if (j >= 1)
result[j] = f.add(result[j - 1], result[j]);
}
genPow = f.multiply(generator, genPow);
}
return result;
}
/*---- Decoder methods ----*/
/**
* Attempts to decode the specified codeword with the maximum error-correcting
* capability allowed, returning either a best-guess message or {@code null}.
* If the number of erroneous values in the codeword is less than or equal to floor(eccLen / 2),
* then decoding is guaranteed to succeed. Otherwise an explicit failure ({@code null} answer)
* is most likely, but wrong answer and right answer are also possible too.
* @param codeword the codeword to decode, whose length must equal {@code this.codewordLen}
* @return a new array representing the decoded message, or {@code null} to indicate failure
* @throws NullPointerException if the codeword is {@code null}
* @throws IllegalArgumentException if the codeword array has the wrong length
*/
public E[] decode(E[] codeword) {
return decode(codeword, eccLen / 2);
}
/**
* Attempts to decode the specified codeword with the specified level of
* error-correcting capability, returning either a best-guess message or {@code null}.
* If the number of erroneous values in the codeword is less than or equal to numErrorsToCorrect,
* then decoding is guaranteed to succeed. Otherwise an explicit failure ({@code null} answer)
* is most likely, but wrong answer and right answer are also possible too.
* @param codeword the codeword to decode, whose length must equal {@code this.codewordLen}
* @param numErrorsToCorrect the number of errors in the codeword to try to fix,
* which must be between 0 to floor(eccLen / 2), inclusive
* @return a new array representing the decoded message, or {@code null} to indicate failure
* @throws NullPointerException if the codeword is {@code null}
* @throws IllegalArgumentException if the codeword array has the wrong length,
* or numErrorsToCorrect is out of range
*/
public E[] decode(E[] codeword, int numErrorsToCorrect) {
// Check arguments
Objects.requireNonNull(codeword);
if (codeword.length != codewordLen)
throw new IllegalArgumentException("Invalid codeword length");
if (numErrorsToCorrect < 0 || numErrorsToCorrect > eccLen / 2)
throw new IllegalArgumentException("Number of errors to correct is out of range");
// Calculate and check syndromes
E[] syndromes = calculateSyndromes(codeword);
if (!areAllZero(syndromes)) {
// At this point, we know the codeword must have some errors
if (numErrorsToCorrect == 0)
return null; // Only detect but not fix errors
// Try to solve for the error locator polynomial
E[] errLocPoly = calculateErrorLocatorPolynomial(syndromes, numErrorsToCorrect);
if (errLocPoly == null)
return null;
// Try to find the codeword indexes where errors might have occurred
int[] errLocs = findErrorLocations(errLocPoly, numErrorsToCorrect);
if (errLocs == null || errLocs.length == 0)
return null;
// Try to find the error values at these indexes
E[] errVals = calculateErrorValues(errLocs, syndromes);
if (errVals == null)
return null;
// Perform repairs to the codeword with the information just derived
E[] newCodeword = fixErrors(codeword, errLocs, errVals);
// Final sanity check by recomputing syndromes
E[] newSyndromes = calculateSyndromes(newCodeword);
if (!areAllZero(newSyndromes))
throw new AssertionError();
codeword = newCodeword;
}
// At this point, all syndromes are zero.
// Extract the message part of the codeword
return Arrays.copyOfRange(codeword, eccLen, codeword.length);
}
// Returns a new array representing the sequence of syndrome values for the given codeword.
// To summarize the math, syndrome[i] = codeword(generator^i).
private E[] calculateSyndromes(E[] codeword) {
// Check arguments
Objects.requireNonNull(codeword);
if (codeword.length != codewordLen)
throw new IllegalArgumentException();
// Evaluate the codeword polynomial at generator powers
E[] result = newArray(eccLen);
E genPow = f.one();
for (int i = 0; i < result.length; i++) {
result[i] = evaluatePolynomial(codeword, genPow);
genPow = f.multiply(generator, genPow);
}
return result;
}
// Returns a new array representing the coefficients of the error locator polynomial
// in little endian, or null if the syndrome values imply too many errors to handle.
private E[] calculateErrorLocatorPolynomial(E[] syndromes, int numErrorsToCorrect) {
// Check arguments
Objects.requireNonNull(syndromes);
if (syndromes.length != eccLen || numErrorsToCorrect <= 0 || numErrorsToCorrect > syndromes.length / 2)
throw new IllegalArgumentException();
// Copy syndrome values into augmented matrix
Matrix matrix = new Matrix<>(numErrorsToCorrect, numErrorsToCorrect + 1, f);
for (int r = 0; r < matrix.rowCount(); r++) {
for (int c = 0; c < matrix.columnCount(); c++) {
E val = syndromes[r + c];
if (c == matrix.columnCount() - 1)
val = f.negate(val);
matrix.set(r, c, val);
}
}
// Solve the system of linear equations
matrix.reducedRowEchelonForm();
// Create result vector filled with zeros. Note that columns without a pivot
// will yield variables that stay at the default value of zero
E[] result = newArray(numErrorsToCorrect + 1);
Arrays.fill(result, f.zero());
result[0] = f.one(); // Constant term is always 1, regardless of the matrix
// Find the column of the pivot in each row, and set the
// appropriate output variable's value based on the column index
outer:
for (int r = 0, c = 0; r < matrix.rowCount(); r++) {
// Advance the column index until a pivot is found, but handle specially if
// the rightmost column is identified as a pivot or if no column is a pivot
while (true) {
if (c == matrix.columnCount())
break outer;
else if (f.equals(matrix.get(r, c), f.zero()))
c++;
else if (c == matrix.columnCount() - 1)
return null; // Linear system is inconsistent
else
break;
}
// Copy the value in the rightmost column to the result vector
result[numErrorsToCorrect - c] = matrix.get(r, numErrorsToCorrect);
}
return result;
}
// Returns a new array that represents indexes into the codeword array where the value
// might be erroneous, or null if it is discovered that the decoding process is impossible.
// This method tries to find roots of the error locator polynomial by brute force.
private int[] findErrorLocations(E[] errLocPoly, int maxSolutions) {
// Check arguments
Objects.requireNonNull(errLocPoly);
if (maxSolutions <= 0 || maxSolutions > codewordLen)
throw new IllegalArgumentException();
// Create temporary buffer for roots found
int[] indexesFound = new int[maxSolutions];
int numFound = 0;
// Evaluate errLocPoly(generator^-i) for 0 <= i < codewordLen
E genRec = f.reciprocal(generator);
E genRecPow = f.one();
for (int i = 0; i < codewordLen; i++) {
// At this point, genRecPow == generator^-i
E polyVal = evaluatePolynomial(errLocPoly, genRecPow);
if (f.equals(polyVal, f.zero())) {
if (numFound >= indexesFound.length)
return null; // Too many solutions
indexesFound[numFound] = i;
numFound++;
}
genRecPow = f.multiply(genRec, genRecPow);
}
return Arrays.copyOf(indexesFound, numFound);
}
// Returns a new array representing the error values/magnitudes at the given error locations,
// or null if the information given is inconsistent (thus decoding is impossible).
// If the result of this method is not null, then after fixing the codeword it is guaranteed
// to have all zero syndromes (but it could be the wrong answer, unequal to the original message).
private E[] calculateErrorValues(int[] errLocs, E[] syndromes) {
// Check arguments
Objects.requireNonNull(errLocs);
Objects.requireNonNull(syndromes);
if (syndromes.length != eccLen)
throw new IllegalArgumentException();
// Calculate and copy values into matrix
Matrix matrix = new Matrix<>(syndromes.length, errLocs.length + 1, f);
for (int c = 0; c < matrix.columnCount() - 1; c++) {
E genPow = pow(generator, errLocs[c]);
E genPowPow = f.one();
for (int r = 0; r < matrix.rowCount(); r++) {
matrix.set(r, c, genPowPow);
genPowPow = f.multiply(genPow, genPowPow);
}
}
for (int r = 0; r < matrix.rowCount(); r++)
matrix.set(r, matrix.columnCount() - 1, syndromes[r]);
// Solve matrix and check basic consistency
matrix.reducedRowEchelonForm();
if (!f.equals(matrix.get(matrix.columnCount() - 1, matrix.columnCount() - 1), f.zero()))
return null; // System of linear equations is inconsistent
// Check that the top left side equals an identity matrix,
// and extract the rightmost column as result vector
E[] result = newArray(errLocs.length);
for (int i = 0; i < result.length; i++) {
if (!f.equals(matrix.get(i, i), f.one()))
return null; // Linear system is under-determined; no unique solution
result[i] = matrix.get(i, matrix.columnCount() - 1);
}
return result;
}
// Returns a new codeword representing the given codeword with the given errors subtracted.
// Always succeeds, as long as the array values are well-formed.
private E[] fixErrors(E[] codeword, int[] errLocs, E[] errVals) {
// Check arguments
Objects.requireNonNull(codeword);
Objects.requireNonNull(errLocs);
Objects.requireNonNull(errVals);
if (codeword.length != codewordLen || errLocs.length != errVals.length)
throw new IllegalArgumentException();
// Clone the codeword and change values at specific indexes
E[] result = codeword.clone();
for (int i = 0; i < errLocs.length; i++)
result[errLocs[i]] = f.subtract(result[errLocs[i]], errVals[i]);
return result;
}
/*---- Simple utility methods ----*/
// Returns a new array of the given length with E as the actual element type.
// This method exists so that unchecked generic operations are confined in one place here.
@SuppressWarnings("unchecked")
private E[] newArray(int len) {
if (len < 0)
throw new NegativeArraySizeException();
return (E[])Array.newInstance(elementType, len);
}
// Returns the value of the given polynomial at the given point. The polynomial is represented
// in little endian. In other words, this method evaluates result = polynomial(point)
// = polynomial[0]*point^0 + polynomial[1]*point^1 + ... + ponylomial[len-1]*point^(len-1).
private E evaluatePolynomial(E[] polynomial, E point) {
Objects.requireNonNull(polynomial);
Objects.requireNonNull(point);
// Horner's method
E result = f.zero();
for (int i = polynomial.length - 1; i >= 0; i--) {
result = f.multiply(point, result);
result = f.add(polynomial[i], result);
}
return result;
}
// Tests whether all elements of the given array are equal to the field's zero element.
private boolean areAllZero(E[] array) {
Objects.requireNonNull(array);
for (E val : array) {
if (!f.equals(val, f.zero()))
return false;
}
return true;
}
// Returns the given field element raised to the given power. The power must be non-negative.
private E pow(E base, int exp) {
Objects.requireNonNull(base);
if (exp < 0)
throw new UnsupportedOperationException();
E result = f.one();
for (int i = 0; i < exp; i++)
result = f.multiply(base, result);
return result;
}
}