mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 10:07:26 -05:00
Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -37,7 +37,8 @@ public class Hamming13
|
||||
*
|
||||
* @param message containing a Hamming protected word
|
||||
* @param offset to the start of the protected word
|
||||
* @return message index for an error bit or -1 if no errors are detected.
|
||||
* @return message index for an error bit, -1 if no errors are detected, or 1000 if more than one bit error is
|
||||
* detected.
|
||||
*/
|
||||
public static int getErrorIndex(BinaryMessage message, int offset)
|
||||
{
|
||||
@@ -45,7 +46,8 @@ public class Hamming13
|
||||
|
||||
if(syndrome > 0)
|
||||
{
|
||||
return offset + ERROR_INDEX[syndrome];
|
||||
int errorIndex = ERROR_INDEX[syndrome];
|
||||
return errorIndex >= 0 ? offset + errorIndex : IHamming.MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
return IHamming.NO_ERRORS;
|
||||
|
||||
@@ -47,13 +47,6 @@ public class Hamming16 implements IHamming
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
//If the syndrome indicates the error is in the final parity bit position, and we already have odd parity, then
|
||||
//flag it as invalid for multiple errors.
|
||||
if(syndrome == 1 && message.getSubMessage(offset, offset + 16).cardinality() % 2 == 1) //check final parity bit
|
||||
{
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
|
||||
@@ -46,13 +46,6 @@ public class Hamming17 implements IHamming
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
//If the syndrome indicates the error is in the final parity bit position, and we already have odd parity, then
|
||||
//flag it as invalid for multiple errors.
|
||||
if(syndrome == 1 && message.getSubMessage(offset, offset + 17).cardinality() % 2 == 1) //check final parity bit
|
||||
{
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
|
||||
421
src/main/java/io/github/dsheirer/edac/ProductCodeDecoder.java
Normal file
421
src/main/java/io/github/dsheirer/edac/ProductCodeDecoder.java
Normal file
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* Hard decision minimum distance decoder for binary product codes, where each row is a codeword of a short row code
|
||||
* and each column is a codeword of a systematic column code with the column parity bits in the final rows.
|
||||
*
|
||||
* Finds the minimum weight error pattern that produces the received row and column syndromes, using an iterative
|
||||
* deepening search over the error patterns (coset) of each information row syndrome. The column parity row errors
|
||||
* are fully determined by the column syndromes and the information row error choices, and the column parity rows then
|
||||
* also match their row syndromes, so only the information rows are searched.
|
||||
*
|
||||
* Corrects every error pattern up to half the product code minimum distance, and most patterns up to the maximum
|
||||
* error count. When more than one error pattern has the minimum weight, the received word is equidistant from more
|
||||
* than one codeword and is treated as uncorrectable.
|
||||
*
|
||||
* Row words are integers where bit i (LSB = 0) holds the bit in column i. Instances are thread safe.
|
||||
*/
|
||||
public class ProductCodeDecoder
|
||||
{
|
||||
public static final int UNCORRECTABLE = -1;
|
||||
//Bounds the decode time for pathological inputs (~20 ms). Error injection testing with random error patterns of
|
||||
//each weight: the largest search was 630k nodes (3 ms) for BPTC(196,96) with a maximum error count of 12.
|
||||
private static final long MAXIMUM_SEARCH_NODES = 4_000_000;
|
||||
private final int mColumnCount;
|
||||
private final int mRowCount;
|
||||
private final int mInformationRowCount;
|
||||
private final int[] mRowChecks;
|
||||
private final int[] mColumnChecks;
|
||||
private final int mColumnCheckBits;
|
||||
private final long mColumnGroupMask;
|
||||
private final int mMaximumErrors;
|
||||
private final int[][] mCosets;
|
||||
private final int[] mMinimumWeights;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param rowChecks parity check matrix column (syndrome of a single bit error) for each row code bit position, where
|
||||
* the array length is the column count (maximum 20).
|
||||
* @param columnChecks parity check matrix column for each column code bit position, where the array length is the
|
||||
* row count. The column code must be systematic, with distinct single bit parity checks for the final (parity)
|
||||
* rows. For a single (even) parity check column code, each value is 1.
|
||||
* @param maximumErrors maximum number of bit errors to correct.
|
||||
*/
|
||||
public ProductCodeDecoder(int[] rowChecks, int[] columnChecks, int maximumErrors)
|
||||
{
|
||||
mColumnCount = rowChecks.length;
|
||||
mRowCount = columnChecks.length;
|
||||
mRowChecks = rowChecks.clone();
|
||||
mColumnChecks = columnChecks.clone();
|
||||
mMaximumErrors = maximumErrors;
|
||||
mColumnCheckBits = bitLength(mColumnChecks);
|
||||
mInformationRowCount = mRowCount - mColumnCheckBits;
|
||||
|
||||
if(mColumnCount < 1 || mColumnCount > 20)
|
||||
{
|
||||
throw new IllegalArgumentException("Column count must be in range 1 - 20");
|
||||
}
|
||||
|
||||
if(mColumnCount * mColumnCheckBits > Long.SIZE || mInformationRowCount < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Unsupported column code dimensions");
|
||||
}
|
||||
|
||||
//Verify the final rows are the systematic parity rows
|
||||
int parityChecks = 0;
|
||||
|
||||
for(int row = mInformationRowCount; row < mRowCount; row++)
|
||||
{
|
||||
int check = mColumnChecks[row];
|
||||
|
||||
if(Integer.bitCount(check) != 1 || (parityChecks & check) != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Column code parity rows must have distinct single bit checks");
|
||||
}
|
||||
|
||||
parityChecks |= check;
|
||||
}
|
||||
|
||||
long groupMask = 0;
|
||||
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
groupMask |= 1L << (column * mColumnCheckBits);
|
||||
}
|
||||
|
||||
mColumnGroupMask = groupMask;
|
||||
|
||||
//Coset lists: the error patterns for each row syndrome, up to the maximum error count, ordered by weight
|
||||
int syndromeCount = 1 << bitLength(mRowChecks);
|
||||
int maximumWeight = Math.min(maximumErrors, mColumnCount);
|
||||
int wordCount = 1 << mColumnCount;
|
||||
int[] syndromes = new int[wordCount];
|
||||
int[] sizes = new int[syndromeCount];
|
||||
|
||||
for(int word = 0; word < wordCount; word++)
|
||||
{
|
||||
syndromes[word] = getSyndrome(word, mRowChecks);
|
||||
|
||||
if(Integer.bitCount(word) <= maximumWeight)
|
||||
{
|
||||
sizes[syndromes[word]]++;
|
||||
}
|
||||
}
|
||||
|
||||
mCosets = new int[syndromeCount][];
|
||||
mMinimumWeights = new int[syndromeCount];
|
||||
|
||||
for(int syndrome = 0; syndrome < syndromeCount; syndrome++)
|
||||
{
|
||||
mCosets[syndrome] = new int[sizes[syndrome]];
|
||||
}
|
||||
|
||||
int[] filled = new int[syndromeCount];
|
||||
|
||||
for(int weight = 0; weight <= maximumWeight; weight++)
|
||||
{
|
||||
for(int word = 0; word < wordCount; word++)
|
||||
{
|
||||
if(Integer.bitCount(word) == weight)
|
||||
{
|
||||
int syndrome = syndromes[word];
|
||||
mCosets[syndrome][filled[syndrome]++] = word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int syndrome = 0; syndrome < syndromeCount; syndrome++)
|
||||
{
|
||||
mMinimumWeights[syndrome] = mCosets[syndrome].length > 0 ? Integer.bitCount(mCosets[syndrome][0]) :
|
||||
maximumWeight + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of bits needed to represent the largest value.
|
||||
*/
|
||||
private static int bitLength(int[] values)
|
||||
{
|
||||
int combined = 0;
|
||||
|
||||
for(int value: values)
|
||||
{
|
||||
combined |= value;
|
||||
}
|
||||
|
||||
return Integer.SIZE - Integer.numberOfLeadingZeros(combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the parity check matrix columns for a code from a syndrome calculator, as the syndrome of each single
|
||||
* bit error.
|
||||
* @param length of the code in bits
|
||||
* @param syndrome calculator for a binary message of the code length.
|
||||
* @return parity check matrix column for each bit position.
|
||||
*/
|
||||
public static int[] getParityChecks(int length, ToIntFunction<BinaryMessage> syndrome)
|
||||
{
|
||||
int[] checks = new int[length];
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
BinaryMessage message = new BinaryMessage(length);
|
||||
message.set(i);
|
||||
checks[i] = syndrome.applyAsInt(message);
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the syndrome of the word.
|
||||
* @param word where bit i (LSB = 0) is code bit position i
|
||||
* @param checks parity check matrix column for each bit position
|
||||
* @return syndrome, zero for a valid codeword.
|
||||
*/
|
||||
public static int getSyndrome(int word, int[] checks)
|
||||
{
|
||||
int syndrome = 0;
|
||||
|
||||
while(word != 0)
|
||||
{
|
||||
syndrome ^= checks[Integer.numberOfTrailingZeros(word)];
|
||||
word &= word - 1;
|
||||
}
|
||||
|
||||
return syndrome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if all rows and columns are valid codewords.
|
||||
* @param rows to check
|
||||
* @return true if valid.
|
||||
*/
|
||||
public boolean isValid(int[] rows)
|
||||
{
|
||||
long columnSyndromes = 0;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
if(getSyndrome(rows[row], mRowChecks) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
columnSyndromes ^= getColumnContribution(row, rows[row]);
|
||||
}
|
||||
|
||||
return columnSyndromes == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects the rows to the nearest valid product codeword when the number of bit errors doesn't exceed the
|
||||
* maximum error count and the nearest codeword is unique.
|
||||
* @param rows to correct, where bit i of each row is the bit in column i. Unchanged when uncorrectable.
|
||||
* @return number of corrected bit errors, or UNCORRECTABLE.
|
||||
*/
|
||||
public int decode(int[] rows)
|
||||
{
|
||||
if(rows.length != mRowCount)
|
||||
{
|
||||
throw new IllegalArgumentException("Expected " + mRowCount + " rows");
|
||||
}
|
||||
|
||||
Search search = new Search();
|
||||
boolean rowErrors = false;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
search.mRowSyndromes[row] = getSyndrome(rows[row], mRowChecks);
|
||||
rowErrors |= search.mRowSyndromes[row] != 0;
|
||||
search.mColumnSyndromes ^= getColumnContribution(row, rows[row]);
|
||||
}
|
||||
|
||||
if(!rowErrors && search.mColumnSyndromes == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Lower bound on the error count of the remaining rows, including the column parity rows, at each search depth
|
||||
int lowerBound = 0;
|
||||
|
||||
for(int row = mInformationRowCount; row < mRowCount; row++)
|
||||
{
|
||||
lowerBound += mMinimumWeights[search.mRowSyndromes[row]];
|
||||
}
|
||||
|
||||
search.mLowerBounds[mInformationRowCount] = lowerBound;
|
||||
|
||||
for(int row = mInformationRowCount - 1; row >= 0; row--)
|
||||
{
|
||||
search.mLowerBounds[row] = search.mLowerBounds[row + 1] + mMinimumWeights[search.mRowSyndromes[row]];
|
||||
}
|
||||
|
||||
int start = Math.max(search.mLowerBounds[0], getErrorColumnCount(search.mColumnSyndromes));
|
||||
|
||||
for(search.mBudget = start; search.mBudget <= mMaximumErrors; search.mBudget++)
|
||||
{
|
||||
search(search, 0, 0, 0L);
|
||||
|
||||
if(search.mAborted || search.mSolutionCount > 1)
|
||||
{
|
||||
return UNCORRECTABLE;
|
||||
}
|
||||
|
||||
if(search.mSolutionCount == 1)
|
||||
{
|
||||
for(int row = 0; row < mInformationRowCount; row++)
|
||||
{
|
||||
rows[row] ^= search.mSolution[row];
|
||||
}
|
||||
|
||||
long residual = search.mColumnSyndromes ^ search.mSolutionContribution;
|
||||
|
||||
for(int row = mInformationRowCount; row < mRowCount; row++)
|
||||
{
|
||||
int check = mColumnChecks[row];
|
||||
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
if(((residual >>> (column * mColumnCheckBits)) & check) != 0)
|
||||
{
|
||||
rows[row] ^= 1 << column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return search.mBudget;
|
||||
}
|
||||
}
|
||||
|
||||
return UNCORRECTABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Depth first search over the information row error patterns, for solutions that don't exceed the error budget.
|
||||
* @param search state
|
||||
* @param row to choose an error pattern for
|
||||
* @param used error count of the error patterns chosen for the preceding rows
|
||||
* @param contribution to the column syndromes from the error patterns chosen for the preceding rows
|
||||
*/
|
||||
private void search(Search search, int row, int used, long contribution)
|
||||
{
|
||||
if(++search.mNodes > MAXIMUM_SEARCH_NODES)
|
||||
{
|
||||
search.mAborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//The remaining column syndromes must be resolved by the remaining rows
|
||||
long residual = search.mColumnSyndromes ^ contribution;
|
||||
|
||||
if(row == mInformationRowCount)
|
||||
{
|
||||
//Each remaining column syndrome bit is a column parity row error
|
||||
if(used + Long.bitCount(residual) <= search.mBudget && ++search.mSolutionCount == 1)
|
||||
{
|
||||
System.arraycopy(search.mChosen, 0, search.mSolution, 0, mInformationRowCount);
|
||||
search.mSolutionContribution = contribution;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(used + Math.max(search.mLowerBounds[row], getErrorColumnCount(residual)) > search.mBudget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int remainingLowerBound = search.mLowerBounds[row + 1];
|
||||
|
||||
for(int error: mCosets[search.mRowSyndromes[row]])
|
||||
{
|
||||
int weight = Integer.bitCount(error);
|
||||
|
||||
if(used + weight + remainingLowerBound > search.mBudget)
|
||||
{
|
||||
break; //Error patterns are ordered by weight
|
||||
}
|
||||
|
||||
search.mChosen[row] = error;
|
||||
search(search, row + 1, used + weight, contribution ^ getColumnContribution(row, error));
|
||||
|
||||
if(search.mAborted || search.mSolutionCount > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the contribution of a row word to the column syndromes, where the syndrome for each column occupies
|
||||
* a group of column check bits, starting with column 0 in the LSBs.
|
||||
*/
|
||||
private long getColumnContribution(int row, int word)
|
||||
{
|
||||
long contribution = 0;
|
||||
long check = mColumnChecks[row];
|
||||
|
||||
while(word != 0)
|
||||
{
|
||||
contribution ^= check << (Integer.numberOfTrailingZeros(word) * mColumnCheckBits);
|
||||
word &= word - 1;
|
||||
}
|
||||
|
||||
return contribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of columns with a non-zero syndrome, where each requires at least one more bit error.
|
||||
*/
|
||||
private int getErrorColumnCount(long columnSyndromes)
|
||||
{
|
||||
long folded = columnSyndromes;
|
||||
|
||||
for(int shift = 1; shift < mColumnCheckBits; shift++)
|
||||
{
|
||||
folded |= columnSyndromes >>> shift;
|
||||
}
|
||||
|
||||
return Long.bitCount(folded & mColumnGroupMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search state for a single decode.
|
||||
*/
|
||||
private final class Search
|
||||
{
|
||||
private final int[] mRowSyndromes = new int[mRowCount];
|
||||
private final int[] mLowerBounds = new int[mInformationRowCount + 1];
|
||||
private final int[] mChosen = new int[mInformationRowCount];
|
||||
private final int[] mSolution = new int[mInformationRowCount];
|
||||
private long mColumnSyndromes;
|
||||
private long mSolutionContribution;
|
||||
private int mBudget;
|
||||
private int mSolutionCount;
|
||||
private long mNodes;
|
||||
private boolean mAborted;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,127 +21,222 @@ package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.IHamming;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) base implementation for BPTC codes using simple even parity column checks.
|
||||
* Block Product Turbo Code (BPTC) base implementation for BPTC codes where each row is a Hamming codeword and each
|
||||
* column has even parity (ie the final row is the column parity row). Bits are ordered row by row.
|
||||
*
|
||||
* Uses hard decision minimum distance decoding that corrects every error pattern up to half the product code minimum
|
||||
* distance and most patterns up to the maximum error count. Optionally falls back to row erasure decoding when the
|
||||
* minimum distance decoder can't correct the message.
|
||||
*/
|
||||
public abstract class BPTCBase
|
||||
{
|
||||
public static int ERRORS_NOT_CORRECTED = -2;
|
||||
private IHamming mHamming;
|
||||
private int mColumnCount;
|
||||
private int mRowCount;
|
||||
private final ProductCodeDecoder mDecoder;
|
||||
private final int[] mRowChecks;
|
||||
private final int[] mRowErrorColumns;
|
||||
private final int mColumnCount;
|
||||
private final int mRowCount;
|
||||
private final int mFallbackMaximumErrors;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param hamming to provide row checking using the correct Hamming error detection and correction algorithm.
|
||||
* @param column count
|
||||
* @param row count
|
||||
* @param rowChecks parity check matrix column for each bit position of the row (Hamming) code, where the array
|
||||
* length is the column count.
|
||||
* @param rowCount number of rows, including the column parity row.
|
||||
* @param maximumErrors maximum number of bit errors to correct with minimum distance decoding.
|
||||
* @param fallbackMaximumErrors maximum number of bit errors to correct with row erasure decoding when the minimum
|
||||
* distance decoder can't correct the message, or 0 to disable the fallback.
|
||||
*/
|
||||
public BPTCBase(IHamming hamming, int columnCount, int rowCount)
|
||||
public BPTCBase(int[] rowChecks, int rowCount, int maximumErrors, int fallbackMaximumErrors)
|
||||
{
|
||||
mHamming = hamming;
|
||||
mColumnCount = columnCount;
|
||||
mRowChecks = rowChecks.clone();
|
||||
mColumnCount = rowChecks.length;
|
||||
mRowCount = rowCount;
|
||||
mFallbackMaximumErrors = fallbackMaximumErrors;
|
||||
int[] columnChecks = new int[rowCount];
|
||||
Arrays.fill(columnChecks, 1);
|
||||
mDecoder = new ProductCodeDecoder(rowChecks, columnChecks, maximumErrors);
|
||||
|
||||
//Single bit error locator for the row code: column for each syndrome, or -1
|
||||
int combined = 0;
|
||||
|
||||
for(int check: rowChecks)
|
||||
{
|
||||
combined |= check;
|
||||
}
|
||||
|
||||
mRowErrorColumns = new int[Integer.highestOneBit(combined) << 1];
|
||||
Arrays.fill(mRowErrorColumns, -1);
|
||||
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
mRowErrorColumns[rowChecks[column]] = column;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the correct() method to employ an optional attempt to correct multiple row, two-bit errors where each
|
||||
* set of row errors don't shadow each other based on a test where the column cardinality is twice the row
|
||||
* cardinality. This is probably not mathematically correct and should only be enabled when the decoded message
|
||||
* payload has a good CRC check after the extraction (ie. only use for FLC).
|
||||
* @return true if enabled, false (default) otherwise
|
||||
*/
|
||||
protected boolean canCorrectMultiRow2BitErrors()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction.
|
||||
* Performs error detection and correction. When corrected, the corrected bit count is incremented by the number
|
||||
* of corrected bits. Otherwise, the message is unchanged and the corrected bit count is set to ERRORS_NOT_CORRECTED.
|
||||
* @param message to correct
|
||||
* @return true if the message was corrected or false if there are uncorrectable errors.
|
||||
*/
|
||||
public boolean correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
BinaryMessage columns = getColumnErrors(message);
|
||||
BinaryMessage rows = getRowErrors(message);
|
||||
int[] rows = new int[mRowCount];
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
return true;
|
||||
rows[row] = getRow(row, message);
|
||||
}
|
||||
|
||||
List<Integer> correctedIndexes = new ArrayList<>();
|
||||
int corrected = mDecoder.decode(rows);
|
||||
|
||||
//1: fix non-shadowed single bit errors in each row
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
if(corrected == ProductCodeDecoder.UNCORRECTABLE && mFallbackMaximumErrors > 0)
|
||||
{
|
||||
int index = correct1BitErrors(row, message, rows);
|
||||
corrected = correctRowErasure(rows);
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
if(corrected == ProductCodeDecoder.UNCORRECTABLE)
|
||||
{
|
||||
message.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(corrected > 0)
|
||||
{
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
correctedIndexes.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Refresh the column errors
|
||||
columns = getColumnErrors(message);
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
message.incrementCorrectedBitCount(correctedIndexes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
//2: fix non-shadowed multi bit errors (up to 2) in each row
|
||||
if(columns.cardinality() <= 2)
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
List<Integer> solution = correctMultiBitErrors(row, message, columns, rows);
|
||||
|
||||
if(solution.size() > 0)
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
correctedIndexes.addAll(solution);
|
||||
message.set(row * mColumnCount + column, (rows[row] & (1 << column)) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
message.incrementCorrectedBitCount(corrected);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row erasure decoding fallback for messages that the minimum distance decoder can't correct (more bit errors than
|
||||
* the maximum, or equidistant from more than one codeword). Corrects single bit errors in each row and then
|
||||
* recomputes one row from the column parity of the other rows, choosing the row that produces the nearest
|
||||
* codeword. Real-world messages can have bit errors that this corrects beyond the minimum distance decoder maximum.
|
||||
* @param rows to correct, unchanged when uncorrectable.
|
||||
* @return number of corrected bit errors, or UNCORRECTABLE.
|
||||
*/
|
||||
private int correctRowErasure(int[] rows)
|
||||
{
|
||||
int[] decoded = rows.clone();
|
||||
int failedRow = -1;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
int syndrome = ProductCodeDecoder.getSyndrome(rows[row], mRowChecks);
|
||||
|
||||
if(syndrome != 0)
|
||||
{
|
||||
int column = mRowErrorColumns[syndrome];
|
||||
|
||||
if(column >= 0)
|
||||
{
|
||||
decoded[row] ^= 1 << column;
|
||||
}
|
||||
else if(failedRow >= 0)
|
||||
{
|
||||
return ProductCodeDecoder.UNCORRECTABLE; //More than one row with uncorrectable errors
|
||||
}
|
||||
else
|
||||
{
|
||||
failedRow = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//3: fix single row, multi-column errors
|
||||
if(rows.cardinality() == 1 && !columns.isEmpty())
|
||||
{
|
||||
List<Integer> solution = correctMultiBitErrors(rows.nextSetBit(0), message, columns, rows);
|
||||
int[] best = null;
|
||||
int bestDistance = Integer.MAX_VALUE;
|
||||
boolean tie = false;
|
||||
|
||||
if(!solution.isEmpty())
|
||||
for(int erased = 0; erased < mRowCount; erased++)
|
||||
{
|
||||
if(failedRow >= 0 && erased != failedRow)
|
||||
{
|
||||
correctedIndexes.addAll(solution);
|
||||
continue;
|
||||
}
|
||||
|
||||
int[] candidate = decoded.clone();
|
||||
int parity = 0;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
if(row != erased)
|
||||
{
|
||||
parity ^= candidate[row];
|
||||
}
|
||||
}
|
||||
|
||||
candidate[erased] = parity;
|
||||
int distance = 0;
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
distance += Integer.bitCount(candidate[row] ^ rows[row]);
|
||||
}
|
||||
|
||||
if(distance < bestDistance)
|
||||
{
|
||||
best = candidate;
|
||||
bestDistance = distance;
|
||||
tie = false;
|
||||
}
|
||||
else if(distance == bestDistance && !Arrays.equals(candidate, best))
|
||||
{
|
||||
tie = true;
|
||||
}
|
||||
}
|
||||
|
||||
//4: fix one or more rows that each have 2 bit errors, not shadowing each other.
|
||||
if(canCorrectMultiRow2BitErrors() && rows.cardinality() > 0 && columns.cardinality() / rows.cardinality() == 2)
|
||||
if(tie || bestDistance > mFallbackMaximumErrors)
|
||||
{
|
||||
List<Integer> solution = correctMultipleRowTwoBitErrorsNotShadowing(columns, rows, message);
|
||||
return ProductCodeDecoder.UNCORRECTABLE;
|
||||
}
|
||||
|
||||
if(!solution.isEmpty())
|
||||
System.arraycopy(best, 0, rows, 0, mRowCount);
|
||||
return bestDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row word from the message, where bit i is the bit in column i.
|
||||
*/
|
||||
private int getRow(int row, BinaryMessage message)
|
||||
{
|
||||
int word = 0;
|
||||
int offset = row * mColumnCount;
|
||||
|
||||
for(int column = 0; column < mColumnCount; column++)
|
||||
{
|
||||
if(message.get(offset + column))
|
||||
{
|
||||
correctedIndexes.addAll(solution);
|
||||
word |= 1 << column;
|
||||
}
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
message.incrementCorrectedBitCount(correctedIndexes.size());
|
||||
return true;
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
message.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
/**
|
||||
* Calculates the row (Hamming) syndrome.
|
||||
* @param row to check
|
||||
* @param message containing bits to check
|
||||
* @return syndrome, zero when the row is a valid codeword.
|
||||
*/
|
||||
private int getRowSyndrome(int row, BinaryMessage message)
|
||||
{
|
||||
return ProductCodeDecoder.getSyndrome(getRow(row, message), mRowChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,49 +256,6 @@ public abstract class BPTCBase
|
||||
return correct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the column from the bit index/offset
|
||||
* @param index for a bit
|
||||
* @return column that contains the index
|
||||
*/
|
||||
public int getColumn(int index)
|
||||
{
|
||||
return index % mColumnCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for a bit that exists at the row and column.
|
||||
* @param column for the bit.
|
||||
* @param row for the bit
|
||||
* @return index for the bit.
|
||||
*/
|
||||
public int getIndex(int column, int row)
|
||||
{
|
||||
return row * mColumnCount + column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the row for bit errors.
|
||||
* @param row to check
|
||||
* @param message containing bits to check
|
||||
* @return true if the row passes the Hamming check or false if not.
|
||||
*/
|
||||
private boolean isRowCorrect(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return mHamming.getErrorIndex(message, row * mColumnCount) == IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the bit error in the message
|
||||
* @param row to check
|
||||
* @param message with bits
|
||||
* @return error index in the range 0-195, or -1 if the message row has no bit errors.
|
||||
*/
|
||||
private int getRowErrorIndex(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return mHamming.getErrorIndex(message, row * mColumnCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bit map indicating the row errors.
|
||||
* @param message containing rows.
|
||||
@@ -215,7 +267,7 @@ public abstract class BPTCBase
|
||||
|
||||
for(int row = 0; row < mRowCount; row++)
|
||||
{
|
||||
rows.set(row, !isRowCorrect(row, message));
|
||||
rows.set(row, getRowSyndrome(row, message) != 0);
|
||||
}
|
||||
|
||||
return rows;
|
||||
@@ -238,191 +290,6 @@ public abstract class BPTCBase
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the indices that make up the intersections of the row and column errors.
|
||||
* @param rows bitmap indicating rows with errors
|
||||
* @param columns bitmap indicating columns with errors
|
||||
* @return set of intersection indices.
|
||||
*/
|
||||
public List<Integer> getIntersectionIndices(BinaryMessage columns, BinaryMessage rows)
|
||||
{
|
||||
List<Integer> intersections = new ArrayList<>();
|
||||
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < mColumnCount; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
intersections.add(getIndex(column, row));
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the indices that make up the intersections of the row and column errors.
|
||||
* @param rows bitmap indicating rows with errors
|
||||
* @param columns bitmap indicating columns with errors
|
||||
* @return set of intersection indices.
|
||||
*/
|
||||
public List<Integer> getIntersectionIndices(BinaryMessage columns, int row)
|
||||
{
|
||||
List<Integer> intersections = new ArrayList<>();
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < mColumnCount; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
intersections.add(getIndex(column, row));
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct multiple rows with two-bit errors each where the error columns are not shadowing each other.
|
||||
* @param columns error map
|
||||
* @param rows error map
|
||||
* @param message to correct
|
||||
* @return corrected indices
|
||||
*/
|
||||
public List<Integer> correctMultipleRowTwoBitErrorsNotShadowing(BinaryMessage columns, BinaryMessage rows,
|
||||
CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> solution = new ArrayList<>();
|
||||
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < mRowCount; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
for(int column1 = columns.nextSetBit(0); column1 >= 0; column1 = columns.nextSetBit(column1 + 1))
|
||||
{
|
||||
for(int column2 = columns.nextSetBit(column1 + 1); column2 >= 0; column2 = columns.nextSetBit(column2 + 1))
|
||||
{
|
||||
int index1 = getIndex(column1, row);
|
||||
int index2 = getIndex(column2, row);
|
||||
message.flip(index1);
|
||||
message.flip(index2);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
solution.add(index1);
|
||||
solution.add(index2);
|
||||
columns.clear(column1);
|
||||
columns.clear(column2);
|
||||
rows.clear(row);
|
||||
column1 = mColumnCount;
|
||||
column2 = mColumnCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
message.flip(index1);
|
||||
message.flip(index2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return solution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects single bit errors in each row using only the Hamming error index.
|
||||
* @param row to correct
|
||||
* @param message with bits
|
||||
* @param rows error flag set
|
||||
* @return bit index that was corrected or -2 if the error was not corrected
|
||||
*/
|
||||
public int correct1BitErrors(int row, CorrectedBinaryMessage message, BinaryMessage rows)
|
||||
{
|
||||
int index = getRowErrorIndex(row, message);
|
||||
|
||||
if(index >= 0 && index < IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
rows.clear(row);
|
||||
return index;
|
||||
}
|
||||
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return ERRORS_NOT_CORRECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects multiple bit errors in a single row when all other rows are corrected first, by flipping the
|
||||
* bits in the row that show errors in the columns and testing the solution.
|
||||
* @param row to correct
|
||||
* @param message with bits
|
||||
* @param columns bit error matrix
|
||||
* @param rows bit error matrix
|
||||
* @return solution or an empty list if no solution.
|
||||
*/
|
||||
public List<Integer> correctMultiBitErrors(int row, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows)
|
||||
{
|
||||
//Test flipping bits in the row everywhere that we have a column error
|
||||
List<Integer> testFlips = new ArrayList<>();
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < mColumnCount;
|
||||
column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
int toFlip = getIndex(column, row);
|
||||
message.flip(toFlip);
|
||||
testFlips.add(toFlip);
|
||||
}
|
||||
|
||||
if(!isRowCorrect(row, message))
|
||||
{
|
||||
int index = getRowErrorIndex(row, message);
|
||||
|
||||
if(index >= 0 && index < IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(testFlips.contains(index))
|
||||
{
|
||||
testFlips.removeIf(value -> value == index);
|
||||
}
|
||||
else
|
||||
{
|
||||
testFlips.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
//Check the columns also
|
||||
|
||||
boolean columnsCorrect = true;
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
columnsCorrect &= isColumnCorrect(getColumn(flip), message);
|
||||
}
|
||||
|
||||
if(columnsCorrect)
|
||||
{
|
||||
rows.clear(row);
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
columns.clear(getColumn(flip));
|
||||
}
|
||||
|
||||
return testFlips;
|
||||
}
|
||||
}
|
||||
|
||||
for(Integer flip: testFlips)
|
||||
{
|
||||
message.flip(flip);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void logErrorMap(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -456,15 +323,10 @@ public abstract class BPTCBase
|
||||
int offset = row * mColumnCount;
|
||||
sb.append(" Row ").append(row).append(": ");
|
||||
sb.append(message.getSubMessage(offset, offset + mColumnCount));
|
||||
int index = getRowErrorIndex(row, message);
|
||||
if(index != IHamming.NO_ERRORS)
|
||||
int syndrome = getRowSyndrome(row, message);
|
||||
if(syndrome != 0)
|
||||
{
|
||||
sb.append(" < Index:").append(index);
|
||||
|
||||
if(index == IHamming.MULTIPLE_ERRORS)
|
||||
{
|
||||
sb.append(" (2+ bit errors)");
|
||||
}
|
||||
sb.append(" < Syndrome:0x").append(Integer.toHexString(syndrome).toUpperCase());
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming16;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
import io.github.dsheirer.edac.soft.SoftBlockCode;
|
||||
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
|
||||
|
||||
@@ -39,10 +40,12 @@ public class BPTC_128_77 extends BPTCBase
|
||||
//require correcting high reliability bits are rejected to avoid false decodes. Noise injection testing: all
|
||||
//soft decisions with a cost up to 3.0 were correct, whereas most of those with a higher cost were false decodes.
|
||||
private static final float SOFT_MAX_CORRECTION_COST = 3.0f;
|
||||
//Maximum corrected bit count for a hard decision result. Hard decisions that correct more bits are treated as
|
||||
//uncorrectable to avoid false decodes, given the weak 5-bit checksum. Noise injection testing: all hard
|
||||
//decisions with more than 6 corrected bits were false decodes.
|
||||
private static final int HARD_MAX_CORRECTED_BITS = 6;
|
||||
//Maximum number of bit errors corrected by the hard decision minimum distance decoder. The product code minimum
|
||||
//distance of 8 guarantees correction of 3 bit errors. Correcting more increases the miscorrection rate, and the
|
||||
//embedded link control is only protected by a 5-bit checksum, with soft decision decoding only applied when hard
|
||||
//decision decoding fails. AWGN simulation of the hard + soft decoding sequence: a maximum of 5 decoded more
|
||||
//messages than the previous hard decision decoder at each noise level, with 3-5x fewer false decodes.
|
||||
private static final int HARD_MAXIMUM_ERRORS = 5;
|
||||
//Soft decision decoder for the Hamming(16,11,4) rows and even parity columns
|
||||
private static final SoftProductCodeDecoder SOFT_DECODER = new SoftProductCodeDecoder(
|
||||
SoftBlockCode.fromCodewordTest(COLUMN_COUNT, word -> Hamming16.getSyndrome(toMessage(word), 0) == 0),
|
||||
@@ -68,16 +71,9 @@ public class BPTC_128_77 extends BPTCBase
|
||||
|
||||
public BPTC_128_77()
|
||||
{
|
||||
super(new Hamming16(), 16, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the base class to attempt to correct multiple rows of 2-bit errors each
|
||||
*/
|
||||
@Override
|
||||
protected boolean canCorrectMultiRow2BitErrors()
|
||||
{
|
||||
return true;
|
||||
//No row erasure fallback, given the weak 5-bit checksum. Recording testing: no messages were lost without it.
|
||||
super(ProductCodeDecoder.getParityChecks(COLUMN_COUNT, message -> Hamming16.getSyndrome(message, 0)), ROW_COUNT,
|
||||
HARD_MAXIMUM_ERRORS, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,9 +87,8 @@ public class BPTC_128_77 extends BPTCBase
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction and extracts the payload from the BPTC encoded message. Hard decision
|
||||
* results that exceed the maximum corrected bit count are treated as uncorrectable. When the hard decision
|
||||
* decoder can't correct the message and soft bits are provided, applies iterative soft decision decoding.
|
||||
* Performs error detection and correction and extracts the payload from the BPTC encoded message. When the hard
|
||||
* decision decoder can't correct the message and soft bits are provided, applies iterative soft decision decoding.
|
||||
* @param message with BPTC encoding.
|
||||
* @param softBits for the 128 (interleaved) bits of the message where a positive value favors a 1 bit, or null.
|
||||
* @return decoded message.
|
||||
@@ -103,11 +98,6 @@ public class BPTC_128_77 extends BPTCBase
|
||||
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||
correct(deinterleaved);
|
||||
|
||||
if(deinterleaved.getCorrectedBitCount() > HARD_MAX_CORRECTED_BITS)
|
||||
{
|
||||
deinterleaved.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
|
||||
}
|
||||
|
||||
if(softBits != null && softBits.length == LENGTH && deinterleaved.getCorrectedBitCount() < 0)
|
||||
{
|
||||
CorrectedBinaryMessage softDecoded = decodeSoft(deinterleave(softBits));
|
||||
|
||||
@@ -22,9 +22,14 @@ package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming16;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field.
|
||||
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field. Each of the two
|
||||
* rows is the same Hamming(16,11,4) codeword, where the second row is inverted (odd column parity) for the reverse
|
||||
* channel, forming a code with a minimum distance of 8.
|
||||
* <p>
|
||||
* Uses hard decision minimum distance decoding across both rows, which corrects up to 3 bit errors.
|
||||
* <p>
|
||||
* See ETSI TS 102 361-1 B.2.2.1 and B.2.2.2
|
||||
*/
|
||||
@@ -32,63 +37,116 @@ public class BPTC_16_2
|
||||
{
|
||||
private static final int[] DEINTERLEAVE = new int[]{0, 24, 1, 25, 2, 26, 3, 27, 4, 28, 5, 29, 6, 30, 7, 31, 8, 16,
|
||||
9, 17, 10, 18, 11, 19, 12, 20, 13, 21, 14, 22, 15, 23};
|
||||
private static final int ROW_LENGTH = 16;
|
||||
private static final int ROW_MASK = 0xFFFF;
|
||||
//Maximum number of bit errors to correct. This is the guaranteed correction capability for the minimum distance of
|
||||
//8, so the nearest codeword is always unique. Error injection testing: corrects all 1-3 bit error patterns and
|
||||
//miscorrects 3.9% of 5-bit error patterns. At a 3% bit error rate, corrects 98.5% of messages versus 93% with a
|
||||
//maximum of 2.
|
||||
private static final int MAXIMUM_ERRORS = 3;
|
||||
private static final int[] CODEWORDS = getCodewords();
|
||||
|
||||
/**
|
||||
* Identifies the Hamming(16,11,4) codewords where bit i of each codeword is message bit i.
|
||||
*/
|
||||
private static int[] getCodewords()
|
||||
{
|
||||
int[] checks = ProductCodeDecoder.getParityChecks(ROW_LENGTH, message -> Hamming16.getSyndrome(message, 0));
|
||||
int[] codewords = new int[1 << 11];
|
||||
int count = 0;
|
||||
|
||||
for(int word = 0; word <= ROW_MASK; word++)
|
||||
{
|
||||
if(ProductCodeDecoder.getSyndrome(word, checks) == 0)
|
||||
{
|
||||
codewords[count++] = word;
|
||||
}
|
||||
}
|
||||
|
||||
return codewords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unscramble and perform FEC checks per paragraph B.2.2.1 for Non-Reverse Channel Single Burst
|
||||
*
|
||||
* @param message with 32 interleaved bits.
|
||||
* @return descrambled and error checked message or null if the process fails or there are too many errors.
|
||||
* @return descrambled and error corrected message or null if there are too many errors.
|
||||
*/
|
||||
public static CorrectedBinaryMessage decodeShortBurst(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
|
||||
|
||||
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//Check for even parity. Bits 0-15 should be the same as bits 16-31.
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
if(deinterleaved.get(x) ^ deinterleaved.get(x + 16))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
return decode(deinterleave(message), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unscramble and perform FEC checks per paragraph B.2.2.2 for Reverse Channel Single Burst
|
||||
*
|
||||
* @param binaryMessage with 32 interleaved bits.
|
||||
* @return descrambled and error checked message or null if the process fails or there are too many errors.
|
||||
* @param message with 32 interleaved bits.
|
||||
* @return descrambled and error corrected message or null if there are too many errors.
|
||||
*/
|
||||
public static CorrectedBinaryMessage decodeReverseChannel(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = deinterleave(message);
|
||||
System.out.println(" DEINTER: " + deinterleaved.toHexString());
|
||||
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
|
||||
System.out.println(" DECODED: " + deinterleaved.toHexString());
|
||||
System.out.println("FEC:" + fec);
|
||||
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
|
||||
return decode(deinterleave(message), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the deinterleaved message to the nearest codeword.
|
||||
* @param deinterleaved message
|
||||
* @param oddParity true when the second row is the inverse of the first row (reverse channel).
|
||||
* @return corrected message with the corrected bit count set, or null if there are too many errors.
|
||||
*/
|
||||
private static CorrectedBinaryMessage decode(CorrectedBinaryMessage deinterleaved, boolean oddParity)
|
||||
{
|
||||
int row1 = 0;
|
||||
int row2 = 0;
|
||||
|
||||
for(int x = 0; x < ROW_LENGTH; x++)
|
||||
{
|
||||
if(deinterleaved.get(x))
|
||||
{
|
||||
row1 |= 1 << x;
|
||||
}
|
||||
|
||||
if(deinterleaved.get(x + ROW_LENGTH))
|
||||
{
|
||||
row2 |= 1 << x;
|
||||
}
|
||||
}
|
||||
|
||||
if(oddParity)
|
||||
{
|
||||
row2 ^= ROW_MASK;
|
||||
}
|
||||
|
||||
int bestDistance = Integer.MAX_VALUE;
|
||||
int bestCodeword = 0;
|
||||
|
||||
for(int codeword: CODEWORDS)
|
||||
{
|
||||
int distance = Integer.bitCount(codeword ^ row1) + Integer.bitCount(codeword ^ row2);
|
||||
|
||||
if(distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestCodeword = codeword;
|
||||
}
|
||||
}
|
||||
|
||||
if(bestDistance > MAXIMUM_ERRORS)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//Check for odd parity. Bits 0-15 should be opposite of bits 16-31.
|
||||
for(int x = 0; x < 16; x++)
|
||||
int second = oddParity ? bestCodeword ^ ROW_MASK : bestCodeword;
|
||||
CorrectedBinaryMessage corrected = new CorrectedBinaryMessage(ROW_LENGTH * 2);
|
||||
|
||||
for(int x = 0; x < ROW_LENGTH; x++)
|
||||
{
|
||||
if(deinterleaved.get(x) == deinterleaved.get(x + 16))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
corrected.set(x, (bestCodeword & (1 << x)) != 0);
|
||||
corrected.set(x + ROW_LENGTH, (second & (1 << x)) != 0);
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
corrected.setCorrectedBitCount(bestDistance);
|
||||
return corrected;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +178,8 @@ public class BPTC_16_2
|
||||
CorrectedBinaryMessage original = new CorrectedBinaryMessage(BinaryMessage.loadHex(msg));
|
||||
System.out.println("ORIGINAL: " + original.toHexString());
|
||||
CorrectedBinaryMessage decoded = decodeReverseChannel(original);
|
||||
System.out.println(" DECODED: " + (decoded != null ? decoded.toHexString() + " corrected bits:" +
|
||||
decoded.getCorrectedBitCount() : "uncorrectable"));
|
||||
System.out.println("-------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,8 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming13;
|
||||
import io.github.dsheirer.edac.Hamming15;
|
||||
import io.github.dsheirer.edac.IHamming;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
import io.github.dsheirer.edac.soft.SoftBlockCode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) 196/96 Support
|
||||
@@ -42,7 +40,6 @@ public class BPTC_196_96
|
||||
public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
|
||||
public static final int CHECKSUM_COLUMN_COUNT = 4;
|
||||
public static final int MESSAGE_START_INDEX = 4;
|
||||
public static final int MAXIMUM_RECURSION_TURBO_DEPTH = 20;
|
||||
private static final int ROW_COUNT = 13;
|
||||
private static final int ROW_LENGTH = 15;
|
||||
private static final int COLUMN_LENGTH = 13;
|
||||
@@ -59,6 +56,16 @@ public class BPTC_196_96
|
||||
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(COLUMN_LENGTH,
|
||||
word -> Hamming13.getSyndrome(toMessage(word, COLUMN_LENGTH), COLUMN_WORD_INDEXES) == 0);
|
||||
|
||||
//Maximum number of bit errors corrected by the hard decision minimum distance decoder. The product code minimum
|
||||
//distance of 9 guarantees correction of 4 bit errors. Miscorrections are rejected by the message CRC check, which
|
||||
//then retries with soft decision decoding. Error injection testing: corrects 99.7% of 8-bit error patterns and the
|
||||
//largest search for random error patterns took 3 ms. Messages with more bit errors fall back to turbo decoding.
|
||||
private static final int HARD_MAXIMUM_ERRORS = 12;
|
||||
private static final ProductCodeDecoder HARD_DECODER = new ProductCodeDecoder(
|
||||
ProductCodeDecoder.getParityChecks(ROW_LENGTH, message -> Hamming15.getSyndrome(message, 0)),
|
||||
ProductCodeDecoder.getParityChecks(COLUMN_LENGTH,
|
||||
message -> Hamming13.getSyndrome(message, COLUMN_WORD_INDEXES)), HARD_MAXIMUM_ERRORS);
|
||||
|
||||
/**
|
||||
* DMR De-interleaving indices
|
||||
*/
|
||||
@@ -151,8 +158,7 @@ public class BPTC_196_96
|
||||
CorrectedBinaryMessage message = deinterleave(original);
|
||||
|
||||
//Perform error detection and correction
|
||||
boolean pursueShadows = true;
|
||||
correct(message, pursueShadows);
|
||||
correct(message);
|
||||
|
||||
if(softBits != null && softBits.length == BPTC_LENGTH && message.getCorrectedBitCount() < 0)
|
||||
{
|
||||
@@ -414,80 +420,6 @@ public class BPTC_196_96
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the row level Hamming(15,11,3) protected words
|
||||
* @param message containing protected words in rows
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int index = -1;
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
//The first bit of the message is a pad/reserved bit and is ignored.
|
||||
offset = (row * 15) + 1;
|
||||
index = Hamming15.getErrorIndex(message, offset);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
rowErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Note: row errors should already be in order and don't need sorted
|
||||
return rowErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the column level Hamming(13,9,3) protected words
|
||||
* @param message containing protected words in columns
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> columnErrors = new ArrayList<>();
|
||||
|
||||
int index = -1;
|
||||
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
columnErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(columnErrors);
|
||||
return columnErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of the values that intersect between list 1 and list 2.
|
||||
* @param list1 to compare
|
||||
* @param list2 to compare
|
||||
* @return intersection
|
||||
*/
|
||||
private static List<Integer> intersection(List<Integer> list1, List<Integer> list2)
|
||||
{
|
||||
List<Integer> intersection = new ArrayList<>();
|
||||
|
||||
for(Integer i : list1)
|
||||
{
|
||||
if(list2.contains(i))
|
||||
{
|
||||
intersection.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
private static void logDiagnostic(CorrectedBinaryMessage message)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -555,321 +487,83 @@ public class BPTC_196_96
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the indices that make up the intersections of the row and column errors.
|
||||
* @param rows bitmap indicating rows with errors
|
||||
* @param columns bitmap indicating columns with errors
|
||||
* @return set of intersection indices.
|
||||
*/
|
||||
public static List<Integer> getIntersectionIndices(BinaryMessage columns, BinaryMessage rows)
|
||||
{
|
||||
List<Integer> intersections = new ArrayList<>();
|
||||
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
intersections.add(getIndex(column, row));
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs turbo decode of the message by testing multiple paths across rows and columns to identify paths
|
||||
* that resolve errors validated by the Hamming checks. Can also detect and often correct shadowing where the bit
|
||||
* errors occur in a 2x2, 2x3, 3x2 or 3x3 column/row grid and can't normally be corrected by the Hamming code.
|
||||
* When corrected, the message corrected bit count is set to the number of bits that differ from the received
|
||||
* message.
|
||||
* Performs hard decision error detection and correction of the de-interleaved message, using minimum distance
|
||||
* decoding, followed by turbo decoding when the minimum distance decoder can't correct the message. When
|
||||
* corrected, the corrected bit count is set to the number of corrected bits. Otherwise, the message is unchanged
|
||||
* and the corrected bit count is set to ERRORS_NOT_CORRECTED. Note: the leading pad bit is not checked.
|
||||
*
|
||||
* @param message to correct.
|
||||
* @param pursueShadows to test intersection flipping for double/triple shadows.
|
||||
* @return true if the message was corrected, false otherwise.
|
||||
*/
|
||||
public static boolean correct(CorrectedBinaryMessage message, boolean pursueShadows)
|
||||
public static boolean correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage received = new CorrectedBinaryMessage(message);
|
||||
boolean corrected = correctTurbo(message, pursueShadows);
|
||||
int[] rows = getRows(message);
|
||||
int corrected = HARD_DECODER.decode(rows);
|
||||
|
||||
if(corrected)
|
||||
if(corrected == ProductCodeDecoder.UNCORRECTABLE)
|
||||
{
|
||||
//The turbo decoder's incremental accounting can count a bit more than once, so measure the corrections
|
||||
received.xor(message);
|
||||
message.setCorrectedBitCount(received.cardinality());
|
||||
}
|
||||
//Fallback: real-world messages can have more bit errors than the minimum distance decoder maximum, spread
|
||||
//across the rows and columns, which the turbo decoder can often correct. Recording testing: recovered 29
|
||||
//of 30 CRC verified messages lost by the minimum distance decoder alone.
|
||||
CorrectedBinaryMessage turbo = new CorrectedBinaryMessage(message);
|
||||
|
||||
return corrected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turbo decode implementation for the correct() method. Note: the corrected bit count set by this method is
|
||||
* not accurate and is replaced by the correct() method.
|
||||
* @param message to correct.
|
||||
* @param pursueShadows to test intersection flipping for double/triple shadows.
|
||||
* @return true if the message was corrected, false otherwise.
|
||||
*/
|
||||
private static boolean correctTurbo(CorrectedBinaryMessage message, boolean pursueShadows)
|
||||
{
|
||||
List<Integer> columnErrors = getColumnErrors(message);
|
||||
List<Integer> rowErrors = getRowErrors(message);
|
||||
|
||||
if(columnErrors.isEmpty() && rowErrors.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Fix the easy bits first.
|
||||
List<Integer> flippedBits = new ArrayList<>();
|
||||
List<Integer> intersection = intersection(columnErrors, rowErrors);
|
||||
|
||||
if(!intersection.isEmpty())
|
||||
{
|
||||
for(Integer i : intersection)
|
||||
if(new BPTC_196_96TurboDecoder().correct(turbo))
|
||||
{
|
||||
message.flip(i);
|
||||
flippedBits.add(i);
|
||||
}
|
||||
rows = getRows(turbo);
|
||||
|
||||
message.incrementCorrectedBitCount(intersection.size());
|
||||
}
|
||||
|
||||
//Set of flags for columns with errors
|
||||
BinaryMessage columns = new BinaryMessage(15);
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
columns.set(column, !isColumnCorrect(column, message));
|
||||
}
|
||||
|
||||
//Set of flags for rows with errors
|
||||
BinaryMessage rows = new BinaryMessage(13);
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
rows.set(row, !isRowCorrect(row, message));
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
return true; //No (more) errors
|
||||
}
|
||||
|
||||
//1: fix up to 2-bit errors per column or row without pursuing multi-column or multi-row paths
|
||||
boolean pursue = false;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
pursue = true;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < 15; column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < 13; row = rows.nextSetBit(row + 1))
|
||||
if(HARD_DECODER.isValid(rows))
|
||||
{
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
flippedBits.addAll(solution);
|
||||
int[] received = getRows(message);
|
||||
corrected = 0;
|
||||
|
||||
for(int row = 0; row < ROW_COUNT; row++)
|
||||
{
|
||||
corrected += Integer.bitCount(rows[row] ^ received[row]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Detect double/triple row/column bit errors that are shadowing each other and preventing the
|
||||
// Hamming codes from giving accurate bit error locations. Flip all of their intersections and
|
||||
// then attempt to correct the message again. We use a flag 'pursueShadows' to ensure that we
|
||||
// only chase the shadow once, not recursively, since this parent method can be called recursively.
|
||||
if(pursueShadows && ((columns.cardinality() == 2 || columns.cardinality() == 3) &&
|
||||
(rows.cardinality() == 2 || rows.cardinality() == 3)))
|
||||
{
|
||||
List<Integer> intersectionIndices = getIntersectionIndices(columns, rows);
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
|
||||
//Set pursue shadows to false so we don't recursively chase shadows
|
||||
pursueShadows = false;
|
||||
|
||||
if(correctTurbo(message, pursueShadows))
|
||||
{
|
||||
columns.clear();
|
||||
rows.clear();
|
||||
int correctedBits = intersectionIndices.size() - message.getCorrectedBitCount();
|
||||
message.incrementCorrectedBitCount(correctedBits);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
Collections.sort(flippedBits);
|
||||
message.incrementCorrectedBitCount(flippedBits.size());
|
||||
}
|
||||
else
|
||||
if(corrected == ProductCodeDecoder.UNCORRECTABLE)
|
||||
{
|
||||
message.setCorrectedBitCount(BPTCBase.ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(corrected > 0)
|
||||
{
|
||||
for(int row = 0; row < ROW_COUNT; row++)
|
||||
{
|
||||
for(int column = 0; column < ROW_LENGTH; column++)
|
||||
{
|
||||
message.set(getIndex(column, row), (rows[row] & (1 << column)) != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.setCorrectedBitCount(corrected);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive method ...
|
||||
* @param column
|
||||
* @param message
|
||||
* @param columns
|
||||
* @param rows
|
||||
* @return
|
||||
* Row words of the de-interleaved message, where bit i of each row is the bit in column i.
|
||||
*/
|
||||
public static List<Integer> correctColumn(int column, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
private static int[] getRows(CorrectedBinaryMessage message)
|
||||
{
|
||||
//Don't let the recursive call exceed threshold
|
||||
if(depth > MAXIMUM_RECURSION_TURBO_DEPTH)
|
||||
int[] rows = new int[ROW_COUNT];
|
||||
|
||||
for(int row = 0; row < ROW_COUNT; row++)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = getColumnErrorIndex(column, message);
|
||||
int row = getRow(index);
|
||||
|
||||
//If the candidate index is in range and the row for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && rows.get(row) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isColumnCorrect(column, message))
|
||||
for(int column = 0; column < ROW_LENGTH; column++)
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the row is now correct, stop here.
|
||||
if(isRowCorrect(row, message))
|
||||
if(message.get(getIndex(column, row)))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
rows[row] |= 1 << column;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the row. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
//If our candidate index is part of the discovered solution, fail, otherwise return it as part of
|
||||
// the solution.
|
||||
if(solution.size() > 0)
|
||||
{
|
||||
columns.clear(column);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional column flips
|
||||
{
|
||||
for(int column2 = columns.nextSetBit(column + 1); column2 >= 0 && column2 < 15; column2 = columns.nextSetBit(column2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctColumn(column2, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(solution2.size() > 0)
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static List<Integer> correctRow(int row, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
{
|
||||
//Don't let the recursive call exceed threshold
|
||||
if(depth > MAXIMUM_RECURSION_TURBO_DEPTH)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = getRowErrorIndex(row, message);
|
||||
int column = getColumn(index);
|
||||
|
||||
//If the candidate index is in range and the column for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && columns.get(column) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the column is now correct, stop here and return the path as the solution
|
||||
if(isColumnCorrect(column, message))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the column. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
//If our candidate index is part of the discovered solution, fail, otherwise return it as part of
|
||||
// the solution.
|
||||
if(solution.size() > 0)
|
||||
{
|
||||
rows.clear(row);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional row flips
|
||||
{
|
||||
for(int row2 = rows.nextSetBit(row + 1); row2 >= 0 && row2 < 13; row2 = rows.nextSetBit(row2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctRow(row2, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(solution2.size() > 0 && !solution2.contains(index))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static int getIndex(int column, int row)
|
||||
@@ -921,16 +615,6 @@ public class BPTC_196_96
|
||||
return Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
}
|
||||
|
||||
public static int getColumn(int index)
|
||||
{
|
||||
return (index - 1) % 15;
|
||||
}
|
||||
|
||||
public static int getRow(int index)
|
||||
{
|
||||
return (index - 1) / 15;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String raw = "0000101111101000000100001010100001010000001000000011100000000000100000000000001000000000001000000000000010000000001111001010000100001000000001100000100001000111110101011111011111101001111011100010";
|
||||
@@ -940,8 +624,7 @@ public class BPTC_196_96
|
||||
|
||||
// correct2(refMessage);
|
||||
|
||||
boolean pursueShadows = true;
|
||||
correct(rawMessage, pursueShadows);
|
||||
correct(rawMessage);
|
||||
System.out.println("RAW: " + raw);
|
||||
System.out.println("COR: " + rawMessage);
|
||||
System.out.println("REF: " + ref);
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming13;
|
||||
import io.github.dsheirer.edac.Hamming15;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Turbo (iterative path search) hard decision decoder for the de-interleaved BPTC(196,96) message. Used as a fallback
|
||||
* for error patterns that the minimum distance decoder can't correct. Real-world messages can have many bit errors
|
||||
* spread across the rows and columns, which this decoder can often correct by following paths of row and column
|
||||
* Hamming error indices that resolve each other. Can also detect and often correct shadowing where the bit errors
|
||||
* occur in a 2x2, 2x3, 3x2 or 3x3 column/row grid.
|
||||
*
|
||||
* The search effort is bounded. The message is modified even when it can't be corrected and the decoder can report
|
||||
* success for an invalid codeword, so the caller must use a copy of the message and verify the result.
|
||||
*
|
||||
* Instances are not thread safe. Use a new instance for each message.
|
||||
*/
|
||||
class BPTC_196_96TurboDecoder
|
||||
{
|
||||
//Maximum recursive search calls per message. Testing with real-world (CRC verified) messages that exceeded the
|
||||
//minimum distance decoder error count: 29 of 30 needed fewer than 1,500 calls, whereas random noise needed a median
|
||||
//of 2,700 and up to 12 million calls. This limit bounds random noise decoding to under 1 ms.
|
||||
private static final int MAXIMUM_SEARCH_CALLS = 3000;
|
||||
private static final int MAXIMUM_RECURSION_DEPTH = 20;
|
||||
private static final int ROW_COUNT = 13;
|
||||
private static final int COLUMN_COUNT = 15;
|
||||
private int mSearchCalls;
|
||||
|
||||
/**
|
||||
* Performs turbo decoding of the de-interleaved message.
|
||||
* @param message to correct, modified even when uncorrectable.
|
||||
* @return true if the decoder reports the message as corrected.
|
||||
*/
|
||||
public boolean correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
return correct(message, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turbo decode implementation.
|
||||
* @param message to correct.
|
||||
* @param pursueShadows to test intersection flipping for double/triple shadows. Shadows are only pursued once.
|
||||
* @return true if the message was corrected, false otherwise.
|
||||
*/
|
||||
private boolean correct(CorrectedBinaryMessage message, boolean pursueShadows)
|
||||
{
|
||||
List<Integer> columnErrors = getColumnErrors(message);
|
||||
List<Integer> rowErrors = getRowErrors(message);
|
||||
|
||||
if(columnErrors.isEmpty() && rowErrors.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Fix the easy bits first, where the row and column error indices agree
|
||||
for(Integer index: intersection(columnErrors, rowErrors))
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
//Set of flags for columns with errors
|
||||
BinaryMessage columns = new BinaryMessage(COLUMN_COUNT);
|
||||
for(int column = 0; column < COLUMN_COUNT; column++)
|
||||
{
|
||||
columns.set(column, !isColumnCorrect(column, message));
|
||||
}
|
||||
|
||||
//Set of flags for rows with errors
|
||||
BinaryMessage rows = new BinaryMessage(ROW_COUNT);
|
||||
for(int row = 0; row < ROW_COUNT; row++)
|
||||
{
|
||||
rows.set(row, !isRowCorrect(row, message));
|
||||
}
|
||||
|
||||
if(columns.isEmpty() && rows.isEmpty())
|
||||
{
|
||||
return true; //No (more) errors
|
||||
}
|
||||
|
||||
//1: fix up to 2-bit errors per column or row without pursuing multi-column or multi-row paths
|
||||
boolean pursue = false;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < COLUMN_COUNT;
|
||||
column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < ROW_COUNT; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
//2: pursue multi-column and multi-row paths
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
pursue = true;
|
||||
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < COLUMN_COUNT;
|
||||
column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
correctColumn(column, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
}
|
||||
|
||||
if(!columns.isEmpty() || !rows.isEmpty())
|
||||
{
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < ROW_COUNT; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
correctRow(row, message, columns, rows, 0, pursue, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//3: Detect double/triple row/column bit errors that are shadowing each other and preventing the Hamming codes
|
||||
//from giving accurate bit error locations. Flip all of their intersections and then attempt to correct the
|
||||
//message again, once.
|
||||
if(pursueShadows && ((columns.cardinality() == 2 || columns.cardinality() == 3) &&
|
||||
(rows.cardinality() == 2 || rows.cardinality() == 3)))
|
||||
{
|
||||
List<Integer> intersectionIndices = getIntersectionIndices(columns, rows);
|
||||
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
|
||||
if(correct(message, false))
|
||||
{
|
||||
columns.clear();
|
||||
rows.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
for(Integer intersectionIndex: intersectionIndices)
|
||||
{
|
||||
message.flip(intersectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columns.isEmpty() && rows.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively tests flipping the bit indicated by the column error index, followed by the bit indicated by the
|
||||
* resulting row error index, and so on, until the path resolves the row and column errors.
|
||||
* @return solution path, or an empty list.
|
||||
*/
|
||||
private List<Integer> correctColumn(int column, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
{
|
||||
if(++mSearchCalls > MAXIMUM_SEARCH_CALLS || depth > MAXIMUM_RECURSION_DEPTH)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = Hamming13.getErrorIndex(message, BPTC_196_96.COLUMN_INDEXES[column]);
|
||||
int row = getRow(index);
|
||||
|
||||
//If the candidate index is in range and the row for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && rows.get(row) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isColumnCorrect(column, message))
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the row is now correct, stop here.
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the row. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctRow(row, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(!solution.isEmpty())
|
||||
{
|
||||
columns.clear(column);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional column flips
|
||||
{
|
||||
for(int column2 = columns.nextSetBit(column + 1); column2 >= 0 && column2 < COLUMN_COUNT;
|
||||
column2 = columns.nextSetBit(column2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctColumn(column2, message, columns, rows, depth + 1, pursue,
|
||||
path);
|
||||
|
||||
if(!solution2.isEmpty())
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively tests flipping the bit indicated by the row error index, followed by the bit indicated by the
|
||||
* resulting column error index, and so on, until the path resolves the row and column errors.
|
||||
* @return solution path, or an empty list.
|
||||
*/
|
||||
private List<Integer> correctRow(int row, CorrectedBinaryMessage message, BinaryMessage columns,
|
||||
BinaryMessage rows, int depth, boolean pursue, List<Integer> path)
|
||||
{
|
||||
if(++mSearchCalls > MAXIMUM_SEARCH_CALLS || depth > MAXIMUM_RECURSION_DEPTH)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
int index = Hamming15.getErrorIndex(message, (row * COLUMN_COUNT) + 1);
|
||||
int column = getColumn(index);
|
||||
|
||||
//If the candidate index is in range and the column for the candidate has an error, proceed
|
||||
if(0 <= index && index <= 195 && columns.get(column) && !path.contains(index))
|
||||
{
|
||||
message.flip(index);
|
||||
|
||||
if(isRowCorrect(row, message))
|
||||
{
|
||||
path.add(index);
|
||||
|
||||
//If the column is now correct, stop here and return the path as the solution
|
||||
if(isColumnCorrect(column, message))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return path;
|
||||
}
|
||||
|
||||
//Recursively test bit flipping in the column. A non-empty list is the solution set.
|
||||
List<Integer> solution = correctColumn(column, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(!solution.isEmpty())
|
||||
{
|
||||
rows.clear(row);
|
||||
return solution;
|
||||
}
|
||||
else if(pursue) //Pursue additional row flips
|
||||
{
|
||||
for(int row2 = rows.nextSetBit(row + 1); row2 >= 0 && row2 < ROW_COUNT;
|
||||
row2 = rows.nextSetBit(row2 + 1))
|
||||
{
|
||||
List<Integer> solution2 = correctRow(row2, message, columns, rows, depth + 1, pursue, path);
|
||||
|
||||
if(!solution2.isEmpty() && !solution2.contains(index))
|
||||
{
|
||||
columns.clear(column);
|
||||
rows.clear(row);
|
||||
return solution2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.removeLast();
|
||||
}
|
||||
|
||||
//Revert the change
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the row level Hamming(15,11,3) protected words
|
||||
*/
|
||||
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = new ArrayList<>();
|
||||
|
||||
for(int row = 0; row < ROW_COUNT; row++)
|
||||
{
|
||||
//The first bit of the message is a pad/reserved bit and is ignored.
|
||||
int index = Hamming15.getErrorIndex(message, (row * COLUMN_COUNT) + 1);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
rowErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
return rowErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the column level Hamming(13,9,3) protected words
|
||||
*/
|
||||
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> columnErrors = new ArrayList<>();
|
||||
|
||||
for(int column = 0; column < COLUMN_COUNT; column++)
|
||||
{
|
||||
int index = Hamming13.getErrorIndex(message, BPTC_196_96.COLUMN_INDEXES[column]);
|
||||
|
||||
if(index >= 0 && index < 1000)
|
||||
{
|
||||
columnErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(columnErrors);
|
||||
return columnErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Values from list 1 that are also in list 2.
|
||||
*/
|
||||
private static List<Integer> intersection(List<Integer> list1, List<Integer> list2)
|
||||
{
|
||||
List<Integer> intersection = new ArrayList<>();
|
||||
|
||||
for(Integer i: list1)
|
||||
{
|
||||
if(list2.contains(i))
|
||||
{
|
||||
intersection.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indices at the intersections of the row and column errors.
|
||||
*/
|
||||
private static List<Integer> getIntersectionIndices(BinaryMessage columns, BinaryMessage rows)
|
||||
{
|
||||
List<Integer> intersections = new ArrayList<>();
|
||||
|
||||
for(int row = rows.nextSetBit(0); row >= 0 && row < ROW_COUNT; row = rows.nextSetBit(row + 1))
|
||||
{
|
||||
for(int column = columns.nextSetBit(0); column >= 0 && column < COLUMN_COUNT;
|
||||
column = columns.nextSetBit(column + 1))
|
||||
{
|
||||
intersections.add(BPTC_196_96.getIndex(column, row));
|
||||
}
|
||||
}
|
||||
|
||||
return intersections;
|
||||
}
|
||||
|
||||
private static boolean isRowCorrect(int row, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming15.getSyndrome(message, (row * COLUMN_COUNT) + 1) == 0;
|
||||
}
|
||||
|
||||
private static boolean isColumnCorrect(int column, CorrectedBinaryMessage message)
|
||||
{
|
||||
return Hamming13.getSyndrome(message, BPTC_196_96.COLUMN_INDEXES[column]) == 0;
|
||||
}
|
||||
|
||||
private static int getColumn(int index)
|
||||
{
|
||||
return (index - 1) % COLUMN_COUNT;
|
||||
}
|
||||
|
||||
private static int getRow(int index)
|
||||
{
|
||||
return (index - 1) / COLUMN_COUNT;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming17;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
import io.github.dsheirer.edac.soft.SoftBlockCode;
|
||||
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
|
||||
|
||||
@@ -33,6 +34,15 @@ public class BPTC_68_36 extends BPTCBase
|
||||
private static final int LENGTH = 68;
|
||||
private static final int ROW_COUNT = 4;
|
||||
private static final int COLUMN_COUNT = 17;
|
||||
//Maximum number of bit errors corrected by the hard decision minimum distance decoder. The product code minimum
|
||||
//distance of 6 guarantees correction of 2 bit errors. Miscorrections are rejected by the CRC-8 check, which then
|
||||
//retries with soft decision decoding.
|
||||
private static final int HARD_MAXIMUM_ERRORS = 4;
|
||||
//Maximum number of bit errors corrected by the row erasure fallback decoder, when the minimum distance decoder
|
||||
//can't correct the message. Recording testing: recovered 19 of 22 CRC verified messages (at 6 dB SNR) lost by the
|
||||
//minimum distance decoder alone, which soft decision decoding didn't recover. Random noise acceptance before the
|
||||
//CRC-8 check: 1.4% versus 34% without a maximum.
|
||||
private static final int FALLBACK_MAXIMUM_ERRORS = 7;
|
||||
//Maximum correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) for a soft
|
||||
//decision result. No limit is applied since the CRC-8 check rejects most false decodes. Noise injection testing:
|
||||
//754 correct and 2 false soft decisions, where the false decisions had costs within the range of the correct
|
||||
@@ -63,7 +73,8 @@ public class BPTC_68_36 extends BPTCBase
|
||||
|
||||
public BPTC_68_36()
|
||||
{
|
||||
super(new Hamming17(), 17, 4);
|
||||
super(ProductCodeDecoder.getParityChecks(COLUMN_COUNT, message -> Hamming17.getSyndrome(message, 0)), ROW_COUNT,
|
||||
HARD_MAXIMUM_ERRORS, FALLBACK_MAXIMUM_ERRORS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
145
src/test/java/io/github/dsheirer/edac/HammingTest.java
Normal file
145
src/test/java/io/github/dsheirer/edac/HammingTest.java
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* JUnit tests for the DMR Hamming codes: every single bit error in every codeword is located.
|
||||
*/
|
||||
public class HammingTest
|
||||
{
|
||||
private static final int[] INDICES_13 = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
|
||||
|
||||
private static BinaryMessage toMessage(int word, int length)
|
||||
{
|
||||
BinaryMessage message = new BinaryMessage(length);
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
if((word & (1 << i)) != 0)
|
||||
{
|
||||
message.set(i);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the code has the expected number of codewords and that the locator finds each single bit error.
|
||||
*/
|
||||
private static void assertSingleErrorsLocated(int length, int informationLength, ToIntFunction<BinaryMessage> syndrome,
|
||||
ToIntFunction<BinaryMessage> locator)
|
||||
{
|
||||
int[] checks = ProductCodeDecoder.getParityChecks(length, syndrome);
|
||||
int[] codewords = IntStream.range(0, 1 << length)
|
||||
.filter(word -> ProductCodeDecoder.getSyndrome(word, checks) == 0).toArray();
|
||||
assertEquals(1 << informationLength, codewords.length);
|
||||
|
||||
for(int codeword: codewords)
|
||||
{
|
||||
assertEquals(IHamming.NO_ERRORS, locator.applyAsInt(toMessage(codeword, length)));
|
||||
|
||||
for(int i = 0; i < length; i++)
|
||||
{
|
||||
assertEquals(i, locator.applyAsInt(toMessage(codeword ^ (1 << i), length)),
|
||||
"Codeword " + Integer.toHexString(codeword) + " bit " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHamming13()
|
||||
{
|
||||
assertSingleErrorsLocated(13, 9, m -> Hamming13.getSyndrome(m, INDICES_13),
|
||||
m -> Hamming13.getErrorIndex(m, INDICES_13));
|
||||
assertSingleErrorsLocated(13, 9, m -> Hamming13.getSyndrome(m, INDICES_13), m -> Hamming13.getErrorIndex(m, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: the two syndromes that don't match a single bit error are flagged as multiple errors.
|
||||
*/
|
||||
@Test
|
||||
void testHamming13MultipleErrors()
|
||||
{
|
||||
int[] checks = ProductCodeDecoder.getParityChecks(13, m -> Hamming13.getSyndrome(m, INDICES_13));
|
||||
int flagged = 0;
|
||||
|
||||
for(int a = 0; a < 13; a++)
|
||||
{
|
||||
for(int b = a + 1; b < 13; b++)
|
||||
{
|
||||
int syndrome = checks[a] ^ checks[b];
|
||||
|
||||
if(syndrome == 9 || syndrome == 13)
|
||||
{
|
||||
BinaryMessage message = toMessage((1 << a) | (1 << b), 13);
|
||||
assertEquals(IHamming.MULTIPLE_ERRORS, Hamming13.getErrorIndex(message, 0));
|
||||
assertEquals(IHamming.MULTIPLE_ERRORS, Hamming13.getErrorIndex(message, INDICES_13));
|
||||
flagged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(12, flagged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHamming15()
|
||||
{
|
||||
assertSingleErrorsLocated(15, 11, m -> Hamming15.getSyndrome(m, 0), m -> Hamming15.getErrorIndex(m, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: single bit errors are located in every position, including the final parity bit, and every double bit
|
||||
* error is detected.
|
||||
*/
|
||||
@Test
|
||||
void testHamming16()
|
||||
{
|
||||
Hamming16 hamming = new Hamming16();
|
||||
assertSingleErrorsLocated(16, 11, m -> Hamming16.getSyndrome(m, 0), m -> hamming.getErrorIndex(m, 0));
|
||||
|
||||
for(int a = 0; a < 16; a++)
|
||||
{
|
||||
for(int b = a + 1; b < 16; b++)
|
||||
{
|
||||
assertEquals(IHamming.MULTIPLE_ERRORS, hamming.getErrorIndex(toMessage((1 << a) | (1 << b), 16), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: single bit errors are located in every position, including the final parity bit for both even and odd
|
||||
* weight codewords.
|
||||
*/
|
||||
@Test
|
||||
void testHamming17()
|
||||
{
|
||||
Hamming17 hamming = new Hamming17();
|
||||
assertSingleErrorsLocated(17, 12, m -> Hamming17.getSyndrome(m, 0), m -> hamming.getErrorIndex(m, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* JUnit tests for the product code minimum distance decoder. The DMR BPTC tests cover error correction capability.
|
||||
*/
|
||||
public class ProductCodeDecoderTest
|
||||
{
|
||||
//Hamming(7,4,3) rows
|
||||
private static final int[] HAMMING_7 = new int[]{0x3, 0x5, 0x6, 0x7, 0x4, 0x2, 0x1};
|
||||
|
||||
/**
|
||||
* Test: column codes whose final rows are not distinct single bit (systematic) parity checks are rejected.
|
||||
*/
|
||||
@Test
|
||||
void testColumnCodeValidation()
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> new ProductCodeDecoder(HAMMING_7, new int[]{1, 3}, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ProductCodeDecoder(HAMMING_7, new int[]{3, 1, 1}, 2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ProductCodeDecoder(HAMMING_7, new int[]{1}, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: Hamming(7,4,3) rows with even parity columns (minimum distance 6) corrects 2 bit errors and leaves the
|
||||
* rows unchanged when uncorrectable.
|
||||
*/
|
||||
@Test
|
||||
void testDecode()
|
||||
{
|
||||
ProductCodeDecoder decoder = new ProductCodeDecoder(HAMMING_7, new int[]{1, 1, 1}, 3);
|
||||
//Row codewords 0x61 (0x3 ^ 0x2 ^ 0x1 = 0) and 0x52 (0x5 ^ 0x4 ^ 0x1 = 0) with the column parity row 0x33
|
||||
int[] codeword = new int[]{0x61, 0x52, 0x33};
|
||||
assertTrue(decoder.isValid(codeword));
|
||||
assertEquals(0, decoder.decode(codeword.clone()));
|
||||
|
||||
//Two errors in the same column: no column parity errors
|
||||
int[] received = new int[]{0x61 ^ 0x01, 0x52 ^ 0x01, 0x33};
|
||||
assertFalse(decoder.isValid(received));
|
||||
assertEquals(2, decoder.decode(received));
|
||||
assertArrayEquals(codeword, received);
|
||||
|
||||
//Two errors in the same row, beyond the row code correction capability
|
||||
received = new int[]{0x61, 0x52, 0x33 ^ 0x30};
|
||||
assertEquals(2, decoder.decode(received));
|
||||
assertArrayEquals(codeword, received);
|
||||
|
||||
//Uncorrectable within the maximum error count: unchanged
|
||||
ProductCodeDecoder limited = new ProductCodeDecoder(HAMMING_7, new int[]{1, 1, 1}, 1);
|
||||
received = new int[]{0x61 ^ 0x01, 0x52 ^ 0x01, 0x33};
|
||||
int[] copy = received.clone();
|
||||
assertEquals(ProductCodeDecoder.UNCORRECTABLE, limited.decode(received));
|
||||
assertArrayEquals(copy, received);
|
||||
}
|
||||
}
|
||||
@@ -113,10 +113,10 @@ public class DMRMessageFramerTest
|
||||
boolean[] burst = createBurst(bptc, COLOR_CODE, DataType.SLOT_IDLE);
|
||||
float[] softSymbols = toSoftSymbols(burst);
|
||||
|
||||
//Add 10 low reliability payload symbol errors
|
||||
//Add 14 low reliability payload symbol errors, which usually exceed the hard decision decoder capability
|
||||
Set<Integer> errors = new HashSet<>();
|
||||
|
||||
while(errors.size() < 10)
|
||||
while(errors.size() < 14)
|
||||
{
|
||||
errors.add(payloadDibits.get(random.nextInt(payloadDibits.size())));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.bptc;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.Hamming13;
|
||||
import io.github.dsheirer.edac.Hamming15;
|
||||
import io.github.dsheirer.edac.Hamming16;
|
||||
import io.github.dsheirer.edac.Hamming17;
|
||||
import io.github.dsheirer.edac.ProductCodeDecoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.function.Predicate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Error injection tests for the DMR BPTC hard decision decoders: every error pattern up to the guaranteed correction
|
||||
* capability (half the product code minimum distance) is corrected. For the codes with an even minimum distance,
|
||||
* error patterns with one more bit error are corrected or detected (message unchanged), but never miscorrected. Also
|
||||
* tests the fallback decoders for error patterns that exceed the minimum distance decoder maximum.
|
||||
*/
|
||||
public class BPTCHardDecisionTest
|
||||
{
|
||||
private static final int[] H15 = ProductCodeDecoder.getParityChecks(15, m -> Hamming15.getSyndrome(m, 0));
|
||||
private static final int[] H13 = ProductCodeDecoder.getParityChecks(13,
|
||||
m -> Hamming13.getSyndrome(m, new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}));
|
||||
private static final int[] H16 = ProductCodeDecoder.getParityChecks(16, m -> Hamming16.getSyndrome(m, 0));
|
||||
private static final int[] H17 = ProductCodeDecoder.getParityChecks(17, m -> Hamming17.getSyndrome(m, 0));
|
||||
|
||||
/**
|
||||
* Valid codewords for the row code.
|
||||
*/
|
||||
private static int[] getCodewords(int[] checks)
|
||||
{
|
||||
return java.util.stream.IntStream.range(0, 1 << checks.length)
|
||||
.filter(word -> ProductCodeDecoder.getSyndrome(word, checks) == 0).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random product codeword as row words: random row codewords for the information rows and column parity
|
||||
* rows from the systematic column code.
|
||||
*/
|
||||
private static int[] createRows(Random random, int[] rowChecks, int[] columnChecks, int parityRows)
|
||||
{
|
||||
int[] codewords = getCodewords(rowChecks);
|
||||
int[] rows = new int[columnChecks.length];
|
||||
int informationRows = columnChecks.length - parityRows;
|
||||
|
||||
for(int row = 0; row < informationRows; row++)
|
||||
{
|
||||
rows[row] = codewords[random.nextInt(codewords.length)];
|
||||
}
|
||||
|
||||
for(int column = 0; column < rowChecks.length; column++)
|
||||
{
|
||||
int columnSyndrome = 0;
|
||||
|
||||
for(int row = 0; row < informationRows; row++)
|
||||
{
|
||||
if((rows[row] & (1 << column)) != 0)
|
||||
{
|
||||
columnSyndrome ^= columnChecks[row];
|
||||
}
|
||||
}
|
||||
|
||||
for(int row = informationRows; row < rows.length; row++)
|
||||
{
|
||||
if((columnSyndrome & columnChecks[row]) != 0)
|
||||
{
|
||||
rows[row] |= 1 << column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts row words to a message where row r column c is bit offset + (r * columns) + c.
|
||||
*/
|
||||
private static CorrectedBinaryMessage toMessage(int[] rows, int columns, int offset)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(offset + rows.length * columns);
|
||||
|
||||
for(int row = 0; row < rows.length; row++)
|
||||
{
|
||||
for(int column = 0; column < columns; column++)
|
||||
{
|
||||
if((rows[row] & (1 << column)) != 0)
|
||||
{
|
||||
message.set(offset + row * columns + column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private static int[] range(int from, int to)
|
||||
{
|
||||
return java.util.stream.IntStream.range(from, to).toArray();
|
||||
}
|
||||
|
||||
private static CorrectedBinaryMessage withErrors(CorrectedBinaryMessage codeword, int[] errors)
|
||||
{
|
||||
CorrectedBinaryMessage received = new CorrectedBinaryMessage(codeword);
|
||||
|
||||
for(int error: errors)
|
||||
{
|
||||
received.flip(error);
|
||||
}
|
||||
|
||||
return received;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the decoder outcome for a received message: corrected to the codeword with the corrected bit count set to
|
||||
* the error count, or (when allowed) uncorrectable with the message unchanged and the corrected bit count set to
|
||||
* ERRORS_NOT_CORRECTED.
|
||||
*/
|
||||
private static void check(Predicate<CorrectedBinaryMessage> decoder, CorrectedBinaryMessage codeword, int[] errors,
|
||||
boolean mustCorrect)
|
||||
{
|
||||
CorrectedBinaryMessage received = withErrors(codeword, errors);
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(received);
|
||||
|
||||
if(decoder.test(message))
|
||||
{
|
||||
if(!message.equals(codeword))
|
||||
{
|
||||
fail("Miscorrected errors at " + Arrays.toString(errors));
|
||||
}
|
||||
|
||||
assertEquals(errors.length, message.getCorrectedBitCount(), "Corrected bit count for " +
|
||||
Arrays.toString(errors));
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mustCorrect)
|
||||
{
|
||||
fail("Failed to correct errors at " + Arrays.toString(errors));
|
||||
}
|
||||
|
||||
assertEquals(received, message, "Uncorrectable message was changed");
|
||||
assertEquals(BPTCBase.ERRORS_NOT_CORRECTED, message.getCorrectedBitCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks every error pattern of the specified weight over the positions.
|
||||
*/
|
||||
private static void checkAll(Predicate<CorrectedBinaryMessage> decoder, CorrectedBinaryMessage codeword,
|
||||
int[] positions, int weight, boolean mustCorrect)
|
||||
{
|
||||
int[] pattern = new int[weight];
|
||||
int[] errors = new int[weight];
|
||||
|
||||
for(int i = 0; i < weight; i++)
|
||||
{
|
||||
pattern[i] = i;
|
||||
}
|
||||
|
||||
while(true)
|
||||
{
|
||||
for(int i = 0; i < weight; i++)
|
||||
{
|
||||
errors[i] = positions[pattern[i]];
|
||||
}
|
||||
|
||||
check(decoder, codeword, errors, mustCorrect);
|
||||
|
||||
//Advance to the next combination
|
||||
int i = weight - 1;
|
||||
|
||||
while(i >= 0 && pattern[i] == positions.length - weight + i)
|
||||
{
|
||||
i--;
|
||||
}
|
||||
|
||||
if(i < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pattern[i]++;
|
||||
|
||||
for(int j = i + 1; j < weight; j++)
|
||||
{
|
||||
pattern[j] = pattern[j - 1] + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks random error patterns of the specified weight over the positions.
|
||||
*/
|
||||
private static void checkRandom(Random random, Predicate<CorrectedBinaryMessage> decoder,
|
||||
CorrectedBinaryMessage codeword, int[] positions, int weight, int trials,
|
||||
boolean mustCorrect)
|
||||
{
|
||||
for(int trial = 0; trial < trials; trial++)
|
||||
{
|
||||
int[] errors = random.ints(0, positions.length).distinct().limit(weight).map(i -> positions[i]).toArray();
|
||||
check(decoder, codeword, errors, mustCorrect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(196,96) has a minimum distance of 9 and corrects every 1-4 bit error pattern. Exhaustive for 1-3 bit
|
||||
* errors, all 2x2 row/column squares and random patterns for 4 bit errors. Bit 0 is a pad bit.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_196_96()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
CorrectedBinaryMessage codeword = toMessage(createRows(random, H15, H13, 4), 15, 1);
|
||||
Predicate<CorrectedBinaryMessage> decoder = BPTC_196_96::correct;
|
||||
int[] positions = range(1, 196);
|
||||
|
||||
check(decoder, codeword, new int[0], true);
|
||||
|
||||
for(int weight = 1; weight <= 3; weight++)
|
||||
{
|
||||
checkAll(decoder, codeword, positions, weight, true);
|
||||
}
|
||||
|
||||
//Errors at the corners of every row/column square, which shadow each other for row and column decoding
|
||||
for(int row1 = 0; row1 < 13; row1++)
|
||||
{
|
||||
for(int row2 = row1 + 1; row2 < 13; row2++)
|
||||
{
|
||||
for(int column1 = 0; column1 < 15; column1++)
|
||||
{
|
||||
for(int column2 = column1 + 1; column2 < 15; column2++)
|
||||
{
|
||||
check(decoder, codeword, new int[]{BPTC_196_96.getIndex(column1, row1),
|
||||
BPTC_196_96.getIndex(column2, row1), BPTC_196_96.getIndex(column1, row2),
|
||||
BPTC_196_96.getIndex(column2, row2)}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRandom(random, decoder, codeword, positions, 4, 100000, true);
|
||||
|
||||
//Beyond the guaranteed correction capability, most patterns are corrected. Error injection testing: 99.7% of 8
|
||||
//bit error patterns are corrected.
|
||||
ProductCodeDecoder validator = new ProductCodeDecoder(H15, H13, 0);
|
||||
|
||||
for(int weight = 5; weight <= 8; weight++)
|
||||
{
|
||||
int corrected = 0;
|
||||
int trials = 2000;
|
||||
|
||||
for(int trial = 0; trial < trials; trial++)
|
||||
{
|
||||
CorrectedBinaryMessage other = toMessage(createRows(random, H15, H13, 4), 15, 1);
|
||||
int[] errors = random.ints(0, positions.length).distinct().limit(weight).map(i -> positions[i])
|
||||
.toArray();
|
||||
CorrectedBinaryMessage received = withErrors(other, errors);
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(received);
|
||||
|
||||
if(BPTC_196_96.correct(message))
|
||||
{
|
||||
assertTrue(validator.isValid(getRows196(message)), "Corrected message is not a valid codeword");
|
||||
|
||||
if(message.equals(other))
|
||||
{
|
||||
corrected++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assertEquals(received, message, "Uncorrectable message was changed");
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(corrected >= trials * 0.99, "Corrected " + corrected + "/" + trials + " " + weight +
|
||||
" bit error patterns");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Row words of the de-interleaved BPTC(196,96) message.
|
||||
*/
|
||||
private static int[] getRows196(CorrectedBinaryMessage message)
|
||||
{
|
||||
int[] rows = new int[13];
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
if(message.get(BPTC_196_96.getIndex(column, row)))
|
||||
{
|
||||
rows[row] |= 1 << column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(196,96) turbo decoding fallback corrects spread error patterns that exceed the minimum distance decoder
|
||||
* maximum: one bit error in each row, in distinct columns (13 bit errors).
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_196_96SpreadErrors()
|
||||
{
|
||||
Random random = new Random(6);
|
||||
|
||||
for(int trial = 0; trial < 200; trial++)
|
||||
{
|
||||
CorrectedBinaryMessage codeword = toMessage(createRows(random, H15, H13, 4), 15, 1);
|
||||
int[] columns = random.ints(0, 15).distinct().limit(13).toArray();
|
||||
int[] errors = new int[13];
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
errors[row] = BPTC_196_96.getIndex(columns[row], row);
|
||||
}
|
||||
|
||||
check(BPTC_196_96::correct, codeword, errors, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(196,96) random words are either uncorrectable (unchanged) or corrected to a valid codeword.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_196_96RandomWords()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
ProductCodeDecoder validator = new ProductCodeDecoder(H15, H13, 0);
|
||||
|
||||
for(int trial = 0; trial < 2000; trial++)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(196);
|
||||
|
||||
for(int i = 0; i < 196; i++)
|
||||
{
|
||||
message.set(i, random.nextBoolean());
|
||||
}
|
||||
|
||||
CorrectedBinaryMessage received = new CorrectedBinaryMessage(message);
|
||||
|
||||
if(BPTC_196_96.correct(message))
|
||||
{
|
||||
assertTrue(validator.isValid(getRows196(message)), "Corrected message is not a valid codeword");
|
||||
}
|
||||
else
|
||||
{
|
||||
assertEquals(received, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(128,77) has a minimum distance of 8, corrects every 1-3 bit error pattern and never miscorrects a 4 bit
|
||||
* error pattern.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_128_77()
|
||||
{
|
||||
Random random = new Random(3);
|
||||
int[] columnChecks = new int[8];
|
||||
Arrays.fill(columnChecks, 1);
|
||||
BPTC_128_77 bptc = new BPTC_128_77();
|
||||
Predicate<CorrectedBinaryMessage> decoder = bptc::correct;
|
||||
int[] positions = range(0, 128);
|
||||
|
||||
for(int codewordCount = 0; codewordCount < 2; codewordCount++)
|
||||
{
|
||||
CorrectedBinaryMessage codeword = toMessage(createRows(random, H16, columnChecks, 1), 16, 0);
|
||||
check(decoder, codeword, new int[0], true);
|
||||
|
||||
for(int weight = 1; weight <= 3; weight++)
|
||||
{
|
||||
checkAll(decoder, codeword, positions, weight, true);
|
||||
}
|
||||
|
||||
checkRandom(random, decoder, codeword, positions, 4, 100000, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(128,77) treats a received word that is equidistant from two codewords as uncorrectable.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_128_77Equidistant()
|
||||
{
|
||||
//A minimum weight (8) product codeword: a weight 4 row codeword repeated in two rows. Received with half of
|
||||
//its bits set, the word is 4 bits from both this codeword and the all-zero codeword.
|
||||
int rowCodeword = Arrays.stream(getCodewords(H16)).filter(word -> Integer.bitCount(word) == 4).findFirst()
|
||||
.orElseThrow();
|
||||
int[] rows = new int[8];
|
||||
rows[2] = rowCodeword & -rowCodeword;
|
||||
rows[2] |= Integer.lowestOneBit(rowCodeword ^ rows[2]);
|
||||
rows[5] = rowCodeword ^ rows[2];
|
||||
CorrectedBinaryMessage received = toMessage(rows, 16, 0);
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(received);
|
||||
assertFalse(new BPTC_128_77().correct(message));
|
||||
assertEquals(received, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(68,36) has a minimum distance of 6, corrects every 1-2 bit error pattern and never miscorrects a 3 bit
|
||||
* error pattern.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_68_36()
|
||||
{
|
||||
Random random = new Random(4);
|
||||
int[] columnChecks = new int[]{1, 1, 1, 1};
|
||||
BPTC_68_36 bptc = new BPTC_68_36();
|
||||
Predicate<CorrectedBinaryMessage> decoder = bptc::correct;
|
||||
int[] positions = range(0, 68);
|
||||
|
||||
//Multiple codewords, since the previous Hamming(17,12,3) decoder failed for some codewords and not others
|
||||
for(int codewordCount = 0; codewordCount < 8; codewordCount++)
|
||||
{
|
||||
CorrectedBinaryMessage codeword = toMessage(createRows(random, H17, columnChecks, 1), 17, 0);
|
||||
check(decoder, codeword, new int[0], true);
|
||||
|
||||
for(int weight = 1; weight <= 2; weight++)
|
||||
{
|
||||
checkAll(decoder, codeword, positions, weight, true);
|
||||
}
|
||||
|
||||
checkAll(decoder, codeword, positions, 3, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(68,36) row erasure fallback corrects most 5-7 bit error patterns in a single row, which exceed the
|
||||
* minimum distance decoder maximum, and never miscorrects them. The patterns where the row decodes to a codeword
|
||||
* that is equidistant with the transmitted codeword are uncorrectable.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_68_36RowErrors()
|
||||
{
|
||||
Random random = new Random(7);
|
||||
int[] columnChecks = new int[]{1, 1, 1, 1};
|
||||
BPTC_68_36 bptc = new BPTC_68_36();
|
||||
int trials = 3000;
|
||||
int corrected = 0;
|
||||
|
||||
for(int trial = 0; trial < trials; trial++)
|
||||
{
|
||||
CorrectedBinaryMessage codeword = toMessage(createRows(random, H17, columnChecks, 1), 17, 0);
|
||||
int row = random.nextInt(4);
|
||||
int[] errors = random.ints(0, 17).distinct().limit(5 + trial % 3).map(column -> row * 17 + column)
|
||||
.toArray();
|
||||
check(bptc::correct, codeword, errors, false);
|
||||
|
||||
CorrectedBinaryMessage message = withErrors(codeword, errors);
|
||||
|
||||
if(bptc.correct(message))
|
||||
{
|
||||
corrected++;
|
||||
}
|
||||
}
|
||||
|
||||
//Error injection testing: corrects 78%
|
||||
assertTrue(corrected >= trials * 0.7, "Corrected " + corrected + "/" + trials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interleaves a 32-bit single burst deinterleaved message, using the BPTC_16_2 deinterleave mapping.
|
||||
*/
|
||||
private static CorrectedBinaryMessage interleave16_2(CorrectedBinaryMessage deinterleaved)
|
||||
{
|
||||
CorrectedBinaryMessage interleaved = new CorrectedBinaryMessage(32);
|
||||
|
||||
for(int x = 0; x < 32; x++)
|
||||
{
|
||||
CorrectedBinaryMessage unit = new CorrectedBinaryMessage(32);
|
||||
unit.set(x);
|
||||
interleaved.set(x, deinterleaved.get(BPTC_16_2.deinterleave(unit).nextSetBit(0)));
|
||||
}
|
||||
|
||||
return interleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: BPTC(16,2) single burst (non-reverse and reverse channel) has a minimum distance of 8, corrects every 1-3
|
||||
* bit error pattern and never miscorrects a 4 bit error pattern.
|
||||
*/
|
||||
@Test
|
||||
void testBPTC_16_2()
|
||||
{
|
||||
Random random = new Random(5);
|
||||
int[] codewords = getCodewords(H16);
|
||||
|
||||
for(boolean reverseChannel: new boolean[]{false, true})
|
||||
{
|
||||
for(int codewordCount = 0; codewordCount < 4; codewordCount++)
|
||||
{
|
||||
int row = codewords[random.nextInt(codewords.length)];
|
||||
CorrectedBinaryMessage codeword = toMessage(new int[]{row, reverseChannel ? row ^ 0xFFFF : row}, 16, 0);
|
||||
Predicate<CorrectedBinaryMessage> decoder = message -> {
|
||||
CorrectedBinaryMessage interleaved = interleave16_2(message);
|
||||
CorrectedBinaryMessage decoded = reverseChannel ? BPTC_16_2.decodeReverseChannel(interleaved) :
|
||||
BPTC_16_2.decodeShortBurst(interleaved);
|
||||
|
||||
if(decoded == null)
|
||||
{
|
||||
message.setCorrectedBitCount(BPTCBase.ERRORS_NOT_CORRECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
message.clear();
|
||||
message.xor(decoded);
|
||||
message.setCorrectedBitCount(decoded.getCorrectedBitCount());
|
||||
return true;
|
||||
};
|
||||
|
||||
check(decoder, codeword, new int[0], true);
|
||||
|
||||
for(int weight = 1; weight <= 3; weight++)
|
||||
{
|
||||
checkAll(decoder, codeword, range(0, 32), weight, true);
|
||||
}
|
||||
|
||||
checkAll(decoder, codeword, range(0, 32), 4, false);
|
||||
}
|
||||
}
|
||||
|
||||
//Non-reverse channel decoder rejects a reverse channel message and vice versa
|
||||
int row = codewords[17];
|
||||
assertNull(BPTC_16_2.decodeShortBurst(interleave16_2(toMessage(new int[]{row, row ^ 0xFFFF}, 16, 0))));
|
||||
assertNotNull(BPTC_16_2.decodeReverseChannel(interleave16_2(toMessage(new int[]{row, row ^ 0xFFFF}, 16, 0))));
|
||||
assertNull(BPTC_16_2.decodeReverseChannel(interleave16_2(toMessage(new int[]{row, row}, 16, 0))));
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ public class BPTC_196_96SoftTest
|
||||
Random random = new Random(2);
|
||||
CorrectedBinaryMessage codeword = createCodeword(random);
|
||||
CorrectedBinaryMessage copy = new CorrectedBinaryMessage(codeword);
|
||||
assertTrue(BPTC_196_96.correct(copy, true));
|
||||
assertTrue(BPTC_196_96.correct(copy));
|
||||
assertEquals(codeword, copy);
|
||||
|
||||
float[] softBits = new float[196];
|
||||
@@ -185,7 +185,7 @@ public class BPTC_196_96SoftTest
|
||||
hard.flip(error);
|
||||
}
|
||||
|
||||
if(!BPTC_196_96.correct(hard, true) || !hard.equals(codeword))
|
||||
if(!BPTC_196_96.correct(hard) || !hard.equals(codeword))
|
||||
{
|
||||
hardFailures++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user