ltmarx / core /bch.ts
harelcain's picture
Upload 37 files
f2f99a3 verified
/**
* BCH (Bose-Chaudhuri-Hocquenghem) Error-Correcting Code
*
* Supports configurable BCH(n, k, t) codes over GF(2^m).
* Uses Berlekamp-Massey decoding with Chien search.
*/
import type { BchParams } from './types.js';
// Primitive polynomials for GF(2^m)
const PRIMITIVE_POLYS: Record<number, number> = {
6: 0x43, // x^6 + x + 1
7: 0x89, // x^7 + x^3 + 1
8: 0x11D, // x^8 + x^4 + x^3 + x^2 + 1
};
/**
* Galois Field GF(2^m) arithmetic
*/
export class GaloisField {
readonly m: number;
readonly size: number; // 2^m
readonly expTable: Int32Array; // alpha^i → element
readonly logTable: Int32Array; // element → i (log_alpha)
constructor(m: number) {
this.m = m;
this.size = 1 << m;
const poly = PRIMITIVE_POLYS[m];
if (!poly) throw new Error(`No primitive polynomial for GF(2^${m})`);
this.expTable = new Int32Array(this.size * 2);
this.logTable = new Int32Array(this.size);
this.logTable[0] = -1; // log(0) undefined
let val = 1;
for (let i = 0; i < this.size - 1; i++) {
this.expTable[i] = val;
this.logTable[val] = i;
val <<= 1;
if (val >= this.size) {
val ^= poly;
val &= this.size - 1;
}
}
// Extend exp table for easier modular arithmetic
for (let i = this.size - 1; i < this.size * 2; i++) {
this.expTable[i] = this.expTable[i - (this.size - 1)];
}
}
mul(a: number, b: number): number {
if (a === 0 || b === 0) return 0;
return this.expTable[this.logTable[a] + this.logTable[b]];
}
div(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero in GF');
if (a === 0) return 0;
let exp = this.logTable[a] - this.logTable[b];
if (exp < 0) exp += this.size - 1;
return this.expTable[exp];
}
pow(a: number, e: number): number {
if (a === 0) return e === 0 ? 1 : 0;
let log = (this.logTable[a] * e) % (this.size - 1);
if (log < 0) log += this.size - 1;
return this.expTable[log];
}
inv(a: number): number {
if (a === 0) throw new Error('Inverse of zero');
return this.expTable[this.size - 1 - this.logTable[a]];
}
}
/**
* Compute the generator polynomial for a binary BCH code.
* The generator is the LCM of minimal polynomials of alpha^1 through alpha^(2t).
* All minimal polynomials over GF(2) have binary (0/1) coefficients.
*/
function computeGeneratorPoly(gf: GaloisField, t: number): Uint8Array {
// Start with g(x) = 1 as a binary polynomial
let gen = new Uint8Array([1]);
const order = gf.size - 1;
const processed = new Set<number>();
for (let i = 1; i <= 2 * t; i++) {
// Normalize i modulo the field order
const norm = i % order;
if (processed.has(norm)) continue;
// Find conjugate class of alpha^i: {i, 2i, 4i, ...} mod order
const conjugates: number[] = [];
let r = norm;
for (let j = 0; j < gf.m; j++) {
const rNorm = r % order;
if (conjugates.includes(rNorm)) break;
conjugates.push(rNorm);
processed.add(rNorm);
r = (r * 2) % order;
}
// Compute minimal polynomial: product of (x + alpha^c) for c in conjugate class
// Start with [1] (= 1), then multiply by each (x + alpha^c)
// Coefficients are in GF(2^m), but we reduce to binary at the end
let minPoly = [1]; // GF(2^m) coefficients
for (const c of conjugates) {
const root = gf.expTable[c];
const newPoly = new Array(minPoly.length + 1).fill(0);
for (let k = 0; k < minPoly.length; k++) {
newPoly[k + 1] ^= minPoly[k]; // x * term
newPoly[k] ^= gf.mul(minPoly[k], root); // root * term
}
minPoly = newPoly;
}
// Convert to binary (all coefficients should be 0 or 1 for minimal polys over GF(2))
const binaryMinPoly = new Uint8Array(minPoly.length);
for (let j = 0; j < minPoly.length; j++) {
binaryMinPoly[j] = minPoly[j] === 0 ? 0 : 1;
}
// Multiply gen by binaryMinPoly (binary polynomial multiplication)
const newGen = new Uint8Array(gen.length + binaryMinPoly.length - 1);
for (let a = 0; a < gen.length; a++) {
if (!gen[a]) continue;
for (let b = 0; b < binaryMinPoly.length; b++) {
if (binaryMinPoly[b]) {
newGen[a + b] ^= 1;
}
}
}
gen = newGen;
}
return gen;
}
/**
* BCH Encoder/Decoder
*/
export class BchCodec {
readonly params: BchParams;
readonly gf: GaloisField;
private readonly genPoly: Uint8Array;
private readonly genPolyDeg: number;
constructor(params: BchParams) {
this.params = params;
this.gf = new GaloisField(params.m);
this.genPoly = computeGeneratorPoly(this.gf, params.t);
this.genPolyDeg = this.genPoly.length - 1;
}
/**
* Encode message bits (length k) into codeword bits (length n)
* Systematic encoding: message bits appear at the start
*/
encode(message: Uint8Array): Uint8Array {
const { n, k } = this.params;
if (message.length !== k) {
throw new Error(`Message length ${message.length} !== k=${k}`);
}
const nk = n - k;
const codeword = new Uint8Array(n);
codeword.set(message);
// Compute remainder: m(x) * x^(n-k) mod g(x) using binary polynomial long division
const dividend = new Uint8Array(n);
// Place message coefficients at positions nk..n-1 (high degree)
// In our polynomial representation, index i = coefficient of x^i
// For systematic encoding: codeword(x) = m(x) * x^(n-k) + remainder
for (let i = 0; i < k; i++) {
dividend[nk + i] = message[i];
}
// Long division
const rem = new Uint8Array(dividend);
for (let i = n - 1; i >= nk; i--) {
if (rem[i]) {
for (let j = 0; j <= this.genPolyDeg; j++) {
rem[i - this.genPolyDeg + j] ^= this.genPoly[j];
}
}
}
// Codeword = message (high positions) + remainder (low positions)
for (let i = 0; i < nk; i++) {
codeword[k + i] = message[i]; // Will be overwritten below
}
// Actually: codeword[i] for i=0..nk-1 is remainder, for i=nk..n-1 is message
// Let's use standard ordering: codeword = [message | parity]
// where c(x) = m(x) * x^(n-k) + (m(x) * x^(n-k) mod g(x))
for (let i = 0; i < k; i++) {
codeword[i] = message[i];
}
for (let i = 0; i < nk; i++) {
codeword[k + i] = rem[i];
}
return codeword;
}
/**
* Decode a received codeword. Returns corrected message bits or null if uncorrectable.
*/
decode(received: Uint8Array): Uint8Array | null {
const { n, k, t } = this.params;
if (received.length !== n) {
throw new Error(`Received length ${received.length} !== n=${n}`);
}
// Convert to polynomial form matching our encoding
// received[0..k-1] = message bits, received[k..n-1] = parity bits
// As polynomial: r(x) = sum r[i] * x^(n-k+i) for i=0..k-1, + sum r[k+i] * x^i for i=0..n-k-1
const nk = n - k;
const rPoly = new Uint8Array(n);
for (let i = 0; i < k; i++) {
rPoly[nk + i] = received[i];
}
for (let i = 0; i < nk; i++) {
rPoly[i] = received[k + i];
}
// Compute syndromes S_1 ... S_{2t}
// S_j = r(alpha^j) for j = 1..2t
const syndromes = new Array<number>(2 * t);
let hasErrors = false;
for (let j = 0; j < 2 * t; j++) {
let s = 0;
let alphaPow = 1; // alpha^((j+1)*0)
const alphaJ = this.gf.expTable[j + 1]; // alpha^(j+1)
for (let i = 0; i < n; i++) {
if (rPoly[i]) {
s ^= alphaPow;
}
alphaPow = this.gf.mul(alphaPow, alphaJ);
}
syndromes[j] = s;
if (s !== 0) hasErrors = true;
}
if (!hasErrors) {
return new Uint8Array(received.subarray(0, k));
}
// Berlekamp-Massey to find error locator polynomial
const sigma = this.berlekampMassey(syndromes, t);
if (!sigma) return null;
// Chien search to find error positions in polynomial representation
const errorPolyPositions = this.chienSearch(sigma, n);
if (!errorPolyPositions || errorPolyPositions.length !== sigma.length - 1) {
return null;
}
// Correct errors in polynomial representation
const correctedPoly = new Uint8Array(rPoly);
for (const pos of errorPolyPositions) {
if (pos >= n) return null;
correctedPoly[pos] ^= 1;
}
// Verify: recompute syndromes should all be zero
for (let j = 0; j < 2 * t; j++) {
let s = 0;
let alphaPow = 1;
const alphaJ = this.gf.expTable[j + 1];
for (let i = 0; i < n; i++) {
if (correctedPoly[i]) s ^= alphaPow;
alphaPow = this.gf.mul(alphaPow, alphaJ);
}
if (s !== 0) return null;
}
// Extract message bits from corrected polynomial
const message = new Uint8Array(k);
for (let i = 0; i < k; i++) {
message[i] = correctedPoly[nk + i];
}
return message;
}
/**
* Berlekamp-Massey algorithm
*/
private berlekampMassey(syndromes: number[], t: number): number[] | null {
const gf = this.gf;
const twoT = 2 * t;
let C = [1];
let B = [1];
let L = 0;
let m = 1;
let b = 1;
for (let n = 0; n < twoT; n++) {
let d = syndromes[n];
for (let i = 1; i <= L; i++) {
if (i < C.length && (n - i) >= 0) {
d ^= gf.mul(C[i], syndromes[n - i]);
}
}
if (d === 0) {
m++;
} else if (2 * L <= n) {
const T = [...C];
const factor = gf.div(d, b);
while (C.length < B.length + m) C.push(0);
for (let i = 0; i < B.length; i++) {
C[i + m] ^= gf.mul(factor, B[i]);
}
L = n + 1 - L;
B = T;
b = d;
m = 1;
} else {
const factor = gf.div(d, b);
while (C.length < B.length + m) C.push(0);
for (let i = 0; i < B.length; i++) {
C[i + m] ^= gf.mul(factor, B[i]);
}
m++;
}
}
if (L > t) return null;
return C;
}
/**
* Chien search: find roots of the error locator polynomial.
*
* sigma(x) = prod(1 - alpha^(e_j) * x), so roots are at x = alpha^(-e_j).
* We test x = alpha^(-i) for i = 0..n-1. If sigma(alpha^(-i)) = 0,
* then error position is i (in polynomial index).
*/
private chienSearch(sigma: number[], n: number): number[] | null {
const gf = this.gf;
const positions: number[] = [];
const degree = sigma.length - 1;
const order = gf.size - 1;
for (let i = 0; i < n; i++) {
// Evaluate sigma at alpha^(-i)
let val = sigma[0]; // = 1
for (let j = 1; j < sigma.length; j++) {
// alpha^(-i*j) = alpha^(order - i*j mod order)
let exp = (order - ((i * j) % order)) % order;
val ^= gf.mul(sigma[j], gf.expTable[exp]);
}
if (val === 0) {
positions.push(i);
}
}
if (positions.length !== degree) return null;
return positions;
}
/**
* Soft-decode: use soft input values to attempt decoding
*/
decodeSoft(softBits: Float64Array): { message: Uint8Array | null; reliable: boolean } {
const hard = new Uint8Array(softBits.length);
for (let i = 0; i < softBits.length; i++) {
hard[i] = softBits[i] > 0 ? 1 : 0;
}
const decoded = this.decode(hard);
if (decoded) {
let reliabilitySum = 0;
for (let i = 0; i < softBits.length; i++) {
reliabilitySum += Math.abs(softBits[i]);
}
const avgReliability = reliabilitySum / softBits.length;
return { message: decoded, reliable: avgReliability > 0.15 };
}
return { message: null, reliable: false };
}
}
/**
* Create BCH codec from standard parameters
*/
export function createBchCodec(params: BchParams): BchCodec {
return new BchCodec(params);
}