File size: 11,769 Bytes
f2f99a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
/**
 * 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);
}