@e04/ft8ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ import { gHex, graymap, icos7 } from "../util/constants.js";
2
+ import { pack77 } from "../util/pack_jt77.js";
3
+ import { generateFT8Waveform, type WaveformOptions } from "../util/waveform.js";
4
+
5
+ function generateLdpcGMatrix(): number[][] {
6
+ const K = 91;
7
+ const M = 83; // 174 - 91
8
+ const gen: number[][] = Array.from({ length: M }, () => new Array(K).fill(0));
9
+
10
+ for (let i = 0; i < M; i++) {
11
+ const hexStr = gHex[i]!;
12
+ for (let j = 0; j < 23; j++) {
13
+ const val = parseInt(hexStr[j]!, 16);
14
+ const limit = j === 22 ? 3 : 4;
15
+ for (let jj = 1; jj <= limit; jj++) {
16
+ const col = j * 4 + jj - 1; // 0-indexed
17
+ if ((val & (1 << (4 - jj))) !== 0) {
18
+ gen[i]![col] = 1;
19
+ }
20
+ }
21
+ }
22
+ }
23
+ return gen;
24
+ }
25
+
26
+ const G = generateLdpcGMatrix();
27
+
28
+ export function encode174_91(msg77: number[]): number[] {
29
+ const poly = 0x2757;
30
+ let crc = 0;
31
+ // padded with 19 zeros (3 zeros + 16 zero-bits for flush)
32
+ const bitArray = [...msg77, 0, 0, 0, ...new Array(16).fill(0)];
33
+ for (let bit = 0; bit < 96; bit++) {
34
+ const nextBit = bitArray[bit]!;
35
+ if ((crc & 0x2000) !== 0) {
36
+ crc = ((crc << 1) | nextBit) ^ poly;
37
+ } else {
38
+ crc = (crc << 1) | nextBit;
39
+ }
40
+ crc &= 0x3fff;
41
+ }
42
+
43
+ const msg91 = [...msg77];
44
+ for (let i = 0; i < 14; i++) {
45
+ msg91.push((crc >> (13 - i)) & 1);
46
+ }
47
+
48
+ const codeword = [...msg91];
49
+ for (let i = 0; i < 83; i++) {
50
+ let sum = 0;
51
+ for (let j = 0; j < 91; j++) {
52
+ sum += msg91[j]! * G[i]![j]!;
53
+ }
54
+ codeword.push(sum % 2);
55
+ }
56
+ return codeword;
57
+ }
58
+
59
+ export function getTones(codeword: number[]): number[] {
60
+ const tones = new Array(79).fill(0);
61
+
62
+ for (let i = 0; i < 7; i++) tones[i] = icos7[i]!;
63
+ for (let i = 0; i < 7; i++) tones[36 + i] = icos7[i]!;
64
+ for (let i = 0; i < 7; i++) tones[72 + i] = icos7[i]!;
65
+
66
+ let k = 7;
67
+ for (let j = 1; j <= 58; j++) {
68
+ const i = j * 3 - 3; // codeword is 0-indexed in JS, but the loop was j=1 to 58
69
+ if (j === 30) k += 7;
70
+ const indx = codeword[i]! * 4 + codeword[i + 1]! * 2 + codeword[i + 2]!;
71
+ tones[k] = graymap[indx]!;
72
+ k++;
73
+ }
74
+ return tones;
75
+ }
76
+
77
+ export function encodeMessage(msg: string): number[] {
78
+ const bits77 = pack77(msg);
79
+ const codeword = encode174_91(bits77);
80
+ return getTones(codeword);
81
+ }
82
+
83
+ export function encode(msg: string, options: WaveformOptions = {}): Float32Array {
84
+ return generateFT8Waveform(encodeMessage(msg), options);
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { type DecodedMessage, type DecodeOptions, decode as decodeFT8 } from "./ft8/decode.js";
2
+ export { encode as encodeFT8 } from "./ft8/encode.js";
@@ -0,0 +1,118 @@
1
+ export const SAMPLE_RATE = 12_000;
2
+ export const NSPS = 1920;
3
+ export const NFFT1 = 2 * NSPS; // 3840
4
+ export const NH1 = NFFT1 / 2; // 1920
5
+ export const NSTEP = NSPS / 4; // 480
6
+ export const NMAX = 15 * SAMPLE_RATE; // 180000
7
+ export const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
8
+ export const NDOWN = 60;
9
+ export const NN = 79;
10
+ export const NS = 21;
11
+ export const ND = 58;
12
+ export const KK = 91;
13
+ export const N_LDPC = 174;
14
+ export const M_LDPC = N_LDPC - KK; // 83
15
+
16
+ export const icos7 = [3, 1, 4, 0, 6, 5, 2] as const;
17
+ export const graymap = [0, 1, 3, 2, 5, 6, 4, 7] as const;
18
+
19
+ export const gHex = [
20
+ "8329ce11bf31eaf509f27fc",
21
+ "761c264e25c259335493132",
22
+ "dc265902fb277c6410a1bdc",
23
+ "1b3f417858cd2dd33ec7f62",
24
+ "09fda4fee04195fd034783a",
25
+ "077cccc11b8873ed5c3d48a",
26
+ "29b62afe3ca036f4fe1a9da",
27
+ "6054faf5f35d96d3b0c8c3e",
28
+ "e20798e4310eed27884ae90",
29
+ "775c9c08e80e26ddae56318",
30
+ "b0b811028c2bf997213487c",
31
+ "18a0c9231fc60adf5c5ea32",
32
+ "76471e8302a0721e01b12b8",
33
+ "ffbccb80ca8341fafb47b2e",
34
+ "66a72a158f9325a2bf67170",
35
+ "c4243689fe85b1c51363a18",
36
+ "0dff739414d1a1b34b1c270",
37
+ "15b48830636c8b99894972e",
38
+ "29a89c0d3de81d665489b0e",
39
+ "4f126f37fa51cbe61bd6b94",
40
+ "99c47239d0d97d3c84e0940",
41
+ "1919b75119765621bb4f1e8",
42
+ "09db12d731faee0b86df6b8",
43
+ "488fc33df43fbdeea4eafb4",
44
+ "827423ee40b675f756eb5fe",
45
+ "abe197c484cb74757144a9a",
46
+ "2b500e4bc0ec5a6d2bdbdd0",
47
+ "c474aa53d70218761669360",
48
+ "8eba1a13db3390bd6718cec",
49
+ "753844673a27782cc42012e",
50
+ "06ff83a145c37035a5c1268",
51
+ "3b37417858cc2dd33ec3f62",
52
+ "9a4a5a28ee17ca9c324842c",
53
+ "bc29f465309c977e89610a4",
54
+ "2663ae6ddf8b5ce2bb29488",
55
+ "46f231efe457034c1814418",
56
+ "3fb2ce85abe9b0c72e06fbe",
57
+ "de87481f282c153971a0a2e",
58
+ "fcd7ccf23c69fa99bba1412",
59
+ "f0261447e9490ca8e474cec",
60
+ "4410115818196f95cdd7012",
61
+ "088fc31df4bfbde2a4eafb4",
62
+ "b8fef1b6307729fb0a078c0",
63
+ "5afea7acccb77bbc9d99a90",
64
+ "49a7016ac653f65ecdc9076",
65
+ "1944d085be4e7da8d6cc7d0",
66
+ "251f62adc4032f0ee714002",
67
+ "56471f8702a0721e00b12b8",
68
+ "2b8e4923f2dd51e2d537fa0",
69
+ "6b550a40a66f4755de95c26",
70
+ "a18ad28d4e27fe92a4f6c84",
71
+ "10c2e586388cb82a3d80758",
72
+ "ef34a41817ee02133db2eb0",
73
+ "7e9c0c54325a9c15836e000",
74
+ "3693e572d1fde4cdf079e86",
75
+ "bfb2cec5abe1b0c72e07fbe",
76
+ "7ee18230c583cccc57d4b08",
77
+ "a066cb2fedafc9f52664126",
78
+ "bb23725abc47cc5f4cc4cd2",
79
+ "ded9dba3bee40c59b5609b4",
80
+ "d9a7016ac653e6decdc9036",
81
+ "9ad46aed5f707f280ab5fc4",
82
+ "e5921c77822587316d7d3c2",
83
+ "4f14da8242a8b86dca73352",
84
+ "8b8b507ad467d4441df770e",
85
+ "22831c9cf1169467ad04b68",
86
+ "213b838fe2ae54c38ee7180",
87
+ "5d926b6dd71f085181a4e12",
88
+ "66ab79d4b29ee6e69509e56",
89
+ "958148682d748a38dd68baa",
90
+ "b8ce020cf069c32a723ab14",
91
+ "f4331d6d461607e95752746",
92
+ "6da23ba424b9596133cf9c8",
93
+ "a636bcbc7b30c5fbeae67fe",
94
+ "5cb0d86a07df654a9089a20",
95
+ "f11f106848780fc9ecdd80a",
96
+ "1fbb5364fb8d2c9d730d5ba",
97
+ "fcb86bc70a50c9d02a5d034",
98
+ "a534433029eac15f322e34c",
99
+ "c989d9c7c3d3b8c55d75130",
100
+ "7bb38b2f0186d46643ae962",
101
+ "2644ebadeb44b9467d1f42c",
102
+ "608cc857594bfbb55d69600",
103
+ ];
104
+
105
+ export const CRC_POLY = 0x2757;
106
+
107
+ export const FTALPH = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
108
+
109
+ export const A1 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
110
+ export const A2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
111
+ export const A3 = "0123456789";
112
+ export const A4 = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
113
+ export const C38 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
114
+
115
+ export const NTOKENS = 2063592;
116
+ export const MAX22 = 4194304; // 2^22
117
+ export const MAX28 = 268435456; // 2^28
118
+ export const MAXGRID4 = 32400;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * CRC-14 computation and checking, shared between encoder and decoder.
3
+ * Polynomial: 0x2757 (x^14 + x^13 + x^10 + x^9 + x^8 + x^6 + x^4 + x^2 + x + 1)
4
+ */
5
+
6
+ export function computeCRC14(msg77: number[]): number {
7
+ const poly = 0x2757;
8
+ let crc = 0;
9
+ const bitArray = [...msg77, 0, 0, 0, ...(new Array(16).fill(0) as number[])];
10
+ for (let bit = 0; bit < 96; bit++) {
11
+ const nextBit = bitArray[bit]!;
12
+ if ((crc & 0x2000) !== 0) {
13
+ crc = ((crc << 1) | nextBit) ^ poly;
14
+ } else {
15
+ crc = (crc << 1) | nextBit;
16
+ }
17
+ crc &= 0x3fff;
18
+ }
19
+ return crc;
20
+ }
21
+
22
+ /**
23
+ * Check CRC-14 of a 91-bit decoded message (77 message + 14 CRC).
24
+ * Returns true if CRC is valid.
25
+ */
26
+ export function checkCRC14(bits91: number[]): boolean {
27
+ const msg77 = bits91.slice(0, 77);
28
+ const receivedCRC = bitsToInt(bits91, 77, 14);
29
+ const computedCRC = computeCRC14(msg77);
30
+ return receivedCRC === computedCRC;
31
+ }
32
+
33
+ function bitsToInt(bits: number[], offset: number, count: number): number {
34
+ let val = 0;
35
+ for (let i = 0; i < count; i++) {
36
+ val = (val << 1) | (bits[offset + i] ?? 0);
37
+ }
38
+ return val;
39
+ }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * LDPC (174,91) Belief Propagation decoder for FT8.
3
+ * Port of bpdecode174_91.f90 and decode174_91.f90.
4
+ */
5
+
6
+ import { gHex, KK, M_LDPC, N_LDPC } from "./constants.js";
7
+ import { checkCRC14 } from "./crc.js";
8
+ import { Mn, Nm, ncw, nrw } from "./ldpc_tables.js";
9
+
10
+ export interface DecodeResult {
11
+ message91: number[];
12
+ cw: number[];
13
+ nharderrors: number;
14
+ dmin: number;
15
+ ntype: number;
16
+ }
17
+
18
+ function platanh(x: number): number {
19
+ if (x > 0.9999999) return 18.71;
20
+ if (x < -0.9999999) return -18.71;
21
+ return 0.5 * Math.log((1 + x) / (1 - x));
22
+ }
23
+
24
+ /**
25
+ * BP decoder for (174,91) LDPC code.
26
+ * llr: log-likelihood ratios (174 values, positive = bit more likely 0)
27
+ * apmask: AP mask (174 values, 1 = a priori bit, don't update from check messages)
28
+ * maxIterations: max BP iterations
29
+ * Returns null if decoding fails, otherwise { message91, cw, nharderrors }
30
+ */
31
+ export function bpDecode174_91(
32
+ llr: Float64Array,
33
+ apmask: Int8Array,
34
+ maxIterations: number,
35
+ ): DecodeResult | null {
36
+ const N = N_LDPC;
37
+ const M = M_LDPC;
38
+
39
+ const tov = new Float64Array(ncw * N);
40
+ const toc = new Float64Array(7 * M);
41
+ const tanhtoc = new Float64Array(7 * M);
42
+ const zn = new Float64Array(N);
43
+ const cw = new Int8Array(N);
44
+
45
+ // Initialize messages to checks
46
+ for (let j = 0; j < M; j++) {
47
+ const w = nrw[j]!;
48
+ for (let i = 0; i < w; i++) {
49
+ toc[i * M + j] = llr[Nm[j]![i]!]!;
50
+ }
51
+ }
52
+
53
+ let nclast = 0;
54
+ let ncnt = 0;
55
+
56
+ for (let iter = 0; iter <= maxIterations; iter++) {
57
+ // Update bit LLRs
58
+ for (let i = 0; i < N; i++) {
59
+ if (apmask[i] !== 1) {
60
+ let sum = 0;
61
+ for (let k = 0; k < ncw; k++) sum += tov[k * N + i]!;
62
+ zn[i] = llr[i]! + sum;
63
+ } else {
64
+ zn[i] = llr[i]!;
65
+ }
66
+ }
67
+
68
+ // Hard decision
69
+ for (let i = 0; i < N; i++) cw[i] = zn[i]! > 0 ? 1 : 0;
70
+
71
+ // Check parity
72
+ let ncheck = 0;
73
+ for (let i = 0; i < M; i++) {
74
+ const w = nrw[i]!;
75
+ let s = 0;
76
+ for (let k = 0; k < w; k++) s += cw[Nm[i]![k]!]!;
77
+ if (s % 2 !== 0) ncheck++;
78
+ }
79
+
80
+ if (ncheck === 0) {
81
+ const bits91 = Array.from(cw.slice(0, KK));
82
+ if (checkCRC14(bits91)) {
83
+ let nharderrors = 0;
84
+ for (let i = 0; i < N; i++) {
85
+ if ((2 * cw[i]! - 1) * llr[i]! < 0) nharderrors++;
86
+ }
87
+ return {
88
+ message91: bits91,
89
+ cw: Array.from(cw),
90
+ nharderrors,
91
+ dmin: 0,
92
+ ntype: 1,
93
+ };
94
+ }
95
+ }
96
+
97
+ // Early stopping
98
+ if (iter > 0) {
99
+ const nd = ncheck - nclast;
100
+ if (nd < 0) {
101
+ ncnt = 0;
102
+ } else {
103
+ ncnt++;
104
+ }
105
+ if (ncnt >= 5 && iter >= 10 && ncheck > 15) return null;
106
+ }
107
+ nclast = ncheck;
108
+
109
+ // Send messages from bits to check nodes
110
+ for (let j = 0; j < M; j++) {
111
+ const w = nrw[j]!;
112
+ for (let i = 0; i < w; i++) {
113
+ const ibj = Nm[j]![i]!;
114
+ let val = zn[ibj]!;
115
+ for (let kk = 0; kk < ncw; kk++) {
116
+ if (Mn[ibj]![kk] === j) {
117
+ val -= tov[kk * N + ibj]!;
118
+ }
119
+ }
120
+ toc[i * M + j] = val;
121
+ }
122
+ }
123
+
124
+ // Send messages from check nodes to variable nodes
125
+ for (let i = 0; i < M; i++) {
126
+ for (let k = 0; k < 7; k++) {
127
+ tanhtoc[k * M + i] = Math.tanh(-toc[k * M + i]! / 2);
128
+ }
129
+ }
130
+
131
+ for (let j = 0; j < N; j++) {
132
+ for (let i = 0; i < ncw; i++) {
133
+ const ichk = Mn[j]![i]!;
134
+ const w = nrw[ichk]!;
135
+ let Tmn = 1.0;
136
+ for (let k = 0; k < w; k++) {
137
+ if (Nm[ichk]![k] !== j) {
138
+ Tmn *= tanhtoc[k * M + ichk]!;
139
+ }
140
+ }
141
+ tov[i * N + j] = 2 * platanh(-Tmn);
142
+ }
143
+ }
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Hybrid BP + OSD-like decoder for (174,91) code.
151
+ * Tries BP first, then falls back to OSD approach for deeper decoding.
152
+ */
153
+ export function decode174_91(
154
+ llr: Float64Array,
155
+ apmask: Int8Array,
156
+ maxosd: number,
157
+ ): DecodeResult | null {
158
+ const maxIterations = 30;
159
+
160
+ // Try BP decoding
161
+ const bpResult = bpDecode174_91(llr, apmask, maxIterations);
162
+ if (bpResult) return bpResult;
163
+
164
+ // OSD-0 fallback: try hard-decision with bit flipping for most unreliable bits
165
+ if (maxosd >= 0) {
166
+ return osdDecode174_91(llr, apmask, maxosd >= 1 ? 2 : 1);
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Simplified OSD decoder for (174,91) code.
174
+ * Uses ordered statistics approach: sort bits by reliability,
175
+ * do Gaussian elimination, try flipping least reliable info bits.
176
+ */
177
+ function osdDecode174_91(
178
+ llr: Float64Array,
179
+ apmask: Int8Array,
180
+ norder: number,
181
+ ): DecodeResult | null {
182
+ const N = N_LDPC;
183
+ const K = KK;
184
+
185
+ const gen = getGenerator();
186
+
187
+ // Sort by reliability (descending)
188
+ const indices = Array.from({ length: N }, (_, i) => i);
189
+ indices.sort((a, b) => Math.abs(llr[b]!) - Math.abs(llr[a]!));
190
+
191
+ // Reorder generator matrix columns
192
+ const genmrb = new Uint8Array(K * N);
193
+ for (let i = 0; i < N; i++) {
194
+ for (let k = 0; k < K; k++) {
195
+ genmrb[k * N + i] = gen[k * N + indices[i]!]!;
196
+ }
197
+ }
198
+
199
+ // Gaussian elimination to get systematic form on the K most-reliable bits
200
+ for (let id = 0; id < K; id++) {
201
+ let found = false;
202
+ for (let icol = id; icol < Math.min(K + 20, N); icol++) {
203
+ if (genmrb[id * N + icol] === 1) {
204
+ if (icol !== id) {
205
+ // Swap columns
206
+ for (let k = 0; k < K; k++) {
207
+ const tmp = genmrb[k * N + id]!;
208
+ genmrb[k * N + id] = genmrb[k * N + icol]!;
209
+ genmrb[k * N + icol] = tmp;
210
+ }
211
+ const tmp = indices[id]!;
212
+ indices[id] = indices[icol]!;
213
+ indices[icol] = tmp;
214
+ }
215
+ for (let ii = 0; ii < K; ii++) {
216
+ if (ii !== id && genmrb[ii * N + id] === 1) {
217
+ for (let c = 0; c < N; c++) {
218
+ genmrb[ii * N + c]! ^= genmrb[id * N + c]!;
219
+ }
220
+ }
221
+ }
222
+ found = true;
223
+ break;
224
+ }
225
+ }
226
+ if (!found) return null;
227
+ }
228
+
229
+ // Hard decisions on reordered received word
230
+ const hdec = new Int8Array(N);
231
+ for (let i = 0; i < N; i++) {
232
+ hdec[i] = llr[indices[i]!]! >= 0 ? 1 : 0;
233
+ }
234
+ const absrx = new Float64Array(N);
235
+ for (let i = 0; i < N; i++) {
236
+ absrx[i] = Math.abs(llr[indices[i]!]!);
237
+ }
238
+
239
+ // Transpose of reordered gen matrix
240
+ const g2 = new Uint8Array(N * K);
241
+ for (let i = 0; i < K; i++) {
242
+ for (let j = 0; j < N; j++) {
243
+ g2[j * K + i] = genmrb[i * N + j]!;
244
+ }
245
+ }
246
+
247
+ function mrbencode(me: Int8Array): Int8Array {
248
+ const codeword = new Int8Array(N);
249
+ for (let i = 0; i < K; i++) {
250
+ if (me[i] === 1) {
251
+ for (let j = 0; j < N; j++) {
252
+ codeword[j]! ^= g2[j * K + i]!;
253
+ }
254
+ }
255
+ }
256
+ return codeword;
257
+ }
258
+
259
+ const m0 = hdec.slice(0, K);
260
+ const c0 = mrbencode(m0);
261
+ const bestCw = new Int8Array(c0);
262
+ let dmin = 0;
263
+ for (let i = 0; i < N; i++) {
264
+ const x = c0[i]! ^ hdec[i]!;
265
+ dmin += x * absrx[i]!;
266
+ }
267
+
268
+ // Order-1: flip single bits in the info portion
269
+ for (let i1 = K - 1; i1 >= 0; i1--) {
270
+ if (apmask[indices[i1]!] === 1) continue;
271
+ const me = new Int8Array(m0);
272
+ me[i1]! ^= 1;
273
+ const ce = mrbencode(me);
274
+ let _nh = 0;
275
+ let dd = 0;
276
+ for (let j = 0; j < N; j++) {
277
+ const x = ce[j]! ^ hdec[j]!;
278
+ _nh += x;
279
+ dd += x * absrx[j]!;
280
+ }
281
+ if (dd < dmin) {
282
+ dmin = dd;
283
+ bestCw.set(ce);
284
+ }
285
+ }
286
+
287
+ // Order-2: flip pairs of least-reliable info bits (limited search)
288
+ if (norder >= 2) {
289
+ const ntry = Math.min(40, K);
290
+ for (let i1 = K - 1; i1 >= K - ntry; i1--) {
291
+ if (apmask[indices[i1]!] === 1) continue;
292
+ for (let i2 = i1 - 1; i2 >= K - ntry; i2--) {
293
+ if (apmask[indices[i2]!] === 1) continue;
294
+ const me = new Int8Array(m0);
295
+ me[i1]! ^= 1;
296
+ me[i2]! ^= 1;
297
+ const ce = mrbencode(me);
298
+ let _nh = 0;
299
+ let dd = 0;
300
+ for (let j = 0; j < N; j++) {
301
+ const x = ce[j]! ^ hdec[j]!;
302
+ _nh += x;
303
+ dd += x * absrx[j]!;
304
+ }
305
+ if (dd < dmin) {
306
+ dmin = dd;
307
+ bestCw.set(ce);
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ // Reorder codeword back to original order
314
+ const finalCw = new Int8Array(N);
315
+ for (let i = 0; i < N; i++) {
316
+ finalCw[indices[i]!] = bestCw[i]!;
317
+ }
318
+
319
+ const bits91 = Array.from(finalCw.slice(0, KK));
320
+ if (!checkCRC14(bits91)) return null;
321
+
322
+ // Compute dmin in original order
323
+ let dminOrig = 0;
324
+ const hdecOrig = new Int8Array(N);
325
+ for (let i = 0; i < N; i++) hdecOrig[i] = llr[i]! >= 0 ? 1 : 0;
326
+ let nhe = 0;
327
+ for (let i = 0; i < N; i++) {
328
+ const x = finalCw[i]! ^ hdecOrig[i]!;
329
+ nhe += x;
330
+ dminOrig += x * Math.abs(llr[i]!);
331
+ }
332
+
333
+ return {
334
+ message91: bits91,
335
+ cw: Array.from(finalCw),
336
+ nharderrors: nhe,
337
+ dmin: dminOrig,
338
+ ntype: 2,
339
+ };
340
+ }
341
+
342
+ let _generator: Uint8Array | null = null;
343
+
344
+ function getGenerator(): Uint8Array {
345
+ if (_generator) return _generator;
346
+
347
+ const K = KK;
348
+ const N = N_LDPC;
349
+ const M = M_LDPC;
350
+
351
+ // Build full generator matrix (K×N) where first K columns are identity
352
+ const gen = new Uint8Array(K * N);
353
+
354
+ for (let i = 0; i < K; i++) gen[i * N + i] = 1;
355
+
356
+ // gHex encodes the M×K generator parity matrix
357
+ // gen_parity[m][k] = 1 means info bit k contributes to parity bit m
358
+ for (let m = 0; m < M; m++) {
359
+ const hexStr = gHex[m]!;
360
+ for (let j = 0; j < 23; j++) {
361
+ const val = parseInt(hexStr[j]!, 16);
362
+ const limit = j === 22 ? 3 : 4;
363
+ for (let jj = 1; jj <= limit; jj++) {
364
+ const col = j * 4 + jj - 1;
365
+ if (col < K && (val & (1 << (4 - jj))) !== 0) {
366
+ // For info bit `col`, parity bit `m` is set
367
+ gen[col * N + K + m] = 1;
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ _generator = gen;
374
+ return gen;
375
+ }