Q65: native QRA65 non-binary decode over GF(64) for weak-signal MFSK
A from-scratch, dependency-free implementation of WSJT-X's Q65 mode — the QRA65 (65, 15) Q-ary repeat-accumulate code over GF(64), decoded by non-binary message passing with a Fast Hadamard Transform per check, fed an incoherent 65-FSK Bessel metric.
Rafe project · app/radio/jtx/q65.py, app/radio/jtx/_tables/q65_code.py, app/radio/jtx/_tables/q65_gen.py · pure NumPy
Abstract
Q65 is the modern member of the WSJT-X weak-signal family: a 65-tone MFSK mode built around a non-binary forward-error-correction code — the QRA65 code, a systematic (65, 15) Q-ary repeat-accumulate (QRA) code over GF(64) — rather than the Reed–Solomon block code of its ancestor JT65 or the binary LDPC codes of FT8/FST4. The 77-bit WSJT-X message payload plus a 12-bit CRC is carried in 15 six-bit GF(64) symbols; the encoder appends 50 parity symbols, shortens away the 2 CRC symbols for transmission, interleaves a fixed 22-symbol synchronising sequence, and radiates 85 continuous-phase FSK tones. The decoder is the interesting half: after a two-stage coarse/fine sync on the 22 sync tones and a 64-way matched-filter per data symbol, an incoherent M-FSK Bessel metric turns tone powers into per-symbol probability vectors, and a non-binary sum–product decoder (qra_extrinsic) iterates belief on the code's factor graph. Each parity check is a group convolution over the additive group of GF(64) = (Z₂)⁶, so it is evaluated in O(M log M) with a Fast Walsh–Hadamard Transform rather than an O(M²) correlation. A symbol-wise MAP decision (qra_mapdecode) reads out the 15 info symbols, a CRC-12 re-check gates acceptance, and the 77 bits unpack through the shared FT8-family message codec. The entire mode — encoder and decoder, GF(64) arithmetic, the code's generator and factor-graph tables — is in-repo NumPy, with zero external binaries, and the encoder is byte-exact against WSJT-X's own q65code oracle (test_jt.py).
1. Motivation
Q65 is WSJT-X's designated mode for the hardest propagation on VHF/UHF and up: ionospheric scatter, EME (moonbounce), aircraft scatter, and troposcatter, where the channel is fading, Doppler-spread, and often below −20 dB SNR in 2500 Hz. Its predecessor JT65 protects a payload with a Reed–Solomon(63, 12) code over GF(64) and a hard-decision Berlekamp–Massey decoder; Q65 keeps the GF(64) alphabet but swaps in a code purpose-built for soft, iterative, probabilistic decoding — the QRA65 code of Nico Palermo, IV3NWV. The payoff is a genuinely soft-in decoder that exploits the full per-tone likelihood, not just a hard tone decision, and that composes cleanly with the incoherent-FSK channel metric.
Rafe already carries every other member of the WSJT-X family natively (FT8/FT4, WSPR, JT4/JT9/JT65, FST4/FST4W — see the sibling specs in this directory). Q65 was the last and hardest: its non-binary QRA code and factor-graph tables cannot be re-derived by hand, so they are transcribed verbatim from WSJT-X's C reference (qra15_65_64_irr_e23.c) and Fortran (q65_encoding_modules.f90) into committed Python tables, and the message-passing decoder is ported on top. This document is enough to reproduce the whole mode from those tables.
2. Background
2.1 The QRA code — Q-ary repeat-accumulate over GF(64)
A QRA (Q-ary Repeat-Accumulate) code is the non-binary generalisation of the classic binary RA/IRA codes. The information is a block of K symbols drawn from a q-ary alphabet (here q = 64 = 2⁶, i.e. GF(64)); each information symbol is repeated a number of times and multiplied by a field coefficient, and the repeats are accumulated (running GF sum) into parity symbols. QRA65 is the specific instance
GF(64), K = 15 information symbols, N = 65 code symbols
so it is a systematic (65, 15) code: the 65-symbol codeword is [15 info symbols | 50 parity symbols], and the 50 parity symbols are GF(64) linear combinations of the 15 info symbols with the coefficients in the committed generator matrix (_tables/q65_gen.py). Because the alphabet is GF(64), each symbol is exactly one 65-FSK tone's worth of information (6 bits), which is why the code and the modulation share the same field.
2.2 Non-binary message passing with a Hadamard transform
The decoder is the non-binary sum–product algorithm on the code's Tanner/factor graph. Messages are not scalars or log-likelihood ratios — each message is a full probability distribution over the 64 field elements (a length-64 vector). Variable (symbol) nodes multiply incoming distributions pointwise (as in binary BP); the interesting node is the check (parity) node, whose constraint is that a set of GF(64) symbols sums (XOR of their 6-bit labels, after field-coefficient scaling) to zero. Marginalising that constraint is a convolution over the additive group of GF(64), which is (Z₂)⁶. The character transform that diagonalises convolution over (Z₂)⁶ is the Walsh–Hadamard transform: transform each incoming message, multiply the transforms pointwise (leave-one-out), transform back. This turns each check's O(M²) group convolution into O(M log M) with a length-64 Fast Walsh–Hadamard Transform (M = 64). This is exactly the structure Nico Palermo's QRA library uses and that q65.py ports.
2.3 65-FSK signalling
The waveform is 65-FSK: 64 data tones (one per GF(64) symbol value) plus one extra tone — the sync tone, the lowest of the 65 — used only for the 22-symbol synchronising sequence. Each transmitted symbol is one continuous-phase sinusoid of duration NSPS samples. Detection is incoherent (no carrier-phase reference): each symbol block is matched-filtered against all 64 data-tone frequencies and the per-tone powers feed the metric of §5.
3. Signal, frame and symbol structure
All numbers below are quoted from the implementation.
3.1 The 85-symbol frame
NSYM = 85 transmitted symbols (q65.py)
= 63 code symbols + 22 sync symbols
- 63 code symbols. The QRA65 codeword is 65 symbols; the encoder shortens by dropping the 2 CRC symbols before transmission:
So symbols at codeword indices 0–12 (77-bit payload + 1 pad bit) and 15–64 (the 50 parity symbols) are sent; the 2 CRC symbols at codeword indices 13, 14 are not transmitted — the receiver reconstructs and re-checks them.short = codeword[0:13] + codeword[15:65] # 13 + 50 = 63 (q65.py) - 22 sync symbols, always the sync tone (tone 0). Their 1-based positions in the 85-symbol frame are fixed (
q65.py):SYNC = (1, 9, 12, 13, 15, 22, 23, 26, 27, 33, 35, 38, 46, 50, 55, 60, 62, 66, 69, 74, 76, 85)
Interleaving (from channel_symbols, q65.py): walk symbol positions i = 1..85; if i is in SYNC, emit tone 0; otherwise emit the next code symbol offset by one, short[k] + 1. Data symbols therefore occupy tones 1..64 and the sync tone is 0, giving the 65-tone alphabet.
3.2 Tones, timing and tone spacing (submodes A–E)
A symbol lasts NSPS samples; the baud (symbol rate) and the tone spacing follow from the T/R period and the submode. From q65.py and q65.py:
NSPS = {15: 1800, 30: 3600, 60: 7200, 120: 16000, 300: 41472} # samples/symbol
BAUD_SUBMODE = {"A": 1, "B": 2, "C": 4, "D": 8, "E": 16} # spacing multiple
At the fixed working sample_rate = 12000:
baud = sample_rate / NSPS
tone spacing = baud * BAUD_SUBMODE[submode]
tone k freq = f0 + k * spacing (tone k, k = 0..64; f0 = base of tone 0)
| period (s) | NSPS | baud (Hz) | symbol (s) | frame (s) = 85·sym | A spacing = baud | E spacing = 16·baud |
|---|---|---|---|---|---|---|
| 15 | 1800 | 6.6667 | 0.150 | 12.75 | 6.667 Hz | 106.7 Hz |
| 30 | 3600 | 3.3333 | 0.300 | 25.50 | 3.333 Hz | 53.3 Hz |
| 60 | 7200 | 1.6667 | 0.600 | 51.00 | 1.667 Hz | 26.7 Hz |
| 120 | 16000 | 0.7500 | 1.333 | 113.3 | 0.750 Hz | 12.0 Hz |
| 300 | 41472 | 0.2894 | 3.456 | 293.8 | 0.289 Hz | 4.63 Hz |
The occupied bandwidth is ≈ 65 × (tone spacing). WSJT-X names a submode by period + letter, e.g. Q65-30A, Q65-60C: the number is the T/R period, the letter is the tone-spacing multiplier (wider spacing = more Doppler tolerance). The app wires Q65 at period 30 by default (app/radio/wsjtx.py); q65.py's own encode_pcm/decode_slot default to period 60, submode A.
3.3 Continuous-phase synthesis
synth (q65.py) builds a phase-continuous 65-FSK waveform: per-sample instantaneous frequency f0 + tone[isym]·spacing, integrated by cumulative sum into phase, sin(phase), float32. encode_pcm (q65.py) places the 85-symbol burst after a lead_s-second lead-in (default 1.0 s) inside a full period-second slot and quantises to little-endian s16.
4. Encode, step by step
channel_symbols(message) (q65.py) turns a text message into the 85 tones.
Step 1 — Pack the 77-bit payload. pack77(message) (the shared FT8-family codec, app/radio/ftx/pack.py; see §6.5 and the message-packing spec) returns 10 bytes carrying the 77-bit payload. The bits are spread MSB-first into a length-90 array; index 77 is a pad bit left 0, indices 78–89 will hold the CRC:
payload = pack77(message) # 10 bytes
mbits = [0]*90
for bi in range(77):
mbits[bi] = (payload[bi//8] >> (7 - bi%8)) & 1
Step 2 — CRC-12. _crc12(mbits) (q65.py) computes a 12-bit CRC and writes it into mbits[78:90] (algorithm in §6.4).
Step 3 — Form 15 GF(64) info symbols. Pack the 90 bits into 15 six-bit symbols, MSB-first (q65.py):
message15[i] = (mbits[i*6] << 5) | (mbits[i*6+1] << 4) | ... | mbits[i*6+5]
Symbols 0–12 carry the 77 payload bits + 1 pad bit (78 bits); symbols 13–14 carry the 12 CRC bits.
Step 4 — Systematic QRA65 encode. _q65_encode(message15) (q65.py):
codeword[0:15] = message15 # systematic part
for i in range(15):
if message15[i] != 0:
for j in range(15, 65):
codeword[j] ^= gf_mult(message15[i], GENERATOR[i][j-15]) # GF(64)
GENERATOR[i] is row i of the 15×50 generator matrix (q65_gen.py); the 50 parity symbols codeword[15:65] are the GF(64) sum of each info symbol times its generator coefficient. gf_mult/gf_add are log/antilog and XOR (§6.1). Result: a 65-symbol codeword.
Step 5 — Shorten. Drop the 2 CRC symbols (indices 13, 14): short = codeword[0:13] + codeword[15:65] → 63 code symbols.
Step 6 — Insert sync & offset. Interleave the 22 sync tones (tone 0) at the SYNC positions; emit data symbols as short[k] + 1 (tones 1–64) elsewhere (§3.1). Result: 85 tones in 0..64.
Then synth/encode_pcm (§3.3) render the waveform.
5. Decode, step by step
decode_slot(pcm, sample_rate=12000, period=60, f_lo=200.0, f_hi=1600.0) (q65.py). The FFT/matched-filter length is one symbol, fftlen = nsps, so the FFT bin width equals the baud, binhz = sample_rate/nsps.
Submode note.
decode_slot(..., submode=)scales the data-tone spacing byBAUD_SUBMODE[submode](A/B/C/D/E = ×1/2/4/8/16,q65.py), so all five submodes decode. Sync detection keys on the base tone at f0 and is submode-independent. Round-trip-tested A–E (test_q65_submodes).
5.1 Coarse sync (t₀, f₀)
q65.py. For each candidate start t0 (stepping nsps//3 over the first 1.5 s) build the per-symbol power spectrum (85 × F matrix of |rFFT|²), normalise each symbol row by its total power, and for each candidate base bin fb in [f_lo/binhz, f_hi/binhz) score the sync-tone energy — the sum, over the 22 sync positions, of the normalised power in bin fb:
sc = sum(mat[p, fb] / tot[p] for p in sync_pos) # sync_pos = SYNC - 1
Keep the (sc, t0, fb) with the best score. (This score is reported as the snr field; it is a sync-correlation strength, not a calibrated dB SNR — see §8.)
5.2 Fine sync
q65.py. Refine f0 to sub-bin resolution by scanning f0 across ±1 bin in steps of binhz/6, maximising the coherent sync-tone power Σ|⟨block, e^{-j2πf0 n/fs}⟩|² over the sync blocks; then refine t0 across ±nsps/2 in steps of nsps/16 at the fixed f0.
5.3 Per-symbol tone powers (the 64-way matched filter)
q65.py. For the 63 data-symbol positions, matched-filter each symbol block against the 64 data-tone complex exponentials f0 + k·binhz, k = 1..64:
dtones = f0 + arange(1,65)*binhz
basis = exp(-2j*pi*outer(dtones, n)/sample_rate) # 64 x fftlen
powers = [ |basis @ block(p, t0)|**2 for p in data_pos ] # 63 x 64
5.4 The incoherent M-FSK Bessel metric
_bessel_metric(powers) (q65.py) converts the (63×64) tone powers into (63×64) per-symbol probability distributions over the 64 tones. This is the soft front end of the decoder and the reason the constant ESNO_METRIC exists.
The channel model. For incoherent (envelope) detection of one tone among M in complex AWGN, the matched-filter output on the transmitted tone has a Rician-distributed magnitude r (mean carries the signal amplitude A), and on each of the other tones a Rayleigh magnitude. The likelihood that tone m was sent, given amplitude rₘ, is proportional (after the common Gaussian factor cancels across tones) to the modified Bessel function
p(m) ∝ I₀( A·rₘ / σ² )
where σ² is the per-quadrature noise variance. That Bessel form is the exact incoherent-FSK metric; the code evaluates it via a rational approximation to log I₀ and normalises across the 64 tones.
Estimating σ and A from the data. Let rsum be the mean tone power over the whole slot. With one signal tone in 64 and Es/N0 = ESNO_METRIC,
amp = sqrt(max(powers, 0))
rsum = powers.mean()
sigma = sqrt( rsum / (1 + ESNO_METRIC/64) / 2 )
so 2σ² = rsum / (1 + ESNO/64) — i.e. rsum = 2σ²(1 + ESNO/64): the mean power is the noise floor 2σ² plus the signal's 1/64 share. Hence the signal amplitude satisfies A² = 2σ²·ESNO (Es/N0 = A²/2σ² = ESNO), and the Bessel argument A·r/σ² becomes
C = sqrt(2*ESNO_METRIC) / sigma # since A = sqrt(2*ESNO)*sigma
v = amp * C # v = A*r/sigma^2, the I0 argument (r = amp)
Why ESNO_METRIC has its value. q65.py:
ESNO_METRIC = 6 * (15.0 / 65.0) * (10 ** 0.28) # ≈ 2.6383
This is the assumed per-symbol Es/N0 (linear) at the mode's design operating point:
6= bits per channel symbol = log₂(64) (one GF(64) tone carries 6 bits);15/65= the code rate R = K/N (15 info symbols per 65 code symbols);10^0.28 = 10^(2.8/10) = 1.9055= the operating Eb/N0 = 2.8 dB in linear.
Because Es = (information bits per symbol)·Eb = (6·R)·Eb, the per-symbol Es/N0 = 6·R·(Eb/N0) = 6·(15/65)·10^0.28 ≈ 2.638. The metric is deliberately fixed to this design SNR rather than estimated per slot — it defines the shape of the soft information the QRA decoder is tuned for.
The log-I₀ approximation. q65.py evaluates a Padé-type rational approximation to log I₀(v) with these exact coefficients, then exponentiates and normalises:
vsq = v*v
v = vsq * (v + 0.039) / (vsq*0.9931 + v*2.6936 + 0.5185) # ≈ log I0(v)
p = exp(min(v, 80)) # clamp to avoid overflow
return p / p.sum(axis=1, keepdims=True) # per-symbol distribution
The min(v, 80) guards exp against overflow at high SNR; the row-normalisation makes each symbol's 64 tone-likelihoods a proper probability vector pix.
5.5 Map tone metrics onto codeword positions
q65.py. Build the 65-position intrinsic array pix (65×64), all rows uniform (1/64) initially, then place the 63 measured distributions at the transmitted codeword positions — the inverse of the encoder's shortening:
pix = tile(UNIFORM, (65,1))
for i in range(13): pix[i] = prob[i] # codeword 0..12 <- data 0..12
for i in range(50): pix[15+i] = prob[13+i] # codeword 15..64 <- data 13..62
# positions 13,14 (the un-sent CRC symbols) stay UNIFORM
5.6 Non-binary message passing — qra_extrinsic
qra_extrinsic(pix, maxiter=100) (q65.py) runs sum–product on the QRA65 factor graph defined by the tables of §6.2. The graph has V = 65 variable (symbol) nodes, C = 116 node slots (of which indices 65..115 = 51 are the real parity checks; indices 0..64 are the degree-1 channel/leaf edges), NMSG = 216 edge-messages, max variable degree MAXVDEG = 5, max check degree MAXCDEG = 3. Each message is a length-64 distribution. V2CMIDX/C2VMIDX list, per node, the edge (message) indices it touches; MSGW[edge] is a GF(64) coefficient handled as a permutation of the 64 axes (PMAT, §6.2).
Initialisation (q65.py): each variable's channel edge c2v[nv] = pix[nv] and its variable→check edges are seeded with pix[nv].
Each iteration:
- Check → variable (
q65.py), for each real checkncin65..115:- FWHT every incoming
v2cmessage on the check's edges; - for each output edge
k, form the leave-one-out product of the other transformed messages, add1e-7to element 0 for numerical safety, and FWHT back (q65.py); - apply the edge's GF permutation: if
MSGW[edge] == 0pass through, else scatter throughPMAT(d[PERM[w]] = out, the "backward" permutation) intoc2v[edge](q65.py).
- FWHT every incoming
- Variable → check (
q65.py), for each variablenv:- for each output edge, form the leave-one-out product of the other incoming
c2vmessages, normalise to sum 1; - apply the edge's GF permutation (gather,
out[PERM[w]], the "forward" permutation) intov2c[edge].
- for each output edge, form the leave-one-out product of the other incoming
- Convergence test (
q65.py):totex = Σ_nv max(v2c[nv]); iftotex > V − 0.01(every symbol's outgoing belief is essentially certain), stop. Otherwise iterate up tomaxiter = 100.
The _fwht routine (q65.py) is the standard in-place length-64 Walsh–Hadamard butterfly (a[:,:h], a[:,h:] = x+y, x−y, doubling h); it is its own inverse up to the overall scale, which the per-symbol normalisation absorbs. qra_extrinsic returns the extrinsic distributions pex for the 65 variable nodes.
5.7 MAP read-out — qra_mapdecode
q65.py. For the K = 15 systematic symbols, multiply extrinsic by intrinsic and take the arg-max:
xdec[k] = argmax( pex[k] * pix[k] ) for k in 0..14
This yields the 15 decoded GF(64) symbols (13 info + 2 CRC).
5.8 CRC-12 gate and payload recovery
q65.py. Re-run _crc12 over the 13 decoded info symbols, form the 2 CRC symbols, and compare to the decoded xdec[13:15]; any mismatch rejects the decode (returns []). On success, extract the 77 payload bits — 12 full symbols (72 bits) plus the top 5 bits of the 13th symbol (its LSB is the pad):
for i in range(12): bits += 6 bits of dgen[i] # 72
for k in range(5): bits += top-5 bits of dgen[12] # + 5 = 77
Pack into 10 bytes and unpack77 (§6.5) to text. The result is one dict {"snr", "dt", "df", "msg"} with dt = t0/fs, df = f0, snr = the sync score.
6. Constants & tables
Every table below is reproduced in full, exactly as committed in the source; a re-implementer copies them verbatim.
6.1 GF(64) arithmetic
GF64LOG / GF64ANTILOG (q65.py) are the log/antilog tables of GF(64), given in full below. The antilog table starts 1, 2, 4, 8, 16, 32, 3, …, so α⁶ = 3 = x + 1, i.e. the field is generated by the primitive polynomial x⁶ + x + 1 with α = x. Addition is XOR masked to 6 bits (_gf_add, q65.py); multiplication is log-add-mod-63 antilog (_gf_mult, q65.py):
GF64ANTILOG[(GF64LOG[a] + GF64LOG[b]) % 63] # for a,b not 0/1
GF64LOG = [ # len 64; log_alpha(i), GF64LOG[0] = -1 (undefined)
-1, 0, 1, 6, 2, 12, 7, 26, 3, 32, 13, 35, 8, 48, 27, 18, 4, 24, 33, 16, 14, 52, 36, 54, 9,
45, 49, 38, 28, 41, 19, 56, 5, 62, 25, 11, 34, 31, 17, 47, 15, 23, 53, 51, 37, 44, 55, 40,
10, 61, 46, 30, 50, 22, 39, 43, 29, 60, 42, 21, 20, 59, 57, 58
]
GF64ANTILOG = [ # len 63; alpha**i, i = 0..62
1, 2, 4, 8, 16, 32, 3, 6, 12, 24, 48, 35, 5, 10, 20, 40, 19, 38, 15, 30, 60, 59, 53, 41,
17, 34, 7, 14, 28, 56, 51, 37, 9, 18, 36, 11, 22, 44, 27, 54, 47, 29, 58, 55, 45, 25, 50,
39, 13, 26, 52, 43, 21, 42, 23, 46, 31, 62, 63, 61, 57, 49, 33
]
6.2 The QRA65 code tables (_tables/q65_code.py)
Header line (q65_code.py) fixes the code and graph dimensions:
K,N,M,V,C,MAXVDEG,MAXCDEG,NMSG = 15,65,64,65,116,5,3,216
VDEG(len 65) — degree of each variable node. First 15 info symbols are high-degree (4–5, they repeat into many parities); the 50 parity symbols are degree 3.sum(VDEG) = 216 = NMSG.CDEG(len 116) — degree of each check slot: the first 65 are 1 (channel leaves), the remaining 51 are 2 or 3 (real parity checks).sum(CDEG) = 216.V2CMIDX(len 325 = 65·MAXVDEG) — per variable, itsMAXVDEG=5edge indices,-1-padded; the first entry of each group is the variable's own channel edge (nv). The first three groups are0, 75, 92, 106, -1, 1, 66, 77, 93, -1, 2, 87, 95, 104, -1; the full list is below.C2VMIDX(len 348 = 116·MAXCDEG) — per check slot, itsMAXCDEG=3edge indices,-1-padded. The 51 real checks (from index 65) each list 2–3 edges.MSGW(len 216) — the GF(64) coefficient on each edge, expressed as a permutation index (0 = identity/×1; nonzero = a row ofPMAT). 36 of the 216 edges are nonzero; distinct nonzero values run up to 62.PMAT(len 4032 = 63·64) — 63 length-64 permutations of the tone axes. Row 0 is the identity0,1,2,…,63;MSGW[edge] = wselects roww, which relabels a message's 64 axes to implement that edge's GF(64) coefficient. (The permutations are in the transform/bit-reversed axis ordering the FWHT works in, not the natural GF ordering — so they must be copied verbatim, not regenerated fromgf_mult.) Row 1 begins0, 33, 1, 32, 2, 35, 3, 34, …(the full 63×64 table is below). In use (q65.py): forwarddst[i] = src[perm[i]](variable→check), backwarddst[perm[i]] = src[i](check→variable).
The complete graph tables (_tables/q65_code.py):
VDEG = [ # len 65 variable-node degrees; sum = 216
4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3
]
CDEG = [ # len 116 check-slot degrees; sum = 216
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2
]
MSGW = [ # len 216 edge GF(64) coeffs (0 = identity; else PMAT row); 36 nonzero
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 14, 0, 0, 13, 37, 0, 27, 56, 62, 29, 0, 52, 34, 62, 4, 3, 22, 25, 0, 22,
0, 20, 10, 0, 43, 53, 60, 0, 0, 0, 62, 0, 5, 0, 61, 36, 31, 61, 59, 10, 0, 29, 39, 25, 18,
0, 14, 11, 50, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
V2CMIDX = [ # len 325 = 65*MAXVDEG; per variable, 5 edge indices, -1 padded
0, 75, 92, 106, -1, 1, 66, 77, 93, -1, 2, 87, 95, 104, -1, 3, 67, 83, 113, -1, 4, 68, 90,
108, -1, 5, 74, 86, 98, -1, 6, 82, 99, 114, -1, 7, 76, 85, 101, 109, 8, 69, 79, 91, 111,
9, 71, 80, 96, 105, 10, 73, 84, 107, 115, 11, 78, 94, 103, 112, 12, 70, 81, 89, 102, 13,
65, 88, 100, -1, 14, 72, 97, 110, -1, 15, 116, 117, -1, -1, 16, 118, 119, -1, -1, 17, 120,
121, -1, -1, 18, 122, 123, -1, -1, 19, 124, 125, -1, -1, 20, 126, 127, -1, -1, 21, 128,
129, -1, -1, 22, 130, 131, -1, -1, 23, 132, 133, -1, -1, 24, 134, 135, -1, -1, 25, 136,
137, -1, -1, 26, 138, 139, -1, -1, 27, 140, 141, -1, -1, 28, 142, 143, -1, -1, 29, 144,
145, -1, -1, 30, 146, 147, -1, -1, 31, 148, 149, -1, -1, 32, 150, 151, -1, -1, 33, 152,
153, -1, -1, 34, 154, 155, -1, -1, 35, 156, 157, -1, -1, 36, 158, 159, -1, -1, 37, 160,
161, -1, -1, 38, 162, 163, -1, -1, 39, 164, 165, -1, -1, 40, 166, 167, -1, -1, 41, 168,
169, -1, -1, 42, 170, 171, -1, -1, 43, 172, 173, -1, -1, 44, 174, 175, -1, -1, 45, 176,
177, -1, -1, 46, 178, 179, -1, -1, 47, 180, 181, -1, -1, 48, 182, 183, -1, -1, 49, 184,
185, -1, -1, 50, 186, 187, -1, -1, 51, 188, 189, -1, -1, 52, 190, 191, -1, -1, 53, 192,
193, -1, -1, 54, 194, 195, -1, -1, 55, 196, 197, -1, -1, 56, 198, 199, -1, -1, 57, 200,
201, -1, -1, 58, 202, 203, -1, -1, 59, 204, 205, -1, -1, 60, 206, 207, -1, -1, 61, 208,
209, -1, -1, 62, 210, 211, -1, -1, 63, 212, 213, -1, -1, 64, 214, 215, -1, -1
]
C2VMIDX = [ # len 348 = 116*MAXCDEG; per check slot, 3 edge indices, -1 padded
0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1, 5, -1, -1, 6, -1, -1, 7, -1, -1, 8,
-1, -1, 9, -1, -1, 10, -1, -1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15, -1, -1,
16, -1, -1, 17, -1, -1, 18, -1, -1, 19, -1, -1, 20, -1, -1, 21, -1, -1, 22, -1, -1, 23,
-1, -1, 24, -1, -1, 25, -1, -1, 26, -1, -1, 27, -1, -1, 28, -1, -1, 29, -1, -1, 30, -1,
-1, 31, -1, -1, 32, -1, -1, 33, -1, -1, 34, -1, -1, 35, -1, -1, 36, -1, -1, 37, -1, -1,
38, -1, -1, 39, -1, -1, 40, -1, -1, 41, -1, -1, 42, -1, -1, 43, -1, -1, 44, -1, -1, 45,
-1, -1, 46, -1, -1, 47, -1, -1, 48, -1, -1, 49, -1, -1, 50, -1, -1, 51, -1, -1, 52, -1,
-1, 53, -1, -1, 54, -1, -1, 55, -1, -1, 56, -1, -1, 57, -1, -1, 58, -1, -1, 59, -1, -1,
60, -1, -1, 61, -1, -1, 62, -1, -1, 63, -1, -1, 64, -1, -1, 65, 116, -1, 66, 117, 118, 67,
119, 120, 68, 121, 122, 69, 123, 124, 70, 125, 126, 71, 127, 128, 72, 129, 130, 73, 131,
132, 74, 133, 134, 75, 135, 136, 76, 137, 138, 77, 139, 140, 78, 141, 142, 79, 143, 144,
80, 145, 146, 81, 147, 148, 82, 149, 150, 83, 151, 152, 84, 153, 154, 85, 155, 156, 86,
157, 158, 87, 159, 160, 88, 161, 162, 89, 163, 164, 90, 165, 166, 91, 167, 168, 92, 169,
170, 93, 171, 172, 94, 173, 174, 95, 175, 176, 96, 177, 178, 97, 179, 180, 98, 181, 182,
99, 183, 184, 100, 185, 186, 101, 187, 188, 102, 189, 190, 103, 191, 192, 104, 193, 194,
105, 195, 196, 106, 197, 198, 107, 199, 200, 108, 201, 202, 109, 203, 204, 110, 205, 206,
111, 207, 208, 112, 209, 210, 113, 211, 212, 114, 213, 214, 115, 215, -1
]
PMAT in full — 63 rows of 64, one permutation per line (row w is selected by MSGW[edge] = w; row 0 is the identity 0,1,…,63):
PMAT = [ # 63 rows of 64; row w = permutation for GF(64) coefficient; row 0 = identity
0, 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,
0, 33, 1, 32, 2, 35, 3, 34, 4, 37, 5, 36, 6, 39, 7, 38, 8, 41, 9, 40, 10, 43, 11, 42, 12, 45, 13, 44, 14, 47, 15, 46, 16, 49, 17, 48, 18, 51, 19, 50, 20, 53, 21, 52, 22, 55, 23, 54, 24, 57, 25, 56, 26, 59, 27, 58, 28, 61, 29, 60, 30, 63, 31, 62,
0, 49, 33, 16, 1, 48, 32, 17, 2, 51, 35, 18, 3, 50, 34, 19, 4, 53, 37, 20, 5, 52, 36, 21, 6, 55, 39, 22, 7, 54, 38, 23, 8, 57, 41, 24, 9, 56, 40, 25, 10, 59, 43, 26, 11, 58, 42, 27, 12, 61, 45, 28, 13, 60, 44, 29, 14, 63, 47, 30, 15, 62, 46, 31,
0, 57, 49, 8, 33, 24, 16, 41, 1, 56, 48, 9, 32, 25, 17, 40, 2, 59, 51, 10, 35, 26, 18, 43, 3, 58, 50, 11, 34, 27, 19, 42, 4, 61, 53, 12, 37, 28, 20, 45, 5, 60, 52, 13, 36, 29, 21, 44, 6, 63, 55, 14, 39, 30, 22, 47, 7, 62, 54, 15, 38, 31, 23, 46,
0, 61, 57, 4, 49, 12, 8, 53, 33, 28, 24, 37, 16, 45, 41, 20, 1, 60, 56, 5, 48, 13, 9, 52, 32, 29, 25, 36, 17, 44, 40, 21, 2, 63, 59, 6, 51, 14, 10, 55, 35, 30, 26, 39, 18, 47, 43, 22, 3, 62, 58, 7, 50, 15, 11, 54, 34, 31, 27, 38, 19, 46, 42, 23,
0, 63, 61, 2, 57, 6, 4, 59, 49, 14, 12, 51, 8, 55, 53, 10, 33, 30, 28, 35, 24, 39, 37, 26, 16, 47, 45, 18, 41, 22, 20, 43, 1, 62, 60, 3, 56, 7, 5, 58, 48, 15, 13, 50, 9, 54, 52, 11, 32, 31, 29, 34, 25, 38, 36, 27, 17, 46, 44, 19, 40, 23, 21, 42,
0, 62, 63, 1, 61, 3, 2, 60, 57, 7, 6, 56, 4, 58, 59, 5, 49, 15, 14, 48, 12, 50, 51, 13, 8, 54, 55, 9, 53, 11, 10, 52, 33, 31, 30, 32, 28, 34, 35, 29, 24, 38, 39, 25, 37, 27, 26, 36, 16, 46, 47, 17, 45, 19, 18, 44, 41, 23, 22, 40, 20, 42, 43, 21,
0, 31, 62, 33, 63, 32, 1, 30, 61, 34, 3, 28, 2, 29, 60, 35, 57, 38, 7, 24, 6, 25, 56, 39, 4, 27, 58, 37, 59, 36, 5, 26, 49, 46, 15, 16, 14, 17, 48, 47, 12, 19, 50, 45, 51, 44, 13, 18, 8, 23, 54, 41, 55, 40, 9, 22, 53, 42, 11, 20, 10, 21, 52, 43,
0, 46, 31, 49, 62, 16, 33, 15, 63, 17, 32, 14, 1, 47, 30, 48, 61, 19, 34, 12, 3, 45, 28, 50, 2, 44, 29, 51, 60, 18, 35, 13, 57, 23, 38, 8, 7, 41, 24, 54, 6, 40, 25, 55, 56, 22, 39, 9, 4, 42, 27, 53, 58, 20, 37, 11, 59, 21, 36, 10, 5, 43, 26, 52,
0, 23, 46, 57, 31, 8, 49, 38, 62, 41, 16, 7, 33, 54, 15, 24, 63, 40, 17, 6, 32, 55, 14, 25, 1, 22, 47, 56, 30, 9, 48, 39, 61, 42, 19, 4, 34, 53, 12, 27, 3, 20, 45, 58, 28, 11, 50, 37, 2, 21, 44, 59, 29, 10, 51, 36, 60, 43, 18, 5, 35, 52, 13, 26,
0, 42, 23, 61, 46, 4, 57, 19, 31, 53, 8, 34, 49, 27, 38, 12, 62, 20, 41, 3, 16, 58, 7, 45, 33, 11, 54, 28, 15, 37, 24, 50, 63, 21, 40, 2, 17, 59, 6, 44, 32, 10, 55, 29, 14, 36, 25, 51, 1, 43, 22, 60, 47, 5, 56, 18, 30, 52, 9, 35, 48, 26, 39, 13,
0, 21, 42, 63, 23, 2, 61, 40, 46, 59, 4, 17, 57, 44, 19, 6, 31, 10, 53, 32, 8, 29, 34, 55, 49, 36, 27, 14, 38, 51, 12, 25, 62, 43, 20, 1, 41, 60, 3, 22, 16, 5, 58, 47, 7, 18, 45, 56, 33, 52, 11, 30, 54, 35, 28, 9, 15, 26, 37, 48, 24, 13, 50, 39,
0, 43, 21, 62, 42, 1, 63, 20, 23, 60, 2, 41, 61, 22, 40, 3, 46, 5, 59, 16, 4, 47, 17, 58, 57, 18, 44, 7, 19, 56, 6, 45, 31, 52, 10, 33, 53, 30, 32, 11, 8, 35, 29, 54, 34, 9, 55, 28, 49, 26, 36, 15, 27, 48, 14, 37, 38, 13, 51, 24, 12, 39, 25, 50,
0, 52, 43, 31, 21, 33, 62, 10, 42, 30, 1, 53, 63, 11, 20, 32, 23, 35, 60, 8, 2, 54, 41, 29, 61, 9, 22, 34, 40, 28, 3, 55, 46, 26, 5, 49, 59, 15, 16, 36, 4, 48, 47, 27, 17, 37, 58, 14, 57, 13, 18, 38, 44, 24, 7, 51, 19, 39, 56, 12, 6, 50, 45, 25,
0, 26, 52, 46, 43, 49, 31, 5, 21, 15, 33, 59, 62, 36, 10, 16, 42, 48, 30, 4, 1, 27, 53, 47, 63, 37, 11, 17, 20, 14, 32, 58, 23, 13, 35, 57, 60, 38, 8, 18, 2, 24, 54, 44, 41, 51, 29, 7, 61, 39, 9, 19, 22, 12, 34, 56, 40, 50, 28, 6, 3, 25, 55, 45,
0, 13, 26, 23, 52, 57, 46, 35, 43, 38, 49, 60, 31, 18, 5, 8, 21, 24, 15, 2, 33, 44, 59, 54, 62, 51, 36, 41, 10, 7, 16, 29, 42, 39, 48, 61, 30, 19, 4, 9, 1, 12, 27, 22, 53, 56, 47, 34, 63, 50, 37, 40, 11, 6, 17, 28, 20, 25, 14, 3, 32, 45, 58, 55,
0, 39, 13, 42, 26, 61, 23, 48, 52, 19, 57, 30, 46, 9, 35, 4, 43, 12, 38, 1, 49, 22, 60, 27, 31, 56, 18, 53, 5, 34, 8, 47, 21, 50, 24, 63, 15, 40, 2, 37, 33, 6, 44, 11, 59, 28, 54, 17, 62, 25, 51, 20, 36, 3, 41, 14, 10, 45, 7, 32, 16, 55, 29, 58,
0, 50, 39, 21, 13, 63, 42, 24, 26, 40, 61, 15, 23, 37, 48, 2, 52, 6, 19, 33, 57, 11, 30, 44, 46, 28, 9, 59, 35, 17, 4, 54, 43, 25, 12, 62, 38, 20, 1, 51, 49, 3, 22, 36, 60, 14, 27, 41, 31, 45, 56, 10, 18, 32, 53, 7, 5, 55, 34, 16, 8, 58, 47, 29,
0, 25, 50, 43, 39, 62, 21, 12, 13, 20, 63, 38, 42, 51, 24, 1, 26, 3, 40, 49, 61, 36, 15, 22, 23, 14, 37, 60, 48, 41, 2, 27, 52, 45, 6, 31, 19, 10, 33, 56, 57, 32, 11, 18, 30, 7, 44, 53, 46, 55, 28, 5, 9, 16, 59, 34, 35, 58, 17, 8, 4, 29, 54, 47,
0, 45, 25, 52, 50, 31, 43, 6, 39, 10, 62, 19, 21, 56, 12, 33, 13, 32, 20, 57, 63, 18, 38, 11, 42, 7, 51, 30, 24, 53, 1, 44, 26, 55, 3, 46, 40, 5, 49, 28, 61, 16, 36, 9, 15, 34, 22, 59, 23, 58, 14, 35, 37, 8, 60, 17, 48, 29, 41, 4, 2, 47, 27, 54,
0, 55, 45, 26, 25, 46, 52, 3, 50, 5, 31, 40, 43, 28, 6, 49, 39, 16, 10, 61, 62, 9, 19, 36, 21, 34, 56, 15, 12, 59, 33, 22, 13, 58, 32, 23, 20, 35, 57, 14, 63, 8, 18, 37, 38, 17, 11, 60, 42, 29, 7, 48, 51, 4, 30, 41, 24, 47, 53, 2, 1, 54, 44, 27,
0, 58, 55, 13, 45, 23, 26, 32, 25, 35, 46, 20, 52, 14, 3, 57, 50, 8, 5, 63, 31, 37, 40, 18, 43, 17, 28, 38, 6, 60, 49, 11, 39, 29, 16, 42, 10, 48, 61, 7, 62, 4, 9, 51, 19, 41, 36, 30, 21, 47, 34, 24, 56, 2, 15, 53, 12, 54, 59, 1, 33, 27, 22, 44,
0, 29, 58, 39, 55, 42, 13, 16, 45, 48, 23, 10, 26, 7, 32, 61, 25, 4, 35, 62, 46, 51, 20, 9, 52, 41, 14, 19, 3, 30, 57, 36, 50, 47, 8, 21, 5, 24, 63, 34, 31, 2, 37, 56, 40, 53, 18, 15, 43, 54, 17, 12, 28, 1, 38, 59, 6, 27, 60, 33, 49, 44, 11, 22,
0, 47, 29, 50, 58, 21, 39, 8, 55, 24, 42, 5, 13, 34, 16, 63, 45, 2, 48, 31, 23, 56, 10, 37, 26, 53, 7, 40, 32, 15, 61, 18, 25, 54, 4, 43, 35, 12, 62, 17, 46, 1, 51, 28, 20, 59, 9, 38, 52, 27, 41, 6, 14, 33, 19, 60, 3, 44, 30, 49, 57, 22, 36, 11,
0, 54, 47, 25, 29, 43, 50, 4, 58, 12, 21, 35, 39, 17, 8, 62, 55, 1, 24, 46, 42, 28, 5, 51, 13, 59, 34, 20, 16, 38, 63, 9, 45, 27, 2, 52, 48, 6, 31, 41, 23, 33, 56, 14, 10, 60, 37, 19, 26, 44, 53, 3, 7, 49, 40, 30, 32, 22, 15, 57, 61, 11, 18, 36,
0, 27, 54, 45, 47, 52, 25, 2, 29, 6, 43, 48, 50, 41, 4, 31, 58, 33, 12, 23, 21, 14, 35, 56, 39, 60, 17, 10, 8, 19, 62, 37, 55, 44, 1, 26, 24, 3, 46, 53, 42, 49, 28, 7, 5, 30, 51, 40, 13, 22, 59, 32, 34, 57, 20, 15, 16, 11, 38, 61, 63, 36, 9, 18,
0, 44, 27, 55, 54, 26, 45, 1, 47, 3, 52, 24, 25, 53, 2, 46, 29, 49, 6, 42, 43, 7, 48, 28, 50, 30, 41, 5, 4, 40, 31, 51, 58, 22, 33, 13, 12, 32, 23, 59, 21, 57, 14, 34, 35, 15, 56, 20, 39, 11, 60, 16, 17, 61, 10, 38, 8, 36, 19, 63, 62, 18, 37, 9,
0, 22, 44, 58, 27, 13, 55, 33, 54, 32, 26, 12, 45, 59, 1, 23, 47, 57, 3, 21, 52, 34, 24, 14, 25, 15, 53, 35, 2, 20, 46, 56, 29, 11, 49, 39, 6, 16, 42, 60, 43, 61, 7, 17, 48, 38, 28, 10, 50, 36, 30, 8, 41, 63, 5, 19, 4, 18, 40, 62, 31, 9, 51, 37,
0, 11, 22, 29, 44, 39, 58, 49, 27, 16, 13, 6, 55, 60, 33, 42, 54, 61, 32, 43, 26, 17, 12, 7, 45, 38, 59, 48, 1, 10, 23, 28, 47, 36, 57, 50, 3, 8, 21, 30, 52, 63, 34, 41, 24, 19, 14, 5, 25, 18, 15, 4, 53, 62, 35, 40, 2, 9, 20, 31, 46, 37, 56, 51,
0, 36, 11, 47, 22, 50, 29, 57, 44, 8, 39, 3, 58, 30, 49, 21, 27, 63, 16, 52, 13, 41, 6, 34, 55, 19, 60, 24, 33, 5, 42, 14, 54, 18, 61, 25, 32, 4, 43, 15, 26, 62, 17, 53, 12, 40, 7, 35, 45, 9, 38, 2, 59, 31, 48, 20, 1, 37, 10, 46, 23, 51, 28, 56,
0, 18, 36, 54, 11, 25, 47, 61, 22, 4, 50, 32, 29, 15, 57, 43, 44, 62, 8, 26, 39, 53, 3, 17, 58, 40, 30, 12, 49, 35, 21, 7, 27, 9, 63, 45, 16, 2, 52, 38, 13, 31, 41, 59, 6, 20, 34, 48, 55, 37, 19, 1, 60, 46, 24, 10, 33, 51, 5, 23, 42, 56, 14, 28,
0, 9, 18, 27, 36, 45, 54, 63, 11, 2, 25, 16, 47, 38, 61, 52, 22, 31, 4, 13, 50, 59, 32, 41, 29, 20, 15, 6, 57, 48, 43, 34, 44, 37, 62, 55, 8, 1, 26, 19, 39, 46, 53, 60, 3, 10, 17, 24, 58, 51, 40, 33, 30, 23, 12, 5, 49, 56, 35, 42, 21, 28, 7, 14,
0, 37, 9, 44, 18, 55, 27, 62, 36, 1, 45, 8, 54, 19, 63, 26, 11, 46, 2, 39, 25, 60, 16, 53, 47, 10, 38, 3, 61, 24, 52, 17, 22, 51, 31, 58, 4, 33, 13, 40, 50, 23, 59, 30, 32, 5, 41, 12, 29, 56, 20, 49, 15, 42, 6, 35, 57, 28, 48, 21, 43, 14, 34, 7,
0, 51, 37, 22, 9, 58, 44, 31, 18, 33, 55, 4, 27, 40, 62, 13, 36, 23, 1, 50, 45, 30, 8, 59, 54, 5, 19, 32, 63, 12, 26, 41, 11, 56, 46, 29, 2, 49, 39, 20, 25, 42, 60, 15, 16, 35, 53, 6, 47, 28, 10, 57, 38, 21, 3, 48, 61, 14, 24, 43, 52, 7, 17, 34,
0, 56, 51, 11, 37, 29, 22, 46, 9, 49, 58, 2, 44, 20, 31, 39, 18, 42, 33, 25, 55, 15, 4, 60, 27, 35, 40, 16, 62, 6, 13, 53, 36, 28, 23, 47, 1, 57, 50, 10, 45, 21, 30, 38, 8, 48, 59, 3, 54, 14, 5, 61, 19, 43, 32, 24, 63, 7, 12, 52, 26, 34, 41, 17,
0, 28, 56, 36, 51, 47, 11, 23, 37, 57, 29, 1, 22, 10, 46, 50, 9, 21, 49, 45, 58, 38, 2, 30, 44, 48, 20, 8, 31, 3, 39, 59, 18, 14, 42, 54, 33, 61, 25, 5, 55, 43, 15, 19, 4, 24, 60, 32, 27, 7, 35, 63, 40, 52, 16, 12, 62, 34, 6, 26, 13, 17, 53, 41,
0, 14, 28, 18, 56, 54, 36, 42, 51, 61, 47, 33, 11, 5, 23, 25, 37, 43, 57, 55, 29, 19, 1, 15, 22, 24, 10, 4, 46, 32, 50, 60, 9, 7, 21, 27, 49, 63, 45, 35, 58, 52, 38, 40, 2, 12, 30, 16, 44, 34, 48, 62, 20, 26, 8, 6, 31, 17, 3, 13, 39, 41, 59, 53,
0, 7, 14, 9, 28, 27, 18, 21, 56, 63, 54, 49, 36, 35, 42, 45, 51, 52, 61, 58, 47, 40, 33, 38, 11, 12, 5, 2, 23, 16, 25, 30, 37, 34, 43, 44, 57, 62, 55, 48, 29, 26, 19, 20, 1, 6, 15, 8, 22, 17, 24, 31, 10, 13, 4, 3, 46, 41, 32, 39, 50, 53, 60, 59,
0, 34, 7, 37, 14, 44, 9, 43, 28, 62, 27, 57, 18, 48, 21, 55, 56, 26, 63, 29, 54, 20, 49, 19, 36, 6, 35, 1, 42, 8, 45, 15, 51, 17, 52, 22, 61, 31, 58, 24, 47, 13, 40, 10, 33, 3, 38, 4, 11, 41, 12, 46, 5, 39, 2, 32, 23, 53, 16, 50, 25, 59, 30, 60,
0, 17, 34, 51, 7, 22, 37, 52, 14, 31, 44, 61, 9, 24, 43, 58, 28, 13, 62, 47, 27, 10, 57, 40, 18, 3, 48, 33, 21, 4, 55, 38, 56, 41, 26, 11, 63, 46, 29, 12, 54, 39, 20, 5, 49, 32, 19, 2, 36, 53, 6, 23, 35, 50, 1, 16, 42, 59, 8, 25, 45, 60, 15, 30,
0, 41, 17, 56, 34, 11, 51, 26, 7, 46, 22, 63, 37, 12, 52, 29, 14, 39, 31, 54, 44, 5, 61, 20, 9, 32, 24, 49, 43, 2, 58, 19, 28, 53, 13, 36, 62, 23, 47, 6, 27, 50, 10, 35, 57, 16, 40, 1, 18, 59, 3, 42, 48, 25, 33, 8, 21, 60, 4, 45, 55, 30, 38, 15,
0, 53, 41, 28, 17, 36, 56, 13, 34, 23, 11, 62, 51, 6, 26, 47, 7, 50, 46, 27, 22, 35, 63, 10, 37, 16, 12, 57, 52, 1, 29, 40, 14, 59, 39, 18, 31, 42, 54, 3, 44, 25, 5, 48, 61, 8, 20, 33, 9, 60, 32, 21, 24, 45, 49, 4, 43, 30, 2, 55, 58, 15, 19, 38,
0, 59, 53, 14, 41, 18, 28, 39, 17, 42, 36, 31, 56, 3, 13, 54, 34, 25, 23, 44, 11, 48, 62, 5, 51, 8, 6, 61, 26, 33, 47, 20, 7, 60, 50, 9, 46, 21, 27, 32, 22, 45, 35, 24, 63, 4, 10, 49, 37, 30, 16, 43, 12, 55, 57, 2, 52, 15, 1, 58, 29, 38, 40, 19,
0, 60, 59, 7, 53, 9, 14, 50, 41, 21, 18, 46, 28, 32, 39, 27, 17, 45, 42, 22, 36, 24, 31, 35, 56, 4, 3, 63, 13, 49, 54, 10, 34, 30, 25, 37, 23, 43, 44, 16, 11, 55, 48, 12, 62, 2, 5, 57, 51, 15, 8, 52, 6, 58, 61, 1, 26, 38, 33, 29, 47, 19, 20, 40,
0, 30, 60, 34, 59, 37, 7, 25, 53, 43, 9, 23, 14, 16, 50, 44, 41, 55, 21, 11, 18, 12, 46, 48, 28, 2, 32, 62, 39, 57, 27, 5, 17, 15, 45, 51, 42, 52, 22, 8, 36, 58, 24, 6, 31, 1, 35, 61, 56, 38, 4, 26, 3, 29, 63, 33, 13, 19, 49, 47, 54, 40, 10, 20,
0, 15, 30, 17, 60, 51, 34, 45, 59, 52, 37, 42, 7, 8, 25, 22, 53, 58, 43, 36, 9, 6, 23, 24, 14, 1, 16, 31, 50, 61, 44, 35, 41, 38, 55, 56, 21, 26, 11, 4, 18, 29, 12, 3, 46, 33, 48, 63, 28, 19, 2, 13, 32, 47, 62, 49, 39, 40, 57, 54, 27, 20, 5, 10,
0, 38, 15, 41, 30, 56, 17, 55, 60, 26, 51, 21, 34, 4, 45, 11, 59, 29, 52, 18, 37, 3, 42, 12, 7, 33, 8, 46, 25, 63, 22, 48, 53, 19, 58, 28, 43, 13, 36, 2, 9, 47, 6, 32, 23, 49, 24, 62, 14, 40, 1, 39, 16, 54, 31, 57, 50, 20, 61, 27, 44, 10, 35, 5,
0, 19, 38, 53, 15, 28, 41, 58, 30, 13, 56, 43, 17, 2, 55, 36, 60, 47, 26, 9, 51, 32, 21, 6, 34, 49, 4, 23, 45, 62, 11, 24, 59, 40, 29, 14, 52, 39, 18, 1, 37, 54, 3, 16, 42, 57, 12, 31, 7, 20, 33, 50, 8, 27, 46, 61, 25, 10, 63, 44, 22, 5, 48, 35,
0, 40, 19, 59, 38, 14, 53, 29, 15, 39, 28, 52, 41, 1, 58, 18, 30, 54, 13, 37, 56, 16, 43, 3, 17, 57, 2, 42, 55, 31, 36, 12, 60, 20, 47, 7, 26, 50, 9, 33, 51, 27, 32, 8, 21, 61, 6, 46, 34, 10, 49, 25, 4, 44, 23, 63, 45, 5, 62, 22, 11, 35, 24, 48,
0, 20, 40, 60, 19, 7, 59, 47, 38, 50, 14, 26, 53, 33, 29, 9, 15, 27, 39, 51, 28, 8, 52, 32, 41, 61, 1, 21, 58, 46, 18, 6, 30, 10, 54, 34, 13, 25, 37, 49, 56, 44, 16, 4, 43, 63, 3, 23, 17, 5, 57, 45, 2, 22, 42, 62, 55, 35, 31, 11, 36, 48, 12, 24,
0, 10, 20, 30, 40, 34, 60, 54, 19, 25, 7, 13, 59, 49, 47, 37, 38, 44, 50, 56, 14, 4, 26, 16, 53, 63, 33, 43, 29, 23, 9, 3, 15, 5, 27, 17, 39, 45, 51, 57, 28, 22, 8, 2, 52, 62, 32, 42, 41, 35, 61, 55, 1, 11, 21, 31, 58, 48, 46, 36, 18, 24, 6, 12,
0, 5, 10, 15, 20, 17, 30, 27, 40, 45, 34, 39, 60, 57, 54, 51, 19, 22, 25, 28, 7, 2, 13, 8, 59, 62, 49, 52, 47, 42, 37, 32, 38, 35, 44, 41, 50, 55, 56, 61, 14, 11, 4, 1, 26, 31, 16, 21, 53, 48, 63, 58, 33, 36, 43, 46, 29, 24, 23, 18, 9, 12, 3, 6,
0, 35, 5, 38, 10, 41, 15, 44, 20, 55, 17, 50, 30, 61, 27, 56, 40, 11, 45, 14, 34, 1, 39, 4, 60, 31, 57, 26, 54, 21, 51, 16, 19, 48, 22, 53, 25, 58, 28, 63, 7, 36, 2, 33, 13, 46, 8, 43, 59, 24, 62, 29, 49, 18, 52, 23, 47, 12, 42, 9, 37, 6, 32, 3,
0, 48, 35, 19, 5, 53, 38, 22, 10, 58, 41, 25, 15, 63, 44, 28, 20, 36, 55, 7, 17, 33, 50, 2, 30, 46, 61, 13, 27, 43, 56, 8, 40, 24, 11, 59, 45, 29, 14, 62, 34, 18, 1, 49, 39, 23, 4, 52, 60, 12, 31, 47, 57, 9, 26, 42, 54, 6, 21, 37, 51, 3, 16, 32,
0, 24, 48, 40, 35, 59, 19, 11, 5, 29, 53, 45, 38, 62, 22, 14, 10, 18, 58, 34, 41, 49, 25, 1, 15, 23, 63, 39, 44, 52, 28, 4, 20, 12, 36, 60, 55, 47, 7, 31, 17, 9, 33, 57, 50, 42, 2, 26, 30, 6, 46, 54, 61, 37, 13, 21, 27, 3, 43, 51, 56, 32, 8, 16,
0, 12, 24, 20, 48, 60, 40, 36, 35, 47, 59, 55, 19, 31, 11, 7, 5, 9, 29, 17, 53, 57, 45, 33, 38, 42, 62, 50, 22, 26, 14, 2, 10, 6, 18, 30, 58, 54, 34, 46, 41, 37, 49, 61, 25, 21, 1, 13, 15, 3, 23, 27, 63, 51, 39, 43, 44, 32, 52, 56, 28, 16, 4, 8,
0, 6, 12, 10, 24, 30, 20, 18, 48, 54, 60, 58, 40, 46, 36, 34, 35, 37, 47, 41, 59, 61, 55, 49, 19, 21, 31, 25, 11, 13, 7, 1, 5, 3, 9, 15, 29, 27, 17, 23, 53, 51, 57, 63, 45, 43, 33, 39, 38, 32, 42, 44, 62, 56, 50, 52, 22, 16, 26, 28, 14, 8, 2, 4,
0, 3, 6, 5, 12, 15, 10, 9, 24, 27, 30, 29, 20, 23, 18, 17, 48, 51, 54, 53, 60, 63, 58, 57, 40, 43, 46, 45, 36, 39, 34, 33, 35, 32, 37, 38, 47, 44, 41, 42, 59, 56, 61, 62, 55, 52, 49, 50, 19, 16, 21, 22, 31, 28, 25, 26, 11, 8, 13, 14, 7, 4, 1, 2,
0, 32, 3, 35, 6, 38, 5, 37, 12, 44, 15, 47, 10, 42, 9, 41, 24, 56, 27, 59, 30, 62, 29, 61, 20, 52, 23, 55, 18, 50, 17, 49, 48, 16, 51, 19, 54, 22, 53, 21, 60, 28, 63, 31, 58, 26, 57, 25, 40, 8, 43, 11, 46, 14, 45, 13, 36, 4, 39, 7, 34, 2, 33, 1,
0, 16, 32, 48, 3, 19, 35, 51, 6, 22, 38, 54, 5, 21, 37, 53, 12, 28, 44, 60, 15, 31, 47, 63, 10, 26, 42, 58, 9, 25, 41, 57, 24, 8, 56, 40, 27, 11, 59, 43, 30, 14, 62, 46, 29, 13, 61, 45, 20, 4, 52, 36, 23, 7, 55, 39, 18, 2, 50, 34, 17, 1, 49, 33,
0, 8, 16, 24, 32, 40, 48, 56, 3, 11, 19, 27, 35, 43, 51, 59, 6, 14, 22, 30, 38, 46, 54, 62, 5, 13, 21, 29, 37, 45, 53, 61, 12, 4, 28, 20, 44, 36, 60, 52, 15, 7, 31, 23, 47, 39, 63, 55, 10, 2, 26, 18, 42, 34, 58, 50, 9, 1, 25, 17, 41, 33, 57, 49,
0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 6, 2, 14, 10, 22, 18, 30, 26, 38, 34, 46, 42, 54, 50, 62, 58, 5, 1, 13, 9, 21, 17, 29, 25, 37, 33, 45, 41, 53, 49, 61, 57,
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 3, 1, 7, 5, 11, 9, 15, 13, 19, 17, 23, 21, 27, 25, 31, 29, 35, 33, 39, 37, 43, 41, 47, 45, 51, 49, 55, 53, 59, 57, 63, 61,
]
6.3 The generator matrix (_tables/q65_gen.py)
GENERATOR is the 15 × 50 parity generator over GF(64): row i holds the 50 coefficients by which info symbol i contributes to the 50 parity symbols (codeword[15:65]). Format = a Python list of 15 rows, each 50 GF(64) values in 0..63, given in full below (q65_gen.py):
GENERATOR = [ # 15 rows x 50 GF(64) coeffs; row i -> parity symbols codeword[15:65]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 34, 34, 34, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 48, 48, 48, 48, 48, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 38, 38, 38, 38, 38, 38, 38, 38],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0],
[0, 0, 0, 0, 0, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0],
]
Encoding (§4 step 4) is codeword[15+j] = Σ_i gf_mult(message15[i], GENERATOR[i][j]).
6.4 CRC-12
CRC12_P = (1,1,0,0,0,0,0,0,0,1,1,1,1) (q65.py; the source labels it 0xF01), a degree-12 generator held as 13 taps. _crc12 (q65.py) is a bit-serial LFSR division over the 90-bit message with a per-symbol bit-reversal front end:
- Bit-reverse each of the 15 six-bit symbols of the 90-bit input into
m[0:90]. - Load the 13-bit register
r = m[0:13]. - For
i = 0..77: setr[12] = m[i+12]; ifr[0]thenr ^= CRC12_P(elementwise over the 13 taps); rotate left (r = r[1:] + [r[0]]). - Write the 12 CRC bits back as two bit-reversed 6-bit groups:
out[78:84] = [r[5],r[4],r[3],r[2],r[1],r[0]],out[84:90] = [r[11],r[10],r[9],r[8],r[7],r[6]].
The decoder recomputes this from the 13 recovered info symbols and compares the two CRC symbols exactly (q65.py).
6.5 Payload — the shared 77-bit message codec
Q65 does not use the JT65-era 72-bit packing in app/radio/jtx/pack.py (packmsg → twelve 6-bit words, NBASE = 262177560, NGBASE = 32400), which is a different codec used by JT4/JT9/JT65. Q65 imports the FT8/FT4 77-bit codec:
from ..ftx.pack import pack77 as _pack77 # q65.py
from ..ftx.unpack import unpack77 # q65.py
pack77 (app/radio/ftx/pack.py) encodes a message string into 10 payload bytes (77 bits + 3 pad) across the standard message types — standard calls with grid/report/RR73/73 (i3 = 1/2), non-standard/compound calls with 22/12-bit callsign hashing (i3 = 4), and free text (i3.n3 = 0.0); unpack77 inverts them. The full field layout (n28/n29 callsign packing, igrid4, the 22/12/10-bit hash store, telemetry) is documented in the shared WSJT-X 77-bit message packing spec and is out of scope here — Q65 simply consumes its 77-bit output. (The JT65-style jtx/pack.py/unpack.py are summarised only to flag that they are not on the Q65 path.)
7. Interoperability & validation
Q65 in this repo is validated bidirectionally against WSJT-X's own reference tools, following the pattern used for the whole family (docs/native-digimodes.md).
- Encoder — byte-exact vs
q65code. WSJT-X shipsq65code, which prints the 85 channel symbols (tones 0..64) for a message.test_jt.pyasserts the nativechannel_symbols("CQ K1ABC FN42")equals the committed golden vectorQ65_CQ(test_jt.py), which was captured fromq65code:0,1,1,1,1,9,5,56,0,48,7,0,0,41,0,41,26,35,52,52,52,0,0,60,8,0,0,39,15,51, 63,61,0,61,0,20,20,0,1,4,30,2,6,6,46,0,13,9,9,0,40,6,56,15,0,15,15,24,24,0, 4,0,29,27,46,0,50,60,0,37,20,20,50,0,50,0,29,39,35,2,8,59,59,10,0test_jt.pyfurther checks the structural invariants — 85 symbols, all in0..64, exactly 22 sync tones (tone 0). - Decoder — round-trip + noisy +
q65sim. WSJT-X'sq65simgenerates test.wavfiles at chosen SNR.test_jt.pyround-tripsencode_pcm → decode_slotfor real messages; the decoder also recovers messages fromq65simreference audio (docs/native-digimodes.md). The QRA core is unit-tested directly (test_jt.py): a codeword is turned into near-hard intrinsics andqra_mapdecode(qra_extrinsic(pix), pix)must return the original 15 info symbols. - Bridge/back-end.
app/radio/wsjtx.pyselects native vs external per theRAFE_WSJTX_BACKENDswitch; when native, Q65 RX callsq65.decode_slot(buf, period=30). The mode is thus a drop-in for the externaljt9 -3Q65 decoder.
A reproduction can self-validate the same way: compare channel_symbols against q65code, and feed q65sim audio to decode_slot.
8. Limitations
Honest scope of this implementation (not of Q65 the mode):
- Single-shot, no deep search. WSJT-X's headline −24…−26 dB sensitivity comes from multi-slot averaging, a-priori (AP) decoding with known-call hypotheses, and a deep list search; none of that is ported. This decoder is a solid single-frame QRA decode "solid at moderate SNR" (
docs/native-digimodes.md), not the full q65 sensitivity ladder. - The
snrfield is a sync score, not dB. It reports the coarse sync-tone correlation strength (best[0],q65.py), not a calibrated dB SNR. - Fixed metric SNR.
ESNO_METRICpins the Bessel metric to the 2.8 dB Eb/N0 design point rather than estimating Es/N0 per slot; far from that point the soft information is mismatched (usually benign, but not optimal on very strong or very weak slots). - Runtime. Pure-Python message passing (up to 100 iterations × 51 checks × FWHTs) and the coarse t₀/f₀ grid search make a slot decode take on the order of seconds — fine for the 30–60 s cadence, but not tuned for throughput.
- Fading/Doppler. The incoherent per-symbol metric handles frequency-flat AWGN; it does not model the fast Doppler spread of EME/scatter beyond what the submode's wide tone spacing already tolerates.
None of these are structural: submode B–E demod, per-slot SNR estimation, and AP/deep search are additive on the working QRA core.
9. References
- N. Palermo, IV3NWV — QRA codes (Q-ary Repeat-Accumulate) and the
qracodeslibrary; the QRA65 (65, 15)/GF(64) code and its message-passing decoder. WSJT-X sourcesqra15_65_64_irr_e23.c(code/graph tables →_tables/q65_code.py) and the FWHT/permutation decoder ported inq65.py. - J. Taylor (K1JT), S. Franke (K9AN), B. Somerville (AC0YE) et al. — WSJT-X:
q65_encoding_modules.f90(generator →_tables/q65_gen.py), the sync vector, and theq65code/q65simreference utilities used as oracles in §7. - K1JT, K9AN, J. Taylor — "The FT4 and FT8 Communication Protocols," QEX, 2020 — the shared 77-bit message payload consumed here (§6.5).
- R. G. Gallager — Low-Density Parity-Check Codes, 1963 — the sum–product / belief-propagation algorithm generalised to GF(q) here.
- D. Divsalar, H. Jin, R. J. McEliece — "Coding theorems for turbo-like codes," 1998 — repeat-accumulate codes, of which QRA is the non-binary form.
- J. G. Proakis — Digital Communications — the incoherent M-FSK envelope detector and its I₀-Bessel likelihood (§5.4).
- Companion specs in this directory:
jt65.md(RS(63,12)/GF(64), the 65-FSK ancestor),fst4.md(binary LDPC sibling),wsjtx-message-packing.md(the payload codec), and the depth/tone benchmark../rvqvoice.md.