hfmodemDocumentation

Constant-table generation and verification

The receiver tables use deterministic generators, closed-form expressions and measured waveform constants. This guide describes their definitions and checks. Comparisons cover the active receiver range; incomplete measurements are marked.

Citations use two roots:

Strength is marked per fact:


1. Computed from a published algorithm — the VB6 Rnd stream

The whiteners and all four interleavers are draws from one generator: the Visual Basic 6 runtime PRNG in msvbvm60. It is a 24-bit linear congruential generator,

s  <-  (s * 0x43FD43FD + 0xC39EC3)  mod  2^24        (full period)
Rnd =  Single(s / 2^24)                              -- IEEE binary32
draw = Int(Rnd * n)                                  -- 0 .. n-1

The subtlety is the last line, and getting it wrong is invisible until it desynchronises a table. Rnd * n is a binary32 multiply. s / 2^24 is exact (a power-of-two divide of a 24-bit integer), but the product rounds to 24 significant bits, round-half-to-even, before Int floors it. Truncating the exact product instead — the tempting (s * n) >> 24 — disagrees with the runtime about once per 100,000 draws, and each disagreement shifts every later value in that column, desynchronising 9 of the 68 interleaver columns. gen's draw() does the binary32 multiply in exact integers. [computed]

The gaps between records were never gaps. The interleaver seeds looked like a list of unrelated magic numbers with 36-to-419-draw holes between the tables. They are one unbroken stream. Record 3's interleaver ends at state 1746102; continuing that same stream yields the record-3 base tables — 395 allocation draws followed by 24 class draws, 419 in all — and lands exactly on state 15597635, which is record 4's stage-1 seed. gen.map2_col3 raises if the stream does not close on that number; it closes. A reseed cannot land on its own successor's seed by luck, so this is the proof the seeds are draws from one generator, not 35 independent constants.


2. Derived in closed form

No algorithm state — each is a formula in an index, byte-identical under gate.

2a. The constellation LUT — reconstructed to the bit

The shared 64-entry complex LUT (gen.constellation_lut, cell = LUT[pos + 6·coded]) is six constellation columns interleaved with period 6: three PSK columns, a 4-QAM square, a 32-QAM cross, and a 64-QAM square. The 64 entries are a truncated prefix of that raster — the zeros in it are where a short PSK column has run out, not constellation nulls. [reconstructed]

Byte-exact generation specifies both the geometry and floating-point arithmetic. Two constants carry the near-quadrant PSK residues onto exact float32 values: VARA's 15-significant-digit Pi literal (3.14159265358979, seven ulp short of the true double), and the 66-bit constant the x87 FSIN/FCOS reduce their argument against (0xC90FDAA22168C234C, taken as a fraction). Compute the PSK phases against the true math.pi and the low bits differ. gate holds the whole LUT byte-for-byte in its on-disk .c64 form (512 bytes).

Inference, marked as inference. In the 32-QAM cross column, only the 10 labels in quadrants 0 and 1 were read off real captures; the other 22 of the 32 are *inferred* from the constellation's sign symmetry, not observed. The geometry the inference rests on (the mx = 2 column folding onto the |Q| = 5u arm with its two rows reversed) reproduces the shipped bytes, which is corroboration, not observation.


3. The PACTOR acquisition markers — signed Hadamard-32 rows

The 16 P2 acquisition marker words (shrike gen.marker_codes) are length-16 QPSK words whose 32 bipolar chips are signed rows of a 32×32 Sylvester-Hadamard matrix under one fixed column permutation (_PTS, the labeling points in F2^5). They are real-orthogonal: the 16×16 Gram matrix is 32·I + i·K with K integer antisymmetric, so the real parts are exactly orthogonal — what a differential correlator needs. Their XOR-span is the Reed–Muller code RM(1,5) up to a coordinate permutation. The P3 pre800 acquisition preamble is the same 32 words with the two band halves swapped and scaled to ±800 taps. [closed form / reconstructed] — gate checks the codes and pre800 byte-for-byte and re-derives the 32·I Gram.

Correction to an earlier reading of our own. An earlier note concluded the markers were "not an orthogonal code." That was an artifact of measuring correlation at length 8 — over the half-word the orthogonality is not visible; over the full length-16 word (or the 32-chip real vector) it is exact. The conclusion was about the measurement window, not the code.


4. Reduced to a documented constant — usedmap

usedmap.u8 is 900,000 bytes. The receiver reads one of 17 interleaved planes from it — the off=3 stride, 17·(128·col + sub) + 3 — and that plane is a set of 36 pilot (column, sub-band) pairs: the reference cells the BW500 L4 receiver skips. 32 pilots serve one sub-band; columns 177 and 323 serve both. 392 columns × 2 sub-bands − 36 = 748 coded cells. gen.usedmap_plane reproduces exactly that stride; gate compares it over the off=3 record and asserts the count is 36. The other 16 records' planes on the other strides are never consulted by this receiver and are out of scope. One constant serves both bandwidths. [reduced]


5. Recovered by measurement — grid480

grid480 is the per-cell differential-phase de-rotation reference, exp(j·2π·k/128) at each cell the receiver fetches. It was not derived from a formula. It was recovered by known-plaintext inversion of our own BlackHole loopback recordings: a BW500 SHORT burst emits only its data body, so inverting the recorded phase against the known payload reads the reference k off each emitted cell. [measured]

Be precise about what the inversion recovered and what it did not:

5a. Recovered by measurement — BW2300's three sub-base bin tables

The three speed levels below the BW2300 base one — hosts BITRATE (3), (2) and (1), 82 / 41 / 24 bps — place their columns the way the base level does: one lit bin per OFDM symbol, at ((base + stride·gray − first_bin) mod span) + first_bin. Each base table is one number per emission column — 228, 228 and 124 — and all three are measured, not generated. [measured]

What recovered it was known-plaintext inversion of recorded VARA audio, not a round trip through our own modulator:

Records 1 and 0, 2026-09-04. The two below it, recovered off gear-down tapes made by putting band-limited noise on a stock caller's input — a transmitter's level follows its receiver's feedback, so the overs that drop a gear are the responder's. They needed no known plaintext: with bpc 3 at stride 2 an allocation reaches only 16 bins of record 0's 64 and 16 of record 1's 32, so one over admits 2**bpc bins a column, the true base is in every over's set, and overs of differing payloads intersect down to one. Record 2 could not be read that way — 16 values at stride 2 on 32 bins is a whole parity of its comb, and its three tape frames leave 0 of 228 columns unique — which is exactly why it needed its payload and these two did not.


6. Signal-processing kernels

The PACTOR symbol pulse and acquisition filters are generated at the point of use. The VARA synthesis prototype is a measured waveform constant.

The roll-off is SCS's, not a fit of ours. [SCS-P4] §11.8 prints the symbol filter for the PACTOR-2-derived speed level — 32 coefficients, 8 per symbol, normalised to unit coefficient sum — and calls it "very similar to the RRC pulse, but optimized concerning spectral side lobes and orthogonality". A true RRC at roll-off 2/3 tracks that published pulse to 3.3e-3 peak. The same fit run against §10.1's 129-tap 1800 Bd filter returns 0.330 against a printed roll-off of 0.33, which is what says the fit is reading the pulse and not itself. So the design is: the shape SCS documents, computed from the closed form rather than transcribed.

And the air agrees with the spec. Swept across roll-off against the six packet headers of a real PACTOR-III session (occ15.wav), the constant-header fit peaks exactly at 2/3 — 0.8510 worst and 0.8759 mean, falling away on both sides and down to 0.8412 by 0.4.

It also decided against a better pulse. Truncating an RRC to 31 taps costs it the Nyquist property it was chosen for, and minimising out-of-band energy under exact orthogonality — §11.8's own two criteria — yields a pulse with a third less stopband energy and orthogonality at 1e-17. That pulse reads the same real session at 0.8481. Better in theory, measurably worse on what SCS transmitters actually put on the band, so it is not what ships.

gate checks 31 taps, symmetry, unit sum, and half amplitude at the 50 Hz Nyquist frequency (the RRC's defining property, at any roll-off), and a matched pair whose residual ISI at the symbol instants is under 1.3e-2.

The distinction is expensive and quiet. Reusing the symbol pulse here still
recovers an off-air marker at 0.997 — the score this section used to cite —
while costing the P2 receive path its whole single-copy margin (decode holds
to σ 0.60 with the right window and fails at σ 0 with the wrong one) and
stopping burst copies summing at any noise level at all. A correlation score
does not report that; `gate` pins the shape.

Against `PACTOR-II_FEC.wav`, `tests/shrike/test_p2rx.py` recovers all three of
an independent decoder's frame markers at their times and codeword indices,
and a Hann of the same support finds **no marker at all**. The interior
zeros are the β = 1 zeros at ±6 samples from the peak.

7. Corrections owed back to the published record

Two published or internal statements are wrong, and re-deriving these tables is what showed it. Recorded here so the correction travels with the tables.


8. PACTOR-III interleaver recurrence

shrike gen.INTERLEAVERS defines eight permutations from SCS's published recurrence: five single walks and three two-dimensional walk products. They are generated at the point of use. Optional reference-vector checks run when the development corpus is available.

The full-frame interleaver geometry remains unresolved (shrike/unknowns.py, U3_INTERLEAVER). The per-speed-level data frames do not consult these full-frame tables.


Summary

Generated tables are built at the point of use. The measured synthesis prototype is packaged as an asset:

File (key)CategoryHow knownProved
prototype_pulse.npzMeasuredWOLA synthesis prototype, deconvolved from our own recordings§6
ConstantHow knownHeld by
Whiteners (BW500, BW2300)Computed — VB6 Rnd, bit=s>>23byte-identical over live extent
Interleavers (4 arrays)Computed — VB6 Rnd permutation streambyte-identical incl. overwritten tail
alloc_col3, map2_col3Closed form — draws from the record-3→4 gapbyte-identical; closes on seed 15597635
map1_col3, clsparm_gray, hdr_raster_srcClosed form — index formulaebyte-identical
constellation_lutReconstructed — VB6 Pi + x87 reductionbyte-identical; 22/32 cross labels inferred
Acquisition markers, pre800Signed Hadamard-32 rows, RM(1,5) spanbyte-identical; 32·I real Gram
usedmapReduced — 36 pilots on one of 17 planesmatches the off=3 record; count 36
grid480Measured — inversion of loopback recordings786/939 on the 128-lattice; 153 underived
symbol_pulseDesigned — RRC at [SCS-P4] §11.8's roll-off, 31 taps; real-air sweep peaks theredoes not ship; gate holds the shape and the Nyquist ISI
acq_windowDesigned — RRC at unit roll-off, 27 taps, peak at 13 of 32does not ship; 3/3 real markers at 0.997–0.998, Hann finds none
acq_lowpassDesigned — firwin(96, 400 Hz, Kaiser β 6.5)does not ship; first image band 76 dB down
Eight P3 interleaversComputed — SCS's published recurrencebyte-identical against the archived bytes

PACTOR-4 C2 and higher normal-long geometry

Runtime C2 arrays use an integer square-root column walk plus compact, hash-pinned index swaps. Twelve of the thirteen runtime sizes have recovered CML generator sessions; the 1160-entry recipe remains unresolved. The replay swaps reproduce the exact target and do not claim to be its original swap trace. The package retains an offline constructor and verifier, including independent upstream checks; see C2 derivation.

The 4968-symbol direct-gather order is the existing 24-by-207 walk product. SL6–8 point labels invert Gray coding on PSK phase. SL9 and SL10 use quadrant geometry, with raw signed 16-bit coordinates at scale 4096. SL10's uniform three-decimal coordinate rounding is an inferred compatibility rule. Complete coordinate and symbol hashes pin label order, axes, dtype, rounding and scaling. None of these geometric generators run the stochastic CML construction during modem startup.

Shipped binaryCategoryHow known
c2-280.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-348.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-352.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-704.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-1056.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-1160.swaps.zReplay swapsExact target, original CML seed/session unresolved; operator-approved runtime representation, resource and canonical hashes pinned
c2-1408.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-1656.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-4140.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-8280.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-12420.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-16560.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned
c2-20700.swaps.zReplay swapsExact recovered CML session; offline proof command retained; operator-approved runtime representation, resource and canonical hashes pinned