A dependency-free JavaScript port of VOACAP, the Voice of America / NTIA-ITS HF propagation predictor (IONCAP lineage). It runs in node and in the browser, and it is bit-identical to the original Fortran: the full COMMON-block state matches exactly (tolerance 0) at every checkpoint, not just the printed output.
Live: https://wu6p.github.io/VOA/web/ (GitHub Pages, the same static page).
./start # serves the page at http://localhost:8700/web/ (./stop to stop)The page has two tabs:
- Point to point: a reliability-by-hour heatmap, the exact
voacapx.outVOACAP prints, and the input deck, so you can re-run it in real VOACAP. - Area map: an azimuthal-equidistant coverage map of REL, SNR, field strength, signal, MUF or SNRxx around the transmitter.
For hams:
- A station can be typed as a Maidenhead grid (
CM87xi), a callsign (JA1XYZ,KH6LC,VE7CC; hamcore's DXCC resolution, with US/Canadian call-area centres) orlat, lon. - Mode presets set the required SNR, with its basis shown:
- CW 38 dB-Hz, fitted to RBN skimmer spots in
validate/; - SSB 48 dB-Hz;
- FT8 13 dB-Hz, WSJT-X's −21 dB in 2.5 kHz.
- CW 38 dB-Hz, fitted to RBN skimmer spots in
- Band chips put a frequency in that mode's segment of each band.
- The area map can run all 24 hours and play them as an animation.
All computation happens in a Web Worker in the page; the server only hands out files.
npm run single # -> dist/voacap.html (3.7 MB)This builds the whole page as one HTML file: the engine, coefficients,
antennas, coastlines and callsign tables all inline. It opens straight from
file:// (a phone's Files app, a laptop on a plane) with no server and no
network. The engine runs in a Blob worker, or in the page if the browser
refuses one. tests/single_build.test.mjs runs the bundled engine against
the golden Fortran outputs, and node tools/web_check.mjs --url file://$PWD/dist/voacap.html drives the page itself.
import { runDeck, runArea } from './engine/voacap.js';
import { loadData } from './engine/data_node.js';
const { out } = runDeck(deckText, { data: loadData() }); // voacapx.out text
const { vg } = runArea(voaText, 'my.voa', { data: loadData() }); // .vg1 … grid files- Point to point:
- all 30 METHODs;
- OUTGRAPH extra plots;
- all antenna types (CCIR Rec.705 0–12, gain tables 13/14, IONCAP 21–30, HFMUFES/ITS-78 31–47 with complex arithmetic, NOSC 48).
- Area coverage (
voacapl … area calc) and inverse area (inv calc). This covers AREAMAP's.voa→ deck conversion (including the ICEPAC, REC533 and map-only deck formats andareafreq.dat), the 360°×91 area antenna tables, the grid geometry, the.vgNoutput and batch area runs (voaareaw.cir,runAreaBatch). - Also:
- daily URSI maps (
MONTHwith a day); - the 2–30 MHz sweep;
- distance and time plots (
.out/.idx/.dst); - batch point to point (
runBatch, B and S); - absorption modes;
- S/I output (METHOD > 200, the
.sifile); - CIRAF-zone test points (
ZONES).
- daily URSI maps (
- Not ported: Harris antennas (≥ 90), which run an external program voacapl doesn't ship. ICEPAC and REC533 themselves aren't in voacapl either: there, those area models only change the deck format.
npm test # ~15 s, no Fortran neededtests/golden/golden.json.gz (1.8 MB) holds 128 inputs and what the Fortran
produced for them:
- 98 point-to-point decks (every METHOD, every shipped antenna, perturbed and synthesised antennas, absorption modes, S/I, ZONES);
- 17 area runs, 6 distance/time plots, 4 batch runs and 3 batch area runs.
The tests compare every output file byte for byte. For 57 of the cases they
also compare a SHA-256 digest of the full COMMON-block state at every
checkpoint. Changing one constant in the noise model by 1 part in 10^8
fails 17 of them. npm run golden (tools/make_golden.py) regenerates the
file from the oracles in ref/.
The reference is jawatson/voacapl @ c12a98b (in ref/, untracked;
tools/make_oracles.sh builds everything).
- Oracles.
r4is the program as shipped.r8is the float64 build.r8v/dbgvare the r8 build linked against V8's own fdlibm (ref/v8math), sosin,log,exp… return the same bits as node. - State dumps.
tools/commons.pyinventories every COMMON block. From that one inventory, a generated FortranDUMPSTand the JSengine/state.jsdump identical state at the same checkpoints. - Differential fuzz on random inputs:
tools/fuzz.py: point to point;--statefor exact full-state comparison,--richfor varied hours, months, OUTGRAPH and second runs,--rand-antsfor perturbed antenna files.tools/fuzz_area.py: area and inverse area, the same options.tools/fuzz_plot.py,tools/fuzz_batch.py,tools/fuzz_ciraf.py: distance/time plots, batch runs, and S/I + CIRAF + area models + area batch.
- Unit tools.
tools/antdrvruns one antenna model in both languages, andtools/constcheck.pychecks every numeric literal against the Fortran.
Vs the VOACAP you download: that build computes in REAL*4, this port in
float64. On 1,000 random circuits, 99.74% of SNR cells print the same and
99.98% are within 1 dB. The rest is a different ray in 21 of 219,072 cells,
or SNRs far below −250 dB. Details: DRIFT.md (tools/drift.py).
NOTES.md lists every trap found on the way: ~50 of them, including upstream
bugs that are deliberately reproduced, such as uninitialised locals,
continuation-line constants and integer-literal typos.
MIT for this port (LICENSE). VOACAP/IONCAP are US-Government works (not
copyrighted); voacapl's modifications are CC0. Natural Earth land data is
public domain.