Built for these platforms, each checked on its own:
High frequency radio signals (HF) travel long distances under differeing conditions making their reach hard to predict: it changes with the hour, the season, and the activity of the sun.
This libary offers three models to predict HF propagation:
- A faithful port of VOACAP written by the US Institute for Telecommunication Sciences
- VOACAP Corrected, a VOACAP implementation with defects fixed,
- Truecast, which runs VOACAP's physics against a more granular daily average, the effective sunspot index, a geomagnetic storm table, a corrected layer height, and a lower edge of the usable window that the original cannot give.
No dependencies: everything here is std.
cargo add hfcastTwo of the three are forms of the port, chosen with
api::Request::model. The third is a separate engine, chosen by the
request itself.
VOACAP is approximately 22,800 lines of FORTRAN 77 in 195 files. It has
783 GOTO statements. It does not use IMPLICIT NONE. Almost all of
its data moves through COMMON blocks and not through arguments.
This is that model translated into Rust, defects included, and it gives the same answer as the original to the last printed character.
The same engine with six recorded defects corrected, and nothing else
changed. src/voacap/model.rs has one method per defect, which is the
complete list of ways the two can differ.
docs/corrected.md records what each correction
moves, and says which ones have no measurement of accuracy behind them.
The third model lives in src/truecast/. It's chosen by the request
rather than by Model, because it's a second engine rather than a
variant of the port.
VOACAP predicts a monthly median, so every day of a month gets the same answer. Truecast conditions that same climatology on the day itself:
- a daily effective sunspot index, fitted from ionosonde soundings, replaces the monthly smoothed number when the caller has one
- a geomagnetic storm table widens the forecast when the measured Kp says the ionosphere is disturbed
- with no network at all the engine derives its own index for the date, from the embedded sunspot table and a fitted day-of-year correction, so a device that never goes online still beats the monthly median.
Each piece is fitted on a ~130-month ionosonde archive and judged only on eight held-out months the fits never saw. docs/comparison.md puts the two models side by side; docs/offline.md is the measured case that the offline form beats the monthly median on individual days.
If the engine copies the defects, then "the same as the original" is something you can test. If it does not, it is an opinion, and that test is what the whole method depends on. Corrections then live in one named place, where each one can be measured alone.
Each test below runs the original Fortran and this engine on the same input, and compares the output character by character.
| Test | What it compares | Result |
|---|---|---|
portcheck |
463,104 printed cells and 23,040 mode labels, over 96 paths | 0 differ |
fuzz |
600 generated inputs, 434,116 lines of output | identical |
areacheck |
749 area points and 17,791 cells | identical |
lufcheck |
1,152 rows of the lowest usable frequency table | identical |
antcheck |
each antenna type, against the gain files of the original | identical |
paritycheck |
7,104 fields the HFcast app reads | 0 differ |
archcheck |
this engine against itself on a different processor | identical |
Plus 279 unit tests and 57 harness and integration tests.
A daily job runs 200 paths through both engines with the space weather of that day. It fails if one number is different.
The port gives the same answers as VOACAP, so it is exactly as accurate as VOACAP. That is a separate question, and this repository measures it against real radio reports: VOACAP puts the good hours and the bad hours in the correct places (correlation +0.76 against measured WSPR reports), and exaggerates the difference between them by approximately four and a half times (slope +0.22). docs/accuracy.md has the measurements, including the comparison with ITU-R P.533.
Truecast is measured against ionosonde soundings, which observe the ionosphere directly where the WSPR record can only infer it. Over the eight held-out months it removes the port's month-to-month bias and improves foF2 error in seven of the eight; on storm hours it improves on the port by 0.16 and 0.58 MHz. Fully offline, with no reading of any kind, it still improves on the port in eleven of twelve years. docs/comparison.md has the tables.
| Path | What it does |
|---|---|
src/voacap/ |
The translated engine |
src/api.rs |
The public interface: a request in, a report out |
src/deck.rs |
Writes the fixed-width input file VOACAP reads |
src/listing.rs |
Reads each number back out of the output |
src/sweep.rs |
Makes input cases that cover the model's regimes |
src/fuzz.rs |
Makes valid inputs from a seed |
src/runner.rs |
Operates a chosen voacapl binary in a separate tree |
src/compare.rs |
Measures how far two outputs differ, field by field |
src/wspr.rs |
Reads collected WSPR reception reports |
src/itu.rs |
Operates the ITU-R P.533 reference implementation |
src/truecast/ |
The second engine: climatology conditioned on the day |
src/giro.rs |
Reads GIRO ionosonde soundings, the ground truth |
src/essn.rs |
Fits the daily effective sunspot index from them |
src/stormfit.rs |
The fitted geomagnetic storm table |
src/bin/ |
The tests, predict, sonde, and spacewx |
embedded/ |
The 560 KB of data the engine needs, compiled in |
There are no dependencies, on purpose because this crate faithfully replicates the reference VOACAP model
Everything is std.
You need a Rust toolchain, gfortran, and a copy of voacapl in
vendor/voacapl.
tools/build-variants.sh # builds the original at five optimisation levels
cargo test
cargo run --release --bin portcheckvoacapl needs an installed itshfbc data tree. The tests read
$HFCAST_ITSHFBC, and use ~/itshfbc if it is not set.
cargo test runs the default build, which reads the coefficients from
that tree. cargo test --all-features also runs the tests that ask for
the compiled-in copy. CI runs both.
Hooks run the checks for you: formatting and clippy before a commit, both test builds and the analysis gates before a push. Turn them on once per clone, because git does not enable a hook directory by itself:
git config core.hooksPath .githooksTo make one prediction:
echo '{"fromLat":47.6,"fromLon":-122.3,"toLat":51.5,"toLon":-0.1,
"month":8,"year":2026,"ssn":60,"watts":100,
"bands":[7.1,14.1,21.1],"requiredSnrDb":24,"noiseDbw":-145}' |
cargo run --release --bin predictEach field above is necessary. predict reads the data tree named in
the request, or $HFCAST_ITSHFBC, or ~/itshfbc. A build with
--features embedded-coefficients accepts "itshfbc":"<embedded>" and
needs no tree.
The same request selects Truecast by swapping "ssn" for
"engine":"truecast". A live daily index is passed as "essn". With
no index at all the engine derives its own for the date — the offline
form, which also takes an optional "day" (the 15th if absent) and an
optional baked "sync" record:
echo '{"fromLat":47.6,"fromLon":-122.3,"toLat":51.5,"toLon":-0.1,
"month":8,"year":2026,"day":17,"engine":"truecast","watts":100,
"bands":[7.1,14.1,21.1],"requiredSnrDb":24,"noiseDbw":-145}' |
cargo run --release --bin predictdocs/port.md has the complete list of tests, the options each one takes, and a "Traps" section that records each way a result has been wrong here before. Read it before you trust a result.
tools/analyze.sh # clippy, complexity, duplication, coverage
tools/analyze.sh --gate # the same, but it fails on a broken gateThe script's own header says what each step is for, and the comment
above parity_allows records which clippy suggestions must never be
applied. Some of them would change the arithmetic and break the
agreement with the original.
| Document | What it covers |
|---|---|
| port.md | How the translation is proved, and the traps |
| corrected.md | Each corrected defect and what it moves |
| sensitivity.md | The measured tolerance |
| accuracy.md | VOACAP against measured radio, and against P.533 |
| reliability.md | The day-to-day spread, and storm days |
| truecast.md | The second pipeline and its contract |
| ionosonde.md | Truecast against ionosonde truth, and the fits |
| comparison.md | The two models, side by side |
| offline.md | The forecast with no network at all |
| soak.md | The recurring daily checks |
| licence.md | Where the code and the data come from |
The engine needs 560 KB of data: the ionospheric maps and noise tables for each month, the antenna files, and one version string. Where they come from decides how they are shipped.
| Part | Size | Origin | In the published crate |
|---|---|---|---|
| Antenna files, version string | 16 KB | NTIA/ITS | yes |
| Sporadic E, E, F1, prediction error | 195 KB | NTIA/ITS | no |
| Atmospheric noise | 216 KB | CCIR Report 322 | no |
| foF2 and M(3000)F2 maps | 134 KB | CCIR Report 340 | no |
The URSI-88 foF2 maps are in no build. They are the one part the ITU does
not publish itself, and nothing here selects them, so a COEFFS URSI88
card needs a real itshfbc root.
The coefficients are behind the embedded-coefficients feature, which is
off by default, and the files are excluded from the package. A build from
crates.io reads them from an itshfbc tree, which is how the reference
engine has always found them:
// From a tree on disk. No feature needed.
let answer = hfcast::service::run(r#"{"itshfbc": "/home/you/itshfbc", ...}"#)?;A build from this repository can compile them in instead, which is what the HFcast phone app does:
cargo build --features embedded-coefficientsAsking for "<embedded>" without the feature fails with a message saying
so, rather than quietly giving a wrong answer.
NOTICE records what is inside embedded/coeffs/, array by
array, and docs/licence.md records how that was
measured and what it does and does not settle. In short: ITU-R Study
Group 3 publishes the CCIR Report 322 and 340 data itself, for
implementers, free from copyright assertions, in its P.372 and P.533
reference software.
Apache-2.0. See LICENSE and NOTICE.
The translated model comes from work that is not subject to copyright protection in the United States, and from changes released under CC0. docs/licence.md records where it comes from in full, and the limits of that finding.