ionotrace

High-performance ionospheric ray tracing engine. Simulate HF radio wave propagation through the Earth's ionosphere using Rust, Python, or WebAssembly.

crates.io PyPI docs.rs License
cargo add ionotrace
pip install ionotrace

Features

📡

Full 3D Ray Tracing

Hamilton's equations in spherical or ECEF coordinates with RK4 / Adams-Moulton adaptive integration.

Blazing Fast

Zero-allocation inner loops. Fan sweeps parallelize automatically via Rayon on multi-core systems.

🧲

Magnetic Field

Dipole, Constant, Cubic, and full IGRF-14 degree-13 spherical harmonics.

🌍

Earth Models

Spherical or WGS-84 spheroid. ECEF integration eliminates polar singularities.

🎯

Target Solver

Find launch angles to hit any geographic target using Nelder-Mead optimization with multi-hop support.

🐍

Python Bindings

pip-installable package via PyO3. Full API access with Rust performance.

Quick Start

Trace a 10 MHz ray at 20° elevation with default ionospheric parameters:

use ionotrace::TraceConfig;

let result = TraceConfig::new(10.0, 20.0).trace().unwrap();

println!("Max height:   {:.1} km", result.max_height);
println!("Ground range: {:.1} km", result.ground_range_km);
println!("Returned:     {}", result.returned_to_ground);
import ionotrace

result = ionotrace.TraceConfig(10.0, 20.0).trace()

print(f"Max height:   {result.max_height:.1f} km")
print(f"Ground range: {result.ground_range_km:.1f} km")
print(f"Returned:     {result.returned_to_ground}")

Single Ray Trace

Configure a ray with custom physics parameters using the builder pattern (Rust) or keyword arguments (Python). Every parameter has a sensible default.

use ionotrace::{TraceConfig, ModelParams};
use ionotrace::params::{ElectronDensityModel, MagneticFieldModel};

let params = ModelParams::builder()
    .ed_model(ElectronDensityModel::DualChapman)
    .mag_model(MagneticFieldModel::Igrf14)
    .fc(8.0)    // critical frequency (MHz)
    .hm(300.0)  // peak height (km)
    .build()
    .unwrap();

let mut config = TraceConfig::new(15.0, 30.0);
config.params = params;
config.azimuth_deg = 45.0;

let result = config.trace().unwrap();
println!("Max height: {:.1} km", result.max_height);

for pt in &result.points {
    println!("  h={:.0}km  lat={:.2}°  lon={:.2}°", pt.height_km, pt.lat_deg, pt.lon_deg);
}
import ionotrace

params = ionotrace.ModelParams(
    ed_model=ionotrace.ElectronDensityModel.DualChapman,
    mag_model=ionotrace.MagneticFieldModel.Igrf14,
    fc=8.0,    # critical frequency (MHz)
    hm=300.0,  # peak height (km)
)

config = ionotrace.TraceConfig(15.0, 30.0, azimuth_deg=45.0, params=params)
result = config.trace()
print(f"Max height: {result.max_height:.1f} km")

for pt in result.points:
    print(f"  h={pt.height_km:.0f}km  lat={pt.lat_deg:.2f}°  lon={pt.lon_deg:.2f}°")

TraceConfig Fields

Field Type Default Description
freq_mhz f64 Transmit frequency in MHz
elevation_deg f64 Launch elevation angle (degrees)
azimuth_deg f64 0.0 Launch azimuth (degrees from N)
tx_lat_deg f64 40.0 Transmitter latitude
step_size f64 5.0 Integration step size (km)
max_steps usize 500 Maximum integration steps
params ModelParams defaults Physics model configuration

Fan Sweep

Trace a fan of rays across a range of elevation angles. On native multi-core systems, rays are traced in parallel via Rayon automatically.

use ionotrace::{fan_trace, FanTraceConfig};

let config = FanTraceConfig {
    freq_mhz: 10.0,
    elev_min: 5.0,
    elev_max: 80.0,
    elev_step: 2.0,
    ..FanTraceConfig::default()
};

let result = fan_trace(&config).unwrap();
println!("Traced {} rays", result.n_rays);

for ray in &result.rays {
    if ray.ground {
        println!("  {:.0}° → {:.0} km (max h: {:.0} km)",
            ray.elev, ray.range_km, ray.max_h);
    }
}
import ionotrace

config = ionotrace.FanTraceConfig(
    freq_mhz=10.0,
    elev_min=5.0,
    elev_max=80.0,
    elev_step=2.0,
)

result = ionotrace.fan_trace(config)
print(f"Traced {result.n_rays} rays")

for ray in result.rays:
    if ray.ground:
        print(f"  {ray.elev:.0f}° → {ray.range_km:.0f} km (max h: {ray.max_h:.0f} km)")

Target Solver

Find the launch elevation and azimuth to hit a specific geographic target. Uses coarse fan scans followed by Nelder-Mead simplex optimization. Supports multi-hop propagation and frequency search.

use ionotrace::{solve_target, TargetConfig, SearchSpec};

let config = TargetConfig {
    target_lat_deg: 50.0,
    target_lon_deg: 5.0,
    tx_lat_deg: 40.0,
    freq_mhz: SearchSpec::Fixed(10.0),
    error_limit_km: 20.0,
    ..TargetConfig::default()
};

let result = solve_target(&config).unwrap();
println!("Traced {} rays in {:.0} ms", result.rays_traced, result.elapsed_ms);

if let Some(best) = &result.best {
    println!("Best solution:");
    println!("  Elevation: {:.1}°", best.elevation_deg);
    println!("  Azimuth:   {:.1}°", best.azimuth_deg);
    println!("  Error:     {:.1} km", best.error_km);
    println!("  Hops:      {}", best.hops);
}
import ionotrace

config = ionotrace.TargetConfig(
    target_lat_deg=50.0,
    target_lon_deg=5.0,
    tx_lat_deg=40.0,
    freq_mhz=10.0,
    error_limit_km=20.0,
)

result = ionotrace.solve_target(config)
print(f"Traced {result.rays_traced} rays in {result.elapsed_ms:.0f} ms")

if result.best:
    print("Best solution:")
    print(f"  Elevation: {result.best.elevation_deg:.1f}°")
    print(f"  Azimuth:   {result.best.azimuth_deg:.1f}°")
    print(f"  Error:     {result.best.error_km:.1f} km")
    print(f"  Hops:      {result.best.hops}")

Physics Models

Every model component can be swapped independently via ModelParams. The engine is modular — combine any electron density profile with any magnetic field model.

Component Options
Electron Density Chapman · ELECT1 · Linear · Quasi-Parabolic · Variable Chapman · Dual Chapman
Magnetic Field Dipole · Constant · Cubic · IGRF-14
Refractive Index Full Appleton-Hartree · No Field · No Collisions · Simplified
Collisions Double-Exponential · Single-Exponential · Constant
Perturbations Torus · Trough · Shock · Bulge · Exponential · None
Earth Model Sphere (R = 6370 km) · WGS-84 Spheroid
Coordinates Spherical (r, θ, φ) · ECEF Cartesian (x, y, z)

Key ModelParams Fields

Field Default Description
fc 10.0 Critical frequency foF2 (MHz)
hm 250.0 Peak height hmF2 (km)
sh 100.0 Scale height (km)
fh 0.8 Gyrofrequency (MHz)
epoch_year 2025.0 IGRF epoch year
ed_model Chapman Electron density profile
mag_model Dipole Magnetic field model
earth_model Sphere Earth shape model

Exporting Results

Save trace results to CSV or JSON for analysis in other tools.

use ionotrace::{TraceConfig, export_trace_csv, export_json};

let result = TraceConfig::new(10.0, 20.0).trace().unwrap();

let csv = export_trace_csv(&result).unwrap();
std::fs::write("trace.csv", &csv).unwrap();

let json = export_json(&result).unwrap();
std::fs::write("trace.json", &json).unwrap();
import ionotrace

result = ionotrace.TraceConfig(10.0, 20.0).trace()

csv_str = result.to_csv()
with open("trace.csv", "w") as f:
    f.write(csv_str)

json_str = result.to_json()
with open("trace.json", "w") as f:
    f.write(json_str)

Building from Source

Rust

cargo add ionotrace

# Or build from source
git clone https://github.com/andrewwetzel/raytracing.git
cd raytracing/packages/raytrace_core
cargo build --release
cargo test

Python

pip install ionotrace

# Or build from source (requires Rust toolchain)
git clone https://github.com/andrewwetzel/raytracing.git
cd raytracing/packages/ionotrace-python
pip install maturin
maturin develop --release

WebAssembly

cd packages/raytrace_core
wasm-pack build --target web --out-dir ../../apps/frontend/pkg

Algorithm

Solves Hamilton's equations H = ½(c²k²/ω² − n²) where n² is the complex refractive index from the Appleton-Hartree formula. Integration uses 4th-order Runge-Kutta with Adams-Moulton predictor-corrector and adaptive step-size control.

Based on: A Versatile Three-Dimensional Ray Tracing Computer Program for Radio Waves in the Ionosphere, R. M. Jones & J. J. Stephenson, OT Report 75-76 (1975). PDF