High-performance ionospheric ray tracing engine. Simulate HF radio wave propagation through the Earth's ionosphere using Rust, Python, or WebAssembly.
Hamilton's equations in spherical or ECEF coordinates with RK4 / Adams-Moulton adaptive integration.
Zero-allocation inner loops. Fan sweeps parallelize automatically via Rayon on multi-core systems.
Dipole, Constant, Cubic, and full IGRF-14 degree-13 spherical harmonics.
Spherical or WGS-84 spheroid. ECEF integration eliminates polar singularities.
Find launch angles to hit any geographic target using Nelder-Mead optimization with multi-hop support.
pip-installable package via PyO3. Full API access with Rust performance.
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}")
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}°")
| 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 |
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)")
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}")
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) |
| 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 |
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)
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
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
cd packages/raytrace_core
wasm-pack build --target web --out-dir ../../apps/frontend/pkg
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