-
Notifications
You must be signed in to change notification settings - Fork 1
Performance Tips
Optimization strategies for speeding up DVOACAP-Python predictions.
Released: November 2025 Speedup: 2.3x faster than v1.0.0
DVOACAP-Python v1.0.1 delivers significant performance improvements through algorithmic optimizations and NumPy vectorization:
| Operation | v1.0.0 | v1.0.1 | Speedup |
|---|---|---|---|
| Single prediction | 0.008s | 0.004s | 2.0x |
| Multi-frequency (9 predictions) | 0.111s | 0.048s | 2.3x |
| 24-hour scan | 0.282s | 0.118s | 2.4x |
| Area coverage (100 predictions) | 0.82s | 0.35s | 2.3x |
| Function calls | 100% | 29-32% | 68-71% reduction |
- Binary Search for Height-to-Density - O(n) → O(log n) complexity
- Vectorized Gaussian Integration - Eliminated 40-iteration loop using NumPy
- Vectorized Oblique Frequency - Eliminated 1,200 nested iterations
- Optimized Fourier Series - Replaced loops with NumPy dot products
See CHANGELOG.md for full v1.0.1 release notes.
- v1.0.1 Performance Improvements
- Understanding Performance
- Quick Wins
- Configuration Optimization
- Code-Level Optimization
- Caching Strategies
- Parallel Processing
- Profiling
- Future Improvements
Typical prediction breakdown:
| Phase | Time % | Operation |
|---|---|---|
| Phase 3: Ionospheric Profiles | ~35% | CCIR coefficient processing, electron density profiles |
| Phase 4: Raytracing | ~40% | Iterative path finding, MUF calculations |
| Phase 5: Signal Predictions | ~20% | Path loss, SNR, reliability calculations |
| Phase 1-2: Geometry/Solar | ~5% | Path calculations, solar zenith angles |
Bottleneck: Raytracing (Phase 4) is the most computationally intensive.
Single prediction (1 frequency, 1 path):
- Fast system (modern CPU): ~4 ms (v1.0.1 optimized - 2.3x faster than v1.0.0)
- Average system: ~10-20 ms
- Slow system (Raspberry Pi): ~50-100 ms
Full dashboard generation (10 regions × 7 bands × 12 hours):
- Fast system: ~20-30 seconds (v1.0.1 optimized - 2.3x faster than v1.0.0)
- Average system: ~30-45 seconds
- Slow system: ~1-2 minutes
Note: See the v1.0.1 Performance Improvements section above for detailed benchmark data and optimization details.
❌ Slow (creates new engine every time):
for region in regions:
engine = PredictionEngine() # DON'T DO THIS
engine.predict(...)✅ Fast (reuse engine):
engine = PredictionEngine() # Create once
for region in regions:
engine.predict(...) # Reuse for all predictionsSpeedup: ~20-30% faster (avoids re-loading CCIR maps)
❌ Slow (separate predictions):
for freq in [7.0, 14.0, 21.0]:
engine.predict(rx_location, utc_time, frequencies=[freq])✅ Fast (batch frequencies):
engine.predict(rx_location, utc_time, frequencies=[7.0, 14.0, 21.0])Speedup: ~3x faster (path geometry computed once)
Dashboard optimization:
# Instead of 10 regions
TARGET_REGIONS = {
'EU': ...,
'JA': ...,
'VK': ...,
# Remove regions you don't work
}
# Instead of 7 bands
BANDS = {
'40m': 7.150,
'20m': 14.150,
'15m': 21.200,
# Remove bands you don't use
}
# Instead of 12 time points, use 8
utc_hours = [0, 3, 6, 9, 12, 15, 18, 21] # Every 3 hours instead of 2Speedup: Proportional to reduction (50% fewer predictions = 50% faster)
Avoid extreme values:
# ✅ Good - typical values
engine.params.ssn = 100.0 # Moderate solar cycle
# ❌ Slow - extreme values take longer to converge
engine.params.ssn = 300.0 # Very high (edge case)For dashboard/batch processing:
# Default: High accuracy, slower
engine.params.min_angle = np.deg2rad(3.0)
# Faster: Slightly lower accuracy
engine.params.min_angle = np.deg2rad(5.0)Speedup: ~10-15% faster Impact: Minimal difference in results for typical amateur radio use
Tune iteration limits (advanced):
# In muf_calculator.py (internal settings)
MAX_ITERATIONS = 50 # Default: 100 (faster but less precise)
CONVERGENCE_TOLERANCE = 1e-3 # Default: 1e-4 (looser tolerance)Speedup: ~15-20% faster Impact: May affect edge case accuracy
Disable features you don't need:
# If you only need MUF (not full signal predictions)
# Use muf_calculator directly instead of full PredictionEngine
from dvoacap.muf_calculator import MufCalculator
calc = MufCalculator()
# ... configure and run MUF calculation only
# Much faster than full prediction❌ Slow (Python loops):
results = []
for i in range(len(frequencies)):
result = calculate_loss(frequencies[i])
results.append(result)✅ Fast (NumPy vectorization):
import numpy as np
results = calculate_loss(np.array(frequencies)) # VectorizedSpeedup: 10-100x for large arrays
Cache expensive operations:
class OptimizedEngine:
def __init__(self):
self._solar_cache = {}
def get_solar_params(self, month, ssn, utc_time):
"""Cache solar calculations"""
cache_key = (month, ssn, utc_time)
if cache_key not in self._solar_cache:
self._solar_cache[cache_key] = compute_solar_params(month, ssn, utc_time)
return self._solar_cache[cache_key]❌ Slow:
for i in range(1000):
point = GeoPoint.from_degrees(lat, lon) # Creates new object each time
# ...✅ Fast:
point = GeoPoint.from_degrees(lat, lon) # Create once
for i in range(1000):
# Reuse point
# ...Cache predictions for identical inputs:
import hashlib
import json
import pickle
from pathlib import Path
class CachedPredictionEngine:
"""Prediction engine with result caching"""
def __init__(self, cache_dir='cache'):
self.engine = PredictionEngine()
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def _cache_key(self, rx_location, frequencies, month, ssn, utc_time):
"""Generate cache key from parameters"""
rx_lat, rx_lon = rx_location.to_degrees()
params = {
'rx_lat': round(rx_lat, 4),
'rx_lon': round(rx_lon, 4),
'frequencies': frequencies,
'month': month,
'ssn': round(ssn, 1),
'utc_time': round(utc_time, 2)
}
key_str = json.dumps(params, sort_keys=True)
return hashlib.md5(key_str.encode()).hexdigest()
def predict(self, rx_location, frequencies, month, ssn, utc_time):
"""Predict with caching"""
cache_key = self._cache_key(rx_location, frequencies, month, ssn, utc_time)
cache_file = self.cache_dir / f"{cache_key}.pkl"
# Check cache
if cache_file.exists():
with open(cache_file, 'rb') as f:
return pickle.load(f)
# Run prediction
self.engine.params.month = month
self.engine.params.ssn = ssn
self.engine.predict(
rx_location=rx_location,
utc_time=utc_time / 24.0,
frequencies=frequencies
)
# Cache results
result = {
'muf': self.engine.muf_calculator.muf,
'predictions': self.engine.predictions
}
with open(cache_file, 'wb') as f:
pickle.dump(result, f)
return result
# Usage
engine = CachedPredictionEngine(cache_dir='prediction_cache')
result = engine.predict(...) # First call: runs prediction
result = engine.predict(...) # Second call: instant (from cache)Speedup: ~∞ for cached results (instant retrieval)
CCIR maps are automatically cached after first load. Don't reload unnecessarily:
# ✅ Good - maps loaded once
maps = FourierMaps()
for month in range(1, 13):
maps.set_conditions(month=month, ssn=100, utc_fraction=0.5)
# Maps are reused
# ❌ Bad - reloads maps each time
for month in range(1, 13):
maps = FourierMaps() # DON'T DO THIS
maps.set_conditions(month=month, ssn=100, utc_fraction=0.5)Process regions in parallel:
from multiprocessing import Pool
from dvoacap import PredictionEngine
from dvoacap.path_geometry import GeoPoint
def predict_region(args):
"""Worker function for parallel processing"""
region_name, region_location, frequencies, month, ssn, utc_time = args
# Each process gets its own engine
engine = PredictionEngine()
engine.params.month = month
engine.params.ssn = ssn
engine.params.tx_location = GeoPoint.from_degrees(44.374, -64.300)
engine.predict(
rx_location=region_location,
utc_time=utc_time / 24.0,
frequencies=frequencies
)
return {
'region': region_name,
'muf': engine.muf_calculator.muf,
'predictions': engine.predictions
}
# Parallel execution
if __name__ == '__main__':
regions = {
'EU': GeoPoint.from_degrees(50.0, 10.0),
'JA': GeoPoint.from_degrees(36.0, 138.0),
'VK': GeoPoint.from_degrees(-33.87, 151.21),
# ... more regions
}
tasks = [
(name, location, [7.0, 14.0, 21.0], 6, 100, 12)
for name, location in regions.items()
]
# Use 4 processes
with Pool(processes=4) as pool:
results = pool.map(predict_region, tasks)
for result in results:
print(f"{result['region']}: MUF {result['muf']:.2f} MHz")Speedup: ~4x with 4 cores (linear scaling up to number of cores)
Note: Use if __name__ == '__main__': guard on Windows!
Use asyncio for concurrent operations:
import asyncio
from dvoacap import PredictionEngine
async def async_predict(region, location, frequencies):
"""Async wrapper for prediction"""
loop = asyncio.get_event_loop()
# Run prediction in thread pool
result = await loop.run_in_executor(
None,
run_prediction,
region, location, frequencies
)
return result
def run_prediction(region, location, frequencies):
"""Synchronous prediction function"""
engine = PredictionEngine()
# ... configure and run prediction
return result
# Run predictions concurrently
async def main():
tasks = [
async_predict('EU', eu_location, [14.0]),
async_predict('JA', ja_location, [14.0]),
async_predict('VK', vk_location, [14.0]),
]
results = await asyncio.gather(*tasks)
return results
# Execute
results = asyncio.run(main())Use cProfile to find slow code:
python3 -m cProfile -o profile.stats generate_predictions.pyAnalyze results:
import pstats
stats = pstats.Stats('profile.stats')
stats.sort_stats('cumulative')
stats.print_stats(20) # Top 20 slowest functionsUse line_profiler for detailed analysis:
# Install
pip install line_profiler
# Add @profile decorator to function
@profile
def my_slow_function():
# ...
# Run profiler
kernprof -l -v my_script.pyCheck memory usage:
pip install memory_profiler
# Run
python3 -m memory_profiler generate_predictions.pyIn Development:
- Numba JIT compilation - Compile hot paths to native code
- Cython modules - Rewrite critical modules in Cython
- Lookup tables - Pre-computed values for common scenarios
- Parallel raytracing - Parallel ray finding for multiple frequencies
Expected speedup: 3-10x for full implementation
Add Numba JIT to hot functions:
from numba import jit
import numpy as np
@jit(nopython=True)
def fast_path_loss_calculation(frequency, distance, layer_params):
"""JIT-compiled path loss calculation"""
# ... computation
return loss
# First call: compilation overhead (~1 second)
result = fast_path_loss_calculation(14.0, 5000, params)
# Subsequent calls: very fast (10-100x speedup)
result = fast_path_loss_calculation(21.0, 5000, params)Status: Experimental (not yet integrated into main codebase)
| Optimization | Speedup | Effort | Notes |
|---|---|---|---|
| Reuse engine instances | 1.3x | Easy | Always do this |
| Batch frequencies | 3x | Easy | Always do this |
| Reduce predictions | Variable | Easy | Trade-off with coverage |
| Caching results | ∞ | Medium | Best for repeated queries |
| Multi-processing | 4x | Medium | For batch jobs |
| Numba/Cython | 10x | Hard | Future work |
Before (slow):
for region in regions:
for band in bands:
for hour in range(24):
engine = PredictionEngine() # ❌ Recreates each time
engine.predict(region, [band], hour)After (fast):
engine = PredictionEngine() # ✅ Create once
# Batch by region
for region in regions:
# Batch frequencies
all_bands = list(bands.values())
# Batch time points
for hour in [0, 3, 6, 9, 12, 15, 18, 21]: # Every 3 hours
engine.predict(region, all_bands, hour)Speedup: ~10-15x faster
from concurrent.futures import ProcessPoolExecutor
from functools import partial
def optimized_multi_region_predict(regions, bands, hours, month, ssn):
"""Optimized multi-region prediction"""
def predict_one(region_name, region_location):
"""Predict for one region (runs in separate process)"""
engine = PredictionEngine()
engine.params.month = month
engine.params.ssn = ssn
engine.params.tx_location = tx_location
results = []
for hour in hours:
engine.predict(
rx_location=region_location,
utc_time=hour / 24.0,
frequencies=bands
)
results.append({
'hour': hour,
'muf': engine.muf_calculator.muf,
'predictions': engine.predictions
})
return region_name, results
# Run in parallel
with ProcessPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(predict_one, name, location): name
for name, location in regions.items()
}
all_results = {}
for future in futures:
region_name, results = future.result()
all_results[region_name] = results
return all_results
# Usage
results = optimized_multi_region_predict(
regions=TARGET_REGIONS,
bands=[7.0, 14.0, 21.0],
hours=[0, 6, 12, 18],
month=6,
ssn=100
)Speedup: ~4x with 4 cores + batching optimizations
import time
from functools import wraps
def timing_decorator(func):
"""Decorator to measure function execution time"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.2f} seconds")
return result
return wrapper
@timing_decorator
def generate_predictions():
# ... prediction code
pass
# Output: generate_predictions took 45.23 seconds- Integration Guide - Build optimized applications
- Dashboard Guide - Optimize dashboard performance
- Known Issues - Performance limitations
- Development Setup - Set up profiling tools
Tip: Start with the quick wins (reuse engines, batch frequencies) for immediate 3-5x speedup with minimal effort!