Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 21 additions & 156 deletions examples/ham_radio_viewshed_analysis.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -537,120 +537,35 @@
"cell_type": "markdown",
"id": "cpu-viewshed-header",
"metadata": {},
"source": [
"## 6. CPU Viewshed Analysis\n",
"\n",
"Let's compute viewsheds for each repeater using the CPU. We'll time the computation to compare with GPU later.\n",
"\n",
"Note that we pass the x, y coordinates directly in EPSG:5070 meters - no pixel coordinate conversion needed!"
]
"source": "## 6. Viewshed Configuration\n\nSet up the parameters and helper functions used by both the CPU and GPU viewshed cells below. Run this cell first, then you can run either the CPU or GPU cell independently."
},
{
"cell_type": "code",
"id": "a3has29un3j",
"source": "# Typical antenna heights\nOBSERVER_HEIGHT = 30 # meters - typical repeater tower/building\nTARGET_HEIGHT = 1.5 # meters - handheld radio user height\n\n# Constants for viewshed interpretation\nINVISIBLE = -1\nVISIBLE = 1\n\n# Number of repeaters to analyze\nmax_repeaters = min(5, len(repeaters))\nselected_repeaters = repeaters.head(max_repeaters).copy()\n\ndef to_binary_visibility(vs):\n \"\"\"Convert viewshed result to binary (VISIBLE/INVISIBLE), handling CuPy arrays.\"\"\"\n data = vs.data\n\n # Convert CuPy to numpy if needed\n if hasattr(data, 'get'): # CuPy array\n data = data.get()\n\n # Create binary visibility: -1 stays -1, everything else becomes 1\n binary = np.where(data == INVISIBLE, INVISIBLE, VISIBLE).astype(np.float32)\n\n result = vs.copy()\n result.data = binary\n return result\n\nprint(f\"Viewshed Configuration:\")\nprint(f\" Observer height: {OBSERVER_HEIGHT}m\")\nprint(f\" Target height: {TARGET_HEIGHT}m\")\nprint(f\" Repeaters to analyze: {max_repeaters}\")\nprint(f\" Selected: {', '.join(selected_repeaters['Callsign'].values)}\")",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "rbojch5pd9o",
"source": "## 7. CPU Viewshed Analysis\n\nCompute viewsheds for each repeater using the CPU. We'll time the computation to compare with GPU later.\n\nNote that we pass the x, y coordinates directly in EPSG:5070 meters - no pixel coordinate conversion needed!",
"metadata": {}
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "cpu-viewshed",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Computing CPU viewsheds for 5 repeaters...\n",
"Observer height: 30m, Target height: 1.5m\n",
"\n",
" W0TLM: 12.30s - 18.5% coverage\n",
" N0VOF: 5.26s - 8.8% coverage\n",
" W0CDS: 6.50s - 16.2% coverage\n",
" K0MTN: 11.92s - 31.5% coverage\n",
" N0DN: 4.05s - 6.8% coverage\n",
"\n",
"Total CPU time: 40.03s\n",
"Average per viewshed: 8.01s\n"
]
}
],
"source": [
"# Typical antenna heights\n",
"OBSERVER_HEIGHT = 30 # meters - typical repeater tower/building\n",
"TARGET_HEIGHT = 1.5 # meters - handheld radio user height\n",
"\n",
"# Constants for viewshed interpretation\n",
"INVISIBLE = -1\n",
"VISIBLE = 1\n",
"\n",
"def to_binary_visibility(vs):\n",
" \"\"\"Convert viewshed result to binary (VISIBLE/INVISIBLE), handling CuPy arrays.\"\"\"\n",
" # Access underlying data array (not .values which forces numpy conversion)\n",
" data = vs.data\n",
" \n",
" # Convert CuPy to numpy if needed\n",
" if hasattr(data, 'get'): # CuPy array\n",
" data = data.get()\n",
" \n",
" # Create binary visibility: -1 stays -1, everything else becomes 1\n",
" binary = np.where(data == INVISIBLE, INVISIBLE, VISIBLE).astype(np.float32)\n",
" \n",
" # Return as xarray DataArray with numpy data\n",
" result = vs.copy()\n",
" result.data = binary\n",
" return result\n",
"\n",
"# Compute viewshed for each repeater (CPU)\n",
"viewsheds_cpu = []\n",
"cpu_times = []\n",
"\n",
"# Limit to first few repeaters for demonstration\n",
"max_repeaters = min(5, len(repeaters))\n",
"selected_repeaters = repeaters.head(max_repeaters).copy()\n",
"\n",
"print(f\"Computing CPU viewsheds for {max_repeaters} repeaters...\")\n",
"print(f\"Observer height: {OBSERVER_HEIGHT}m, Target height: {TARGET_HEIGHT}m\")\n",
"print()\n",
"\n",
"for idx, row in selected_repeaters.iterrows():\n",
" callsign = row.get('Callsign', f'Repeater {idx}')\n",
" \n",
" # Use data-space coordinates directly (EPSG:5070 meters)\n",
" viewpoint_x = row['x_5070']\n",
" viewpoint_y = row['y_5070']\n",
" \n",
" start_time = time.time()\n",
" vs = viewshed(\n",
" dem,\n",
" x=viewpoint_x,\n",
" y=viewpoint_y,\n",
" observer_elev=OBSERVER_HEIGHT,\n",
" target_elev=TARGET_HEIGHT\n",
" )\n",
" elapsed = time.time() - start_time\n",
" cpu_times.append(elapsed)\n",
" \n",
" # Convert to binary visibility\n",
" vs_binary = to_binary_visibility(vs)\n",
" vs_binary.name = callsign\n",
" viewsheds_cpu.append(vs_binary)\n",
" \n",
" visible_cells = int((vs_binary.data == VISIBLE).sum())\n",
" total_cells = vs_binary.size\n",
" coverage_pct = 100 * visible_cells / total_cells\n",
" \n",
" print(f\" {callsign}: {elapsed:.2f}s - {coverage_pct:.1f}% coverage\")\n",
"\n",
"print(f\"\\nTotal CPU time: {sum(cpu_times):.2f}s\")\n",
"print(f\"Average per viewshed: {np.mean(cpu_times):.2f}s\")"
]
"outputs": [],
"source": "# Compute viewshed for each repeater (CPU)\nviewsheds_cpu = []\ncpu_times = []\n\nprint(f\"Computing CPU viewsheds for {max_repeaters} repeaters...\")\nprint(f\"Observer height: {OBSERVER_HEIGHT}m, Target height: {TARGET_HEIGHT}m\")\nprint()\n\nfor idx, row in selected_repeaters.iterrows():\n callsign = row.get('Callsign', f'Repeater {idx}')\n \n # Use data-space coordinates directly (EPSG:5070 meters)\n viewpoint_x = row['x_5070']\n viewpoint_y = row['y_5070']\n \n start_time = time.time()\n vs = viewshed(\n dem,\n x=viewpoint_x,\n y=viewpoint_y,\n observer_elev=OBSERVER_HEIGHT,\n target_elev=TARGET_HEIGHT\n )\n elapsed = time.time() - start_time\n cpu_times.append(elapsed)\n \n # Convert to binary visibility\n vs_binary = to_binary_visibility(vs)\n vs_binary.name = callsign\n viewsheds_cpu.append(vs_binary)\n \n visible_cells = int((vs_binary.data == VISIBLE).sum())\n total_cells = vs_binary.size\n coverage_pct = 100 * visible_cells / total_cells\n \n print(f\" {callsign}: {elapsed:.2f}s - {coverage_pct:.1f}% coverage\")\n\nprint(f\"\\nTotal CPU time: {sum(cpu_times):.2f}s\")\nprint(f\"Average per viewshed: {np.mean(cpu_times):.2f}s\")"
},
{
"cell_type": "markdown",
"id": "gpu-viewshed-header",
"metadata": {},
"source": [
"## 7. GPU-Accelerated Viewshed with rtxpy\n",
"\n",
"When CuPy is available, xarray-spatial automatically uses GPU acceleration via rtxpy for viewshed calculations. The API is identical - you just need to provide data as a CuPy array.\n",
"\n",
"This can provide **100-300x speedup** for large rasters!"
]
"source": "## 8. GPU-Accelerated Viewshed with rtxpy\n\nWhen CuPy is available, xarray-spatial automatically uses GPU acceleration via rtxpy for viewshed calculations. The API is identical - you just need to provide data as a CuPy array.\n\nThis can provide **100-300x speedup** for large rasters!\n\n**Note:** You can run this cell independently of the CPU cell above - just make sure you've run the **Viewshed Configuration** cell (Section 6) first."
},
{
"cell_type": "code",
Expand Down Expand Up @@ -1747,57 +1662,7 @@
"cell_type": "markdown",
"id": "conclusion",
"metadata": {},
"source": [
"## 12. Conclusion\n",
"\n",
"### What We Accomplished\n",
"\n",
"In this notebook, we:\n",
"\n",
"1. **Downloaded real terrain data** using the SRTM 30m dataset via the `elevation` package\n",
"2. **Reprojected to EPSG:5070** (Albers Equal Area) for accurate distance/area calculations\n",
"3. **Retrieved actual repeater locations** from the RepeaterBook API and transformed coordinates\n",
"4. **Computed viewsheds** using data-space coordinates (meters) directly - no pixel conversion needed\n",
"5. **Demonstrated GPU acceleration** with xarray-spatial and rtxpy (73x speedup!)\n",
"6. **Visualized coverage** with hillshade overlays and combined analysis\n",
"7. **Simulated a lost hiker rescue scenario** using:\n",
" - `xrspatial.slope()` for terrain safety analysis\n",
" - A* pathfinding to find a safe route to radio coverage\n",
" - Elevation profiling and hiking time estimation\n",
"\n",
"### Key Takeaways\n",
"\n",
"- **Terrain matters**: The Rocky Mountain foothills create significant \"radio shadows\" where VHF/UHF signals cannot reach\n",
"- **Height is key**: Repeaters placed on elevated terrain (like Lookout Mountain) achieve dramatically better coverage\n",
"- **GPU acceleration**: For large DEMs, GPU processing via rtxpy can provide 70-300x speedup\n",
"- **Combined coverage**: Analyzing multiple repeaters together reveals gaps and redundancies\n",
"- **Proper projections**: Using EPSG:5070 allows accurate area calculations in km²\n",
"- **Practical applications**: Viewshed + slope analysis enables search-and-rescue routing\n",
"\n",
"### Limitations and Real-World Considerations\n",
"\n",
"This analysis provides a **simplified model** of radio coverage. Real-world factors not considered include:\n",
"\n",
"- **Fresnel zone clearance**: Radio waves need additional clearance beyond line-of-sight\n",
"- **Antenna patterns**: Antennas don't radiate equally in all directions\n",
"- **Atmospheric effects**: Temperature inversions, humidity, etc.\n",
"- **Urban clutter**: Buildings and other structures\n",
"- **Frequency-specific behavior**: Different frequencies propagate differently\n",
"- **Power levels and receiver sensitivity**: Not all signals that \"can\" reach will be usable\n",
"\n",
"For more accurate RF coverage prediction, consider dedicated tools like:\n",
"- SPLAT! (free, open-source)\n",
"- Radio Mobile (free)\n",
"- Commercial tools like EDX SignalPro\n",
"\n",
"### Further Resources\n",
"\n",
"- [xarray-spatial documentation](https://xarray-spatial.org/)\n",
"- [xarray-spatial viewshed GPU example](https://github.com/xarray-contrib/xarray-spatial/blob/master/examples/viewshed_gpu.ipynb)\n",
"- [RepeaterBook](https://www.repeaterbook.com/)\n",
"- [ARRL Technical Information Service](http://www.arrl.org/tis)\n",
"- [VHF/UHF Propagation](https://www.arrl.org/vhf-uhf-propagation)"
]
"source": "## 12. Conclusion\n\n### What We Accomplished\n\nIn this notebook, we:\n\n1. **Downloaded real terrain data** using SRTM 30m tiles directly from the USGS National Map\n2. **Reprojected to EPSG:5070** (Albers Equal Area) for accurate distance/area calculations\n3. **Retrieved actual repeater locations** from the RepeaterBook API and transformed coordinates\n4. **Computed viewsheds** using data-space coordinates (meters) directly - no pixel conversion needed\n5. **Demonstrated GPU acceleration** with xarray-spatial and rtxpy (73x speedup!)\n6. **Visualized coverage** with hillshade overlays and combined analysis\n7. **Simulated a lost hiker rescue scenario** using:\n - `xrspatial.slope()` for terrain safety analysis\n - A* pathfinding to find a safe route to radio coverage\n - Elevation profiling and hiking time estimation\n\n### Key Takeaways\n\n- **Terrain matters**: The Rocky Mountain foothills create significant \"radio shadows\" where VHF/UHF signals cannot reach\n- **Height is key**: Repeaters placed on elevated terrain (like Lookout Mountain) achieve dramatically better coverage\n- **GPU acceleration**: For large DEMs, GPU processing via rtxpy can provide 70-300x speedup\n- **Combined coverage**: Analyzing multiple repeaters together reveals gaps and redundancies\n- **Proper projections**: Using EPSG:5070 allows accurate area calculations in km²\n- **Practical applications**: Viewshed + slope analysis enables search-and-rescue routing\n\n### Limitations and Real-World Considerations\n\nThis analysis provides a **simplified model** of radio coverage. Real-world factors not considered include:\n\n- **Fresnel zone clearance**: Radio waves need additional clearance beyond line-of-sight\n- **Antenna patterns**: Antennas don't radiate equally in all directions\n- **Atmospheric effects**: Temperature inversions, humidity, etc.\n- **Urban clutter**: Buildings and other structures\n- **Frequency-specific behavior**: Different frequencies propagate differently\n- **Power levels and receiver sensitivity**: Not all signals that \"can\" reach will be usable\n\nFor more accurate RF coverage prediction, consider dedicated tools like:\n- SPLAT! (free, open-source)\n- Radio Mobile (free)\n- Commercial tools like EDX SignalPro\n\n### Further Resources\n\n- [xarray-spatial documentation](https://xarray-spatial.org/)\n- [xarray-spatial viewshed GPU example](https://github.com/xarray-contrib/xarray-spatial/blob/master/examples/viewshed_gpu.ipynb)\n- [RepeaterBook](https://www.repeaterbook.com/)\n- [ARRL Technical Information Service](http://www.arrl.org/tis)\n- [VHF/UHF Propagation](https://www.arrl.org/vhf-uhf-propagation)"
},
{
"cell_type": "code",
Expand Down Expand Up @@ -1837,4 +1702,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ dependencies = [

[project.optional-dependencies]
# GPU dependencies - cupy provides CUDA context management
# Install cupy via: conda install -c conda-forge cupy
# Note: otk-pyoptix must be installed separately from NVIDIA
# See: https://github.com/NVIDIA/otk-pyoptix
cuda12 = ["cupy-cuda12x>=12.0"]
cuda11 = ["cupy-cuda11x>=11.0"]
tests = ["pytest"]

[project.urls]
Expand Down
Loading