From 76c4b8cbc3848e430194c1d50b4e3b00e3eede82 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 03:36:06 +0000 Subject: [PATCH 1/2] docs: ITM integration review - audit findings and adoption roadmap Full codebase audit of propagation model integration covering: - 6 bugs found (broken backend endpoint, missing environment params in 3 coverage paths, batch FSPL-only) - Math verified correct against ITU-R P.525/P.526, Hata (1980), NTIA ITM, Semtech SX1262 - Parameter flow matrix showing where sidebar values reach vs get dropped - 4-phase adoption roadmap from critical fixes to advanced features https://claude.ai/code/session_012EMZvmghs9ofADggaGrF9Y --- Documentation/ITM-INTEGRATION-REVIEW.md | 426 ++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 Documentation/ITM-INTEGRATION-REVIEW.md diff --git a/Documentation/ITM-INTEGRATION-REVIEW.md b/Documentation/ITM-INTEGRATION-REVIEW.md new file mode 100644 index 0000000..bbdc9f5 --- /dev/null +++ b/Documentation/ITM-INTEGRATION-REVIEW.md @@ -0,0 +1,426 @@ +# MeshRF ITM Integration Review & Adoption Roadmap + +**Date:** 2026-02-09 +**Scope:** Full audit of propagation model integration, device parameter flow, math accuracy, and model-switching infrastructure. + +--- + +## Executive Summary + +MeshRF has a **strong foundation** for ITM integration. The C++ WASM Longley-Rice implementation uses the actual NTIA reference source code, the math formulas are verified correct against ITU-R standards, and the sidebar UI provides comprehensive device parameter editing. However, the audit reveals **several critical gaps between the data the user configures and the data that actually reaches the calculations**. The model-switching system (ITM/Hata/Bullington/FSPL) has a broken backend path, and environment parameters (ground type, climate) are dropped in multiple recalculation paths. + +**Bottom line:** The data exists. The math is sound. The wiring has gaps. + +--- + +## Part 1: What Works Well + +### 1.1 WASM ITM Integration (Link Analysis) + +The primary path -- clicking two points on the map with ITM (WASM) selected -- is **fully functional and correctly wired**. + +**Data flow (verified):** +``` +User clicks map -> LinkLayer.runAnalysis() + -> fetchElevationPath() for terrain profile + -> useWasmITM.calculatePathLoss() with: + - elevationProfile (from tile server) + - stepSizeMeters (calculated from profile) + - frequencyMHz (from RFContext.freq) + - txHeightM (from nodeConfigs.A.antennaHeight) + - rxHeightM (from nodeConfigs.B.antennaHeight) + - groundEpsilon (from GROUND_TYPES[groundType]) + - groundSigma (from GROUND_TYPES[groundType]) + - climate (from RFContext.climate) + -> ITM_P2P_TLS() C++ function (NTIA reference) + -> path_loss_db returned + -> calculateLinkBudget() with per-node A/B params +``` + +**File:** `src/components/Map/LinkLayer.jsx:92-118` + +This path correctly uses: per-node antenna heights, per-node gains/losses, user-selected ground type, climate zone, frequency, and the full ITM terrain model. + +### 1.2 WASM RF Coverage + +The coverage heatmap tool also correctly invokes the full ITM model via WASM for every radial from the transmitter. + +**File:** `src/hooks/useRFCoverageTool.js:153-172` + +Parameters passed: tx height, rx height, frequency, tx power (with cable loss subtracted), tx gain, rx gain, rx sensitivity, epsilon, sigma, climate. The C++ coverage function (`libmeshrf/src/meshrf_coverage.cpp`) calls `calculate_radial_loss()` which invokes `ITM_P2P_TLS()` for each pixel. + +### 1.3 Per-Node A/B Configuration + +The dual-node system in RFContext is well-designed: +- GLOBAL mode updates both nodes simultaneously +- A/B mode updates individual nodes +- Device power caps, antenna gain sync, and cable loss are all computed correctly +- Link budget correctly pulls txPower/txGain/txLoss from Node A and rxGain/rxLoss from Node B + +**File:** `src/context/RFContext.jsx:70-82` + +### 1.4 Math Accuracy (Verified) + +All core formulas have been verified against authoritative sources: + +| Formula | Source | Status | +|---------|--------|--------| +| FSPL: `20log10(d) + 20log10(f) + 32.44` | ITU-R P.525-4 | Correct (32.45 in ITU exact; 0.01 dB immaterial) | +| Okumura-Hata (all variants) | Hata (1980) IEEE paper | Correct (all 4 environment types) | +| Knife-edge diffraction J(v) | ITU-R P.526-14 Eq. 31 | Correct | +| Fresnel zone radius (17.32 constant) | sqrt(300) derivation | Correct | +| Earth bulge h=(d1*d2)/(2*Re) | Standard microwave textbook | Correct | +| K-factor = 1.33 | Standard atmosphere (4/3 earth) | Correct | +| ITM N_0=301, mdvar=12, T/L/S=50/50/50 | NTIA reference | Correct and appropriate | + +### 1.5 Model Selector UI + +The LinkAnalysisPanel provides a clean dropdown with all four models: +- Longley-Rice ITM (Full) -- `itm_wasm` +- Free Space (Optimistic) -- `fspl` +- Bullington (Terrain Helper) -- `bullington` +- Okumura-Hata (Statistical) -- `hata` + +Plus an environment selector for Hata (urban/suburban/rural) with validity warnings. + +**File:** `src/components/Map/LinkAnalysisPanel.jsx:411-422` + +--- + +## Part 2: Critical Issues Found + +### BUG-1: Backend `/calculate-link` Endpoint is Broken (CRITICAL) + +**File:** `rf-engine/server.py:68-79` + +```python +path_loss_db = rf_physics.calculate_path_loss( + dist_m, + elevs, + req.frequency_mhz, + req.tx_height, + req.rx_height, + req.rx_height, # <-- BUG: duplicate positional arg + model=req.model, # <-- conflicts with positional + environment=req.environment, + k_factor=req.k_factor, + clutter_height=req.clutter_height +) +``` + +`req.rx_height` is passed twice. The second instance occupies the `model` positional parameter slot, then `model=req.model` is also passed as a keyword argument. Python raises `TypeError: calculate_path_loss() got multiple values for argument 'model'`. + +**Impact:** Any model that routes through the backend (Hata, Bullington, ITM non-WASM) will **crash with a 500 error**. Only `itm_wasm` works because it bypasses the backend entirely and runs client-side WASM. + +**Fix:** Remove the duplicate `req.rx_height` on line 74. + +### BUG-2: Environment Params Not Passed to CoverageClickHandler (HIGH) + +**File:** `src/components/Map/MapContainer.jsx:509-522` + +The `rfContext` prop passed to `CoverageClickHandler` is **missing** `groundType`, `climate`, and `calculateSensitivity`: + +```javascript +rfContext={{ + freq, txPower: proxyTx, antennaGain: proxyGain, + bw, sf, cr, antennaHeight, getAntennaHeightMeters, + rxHeight, txLoss: cableLoss, rxLoss: 0, + rxAntennaGain: nodeConfigs.B.antennaGain, + // MISSING: groundType, climate, calculateSensitivity +}} +``` + +**Impact:** `CoverageClickHandler` (line 24) falls back to `GROUND_TYPES['Average Ground']` for epsilon/sigma and `undefined` for climate (which defaults to 5 in WASM). The user's ground type and climate zone selections in the sidebar are **silently ignored** on initial RF coverage clicks. + +### BUG-3: RF Coverage Recalculation Drops Environment Params (HIGH) + +**File:** `src/components/Map/MapContainer.jsx:226-238` + +When the "Update Calculation" button triggers a recalculation, the `rfParams` object is rebuilt **without** `epsilon`, `sigma`, or `climate`: + +```javascript +const rfParams = { + freq, txPower: proxyTx, txGain: proxyGain, + txLoss: cableLoss, rxLoss: 0, + rxGain: nodeConfigs.B.antennaGain || 2.15, + rxSensitivity: currentSensitivity, + bw, sf, cr, rxHeight, + // MISSING: epsilon, sigma, climate +}; +``` + +**Impact:** After parameter updates, the coverage map recalculates using hardcoded WASM defaults (Average Ground, Continental Temperate) instead of user selections. + +### BUG-4: RF Observer Drag Drops Multiple Params (MEDIUM) + +**File:** `src/components/Map/MapContainer.jsx:621-631` + +When dragging the RF coverage transmitter marker, the recalc builds rfParams missing: `txLoss`, `epsilon`, `sigma`, `climate`. Also hardcodes `rxGain: 2.15` instead of using `nodeConfigs.B.antennaGain`. + +### BUG-5: Backend Hata/Bullington Falls Back to Bullington for ITM (LOW) + +**File:** `rf-engine/rf_physics.py:149` + +```python +if model == 'bullington' or model == 'itm' or model == 'itm_wasm': + diffraction = calculate_bullington_loss(...) + return fspl + diffraction +``` + +When the backend model is set to `itm` or `itm_wasm`, it silently falls back to FSPL + Bullington diffraction instead of running actual ITM. There is no server-side ITM implementation (the `itmlogic` Python package is listed in requirements.txt but never imported). This is only relevant if BUG-1 is fixed, since the backend currently crashes before reaching this code. + +### BUG-6: Batch Processing Uses FSPL Only (MEDIUM) + +**File:** `src/components/Map/BatchProcessing.jsx:213-222` + +Batch mesh report uses `calculateLinkBudget()` without `pathLossOverride`, meaning it always falls back to FSPL. It never calls the backend or WASM for terrain-aware propagation. For a mesh planning tool, this means batch reports can be overly optimistic. + +Additionally, batch processing uses the proxy `antennaHeight` and `antennaGain` (GLOBAL mode values) for both TX and RX instead of per-node configs, and doesn't apply `fadeMargin`. + +--- + +## Part 3: Parameter Flow Gaps + +### What the User Can Configure (Sidebar) + +| Parameter | UI Control | Location | +|-----------|-----------|----------| +| Device (preset) | Dropdown | Sidebar:305-316 | +| Antenna (preset/custom) | Dropdown + gain input | Sidebar:318-345 | +| Antenna Height | Slider 1-50m | Sidebar:347-359 | +| RX Height | Slider 1-30m | Sidebar:361-380 | +| Cable Type | Dropdown | Sidebar:385-396 | +| Cable Length | Numeric input | Sidebar:398-413 | +| TX Power | Slider 0-max | Sidebar:420-435 | +| K-Factor | Numeric input | Sidebar:537-548 | +| Clutter Height | Numeric input | Sidebar:550-562 | +| Ground Type | Dropdown | Sidebar:567-578 | +| Climate Zone | Dropdown | Sidebar:581-591 | +| Fade Margin | Slider 0-20 dB | Sidebar:595-611 | +| Radio Preset / Custom | Dropdown + fields | Sidebar:615-695 | + +### Where Parameters Actually Reach Calculations + +| Parameter | Link (ITM WASM) | Link (Backend) | RF Coverage (Click) | RF Coverage (Recalc) | RF Coverage (Drag) | Batch | +|-----------|:---:|:---:|:---:|:---:|:---:|:---:| +| TX Power | Y | CRASH | Y | Y | Y | Y | +| TX Antenna Gain | Y (per-node) | CRASH | Y | Y | hardcoded 2.15 | Y (global) | +| TX Antenna Height | Y (per-node) | CRASH | Y | Y | Y | Y (global) | +| TX Cable Loss | Y (device loss only) | CRASH | Y | Y | MISSING | Y | +| RX Antenna Gain | Y (per-node) | CRASH | fallback 2.15 | fallback 2.15 | hardcoded 2.15 | Y (global) | +| RX Height | N/A | CRASH | Y | Y | Y | N/A | +| Frequency | Y | CRASH | Y | Y | Y | Y | +| SF / BW | Y (sensitivity) | CRASH | Y | Y | Y | Y | +| K-Factor | Y | CRASH | N/A | N/A | N/A | Y | +| Clutter Height | Y | CRASH | N/A | N/A | N/A | Y | +| Ground Type (eps/sig) | Y | CRASH | MISSING | MISSING | MISSING | N/A | +| Climate Zone | Y | CRASH | MISSING | MISSING | MISSING | N/A | +| Fade Margin | Y | CRASH | N/A | N/A | N/A | MISSING | +| Propagation Model | Y (WASM only) | CRASH | always ITM WASM | always ITM WASM | always ITM WASM | FSPL only | + +**Legend:** Y = correctly passed, MISSING = silently uses defaults, CRASH = server error, N/A = not applicable to this tool + +--- + +## Part 4: Math Accuracy Notes + +### 4.1 Minor Discrepancies (Non-Critical) + +**FSPL Constant:** JavaScript and Python use `32.44`, C++ ITM uses `32.45`. The exact value per ITU-R P.525-4 (using c = 299,792,458 m/s) is 32.45. The difference is 0.01 dB -- completely immaterial for planning purposes but could be harmonized for consistency. + +**LoRa Base Sensitivity:** Code uses `-123 dBm` for SF7/125kHz. The Semtech SX1262 datasheet specifies `-124 dBm` in power-saving RX mode. The code value appears to originate from the older SX1276 datasheet. For SX1262-based devices (which all MeshCore devices use), `-124 dBm` would be more accurate. This makes all link budgets **1 dB optimistic**. + +**SF Gain Approximation:** Code uses a flat `2.5 dB/step`. Actual SX1262 datasheet values show ~3 dB/step for SF7-SF10 and ~1.5 dB/step for SF11-SF12. The 2.5 dB average is reasonable for planning but could be improved with a lookup table for precision. + +### 4.2 Dual Sensitivity Calculation + +RFContext exports two different sensitivity calculation methods: + +1. **calculateLinkBudget** (rfMath.js:81-91): Uses `base + bwFactor + sfFactor` with SF_GAIN_PER_STEP=2.5 +2. **calculateSensitivity** (RFContext.jsx:293-312): Uses thermal noise floor + NF + SNR lookup table + +These give slightly different results. For SF7/125kHz: +- Method 1: -123 + 0 + 0 = **-123 dBm** +- Method 2: -174 + 10*log10(125000) + 6 + (-7.5) = -174 + 51 + 6 - 7.5 = **-124.5 dBm** + +The 1.5 dB discrepancy means coverage maps (which use `calculateSensitivity`) are slightly more conservative than link analysis (which uses `calculateLinkBudget`). This inconsistency should be resolved by using a single canonical method. + +### 4.3 Okumura-Hata Validity Bounds + +The Hata model is designed for: +- **Frequency:** 150-1500 MHz (LoRa 915 MHz is within range) +- **Distance:** 1-20 km (code allows < 1 km by clamping to 0.1 km) +- **Base station height:** 30-200 m (most LoRa deployments are 2-15 m -- well below minimum) +- **Mobile height:** 1-10 m + +The UI correctly warns when parameters are outside Hata bounds, but the model still runs with potentially unreliable results for typical LoRa antenna heights. + +--- + +## Part 5: Model Switching Architecture + +### Current State + +The model selector in LinkAnalysisPanel sets `propagationSettings.model` which flows to LinkLayer. The routing logic: + +``` +itm_wasm (default) -> Client-side WASM ITM (works correctly) +hata -> Backend API call (CRASHES - BUG-1) +bullington -> Backend API call (CRASHES - BUG-1) +fspl -> Backend API call (CRASHES - BUG-1) +``` + +**Only the WASM ITM path is functional.** The model selector appears to work but the three non-WASM options silently fail (the frontend catches the error and falls back to no backend path loss, which means FSPL-only via calculateLinkBudget). + +### RF Coverage Tool + +The RF coverage tool **always** uses WASM ITM regardless of the model selector. There is no model switching for coverage maps -- they are hardwired to the C++ ITM implementation. This is actually a reasonable design choice since ITM is the most accurate model, but users may expect the model selector to affect coverage maps too. + +### Viewshed Tool + +The viewshed tool is pure line-of-sight geometry and doesn't use any propagation model. This is correct behavior. + +--- + +## Part 6: Adoption Roadmap + +### Phase 1: Critical Bug Fixes (Immediate) + +**P1-1. Fix Backend Double-Parameter Bug** +- File: `rf-engine/server.py:74` +- Fix: Remove duplicate `req.rx_height` positional argument +- Impact: Unblocks Hata, Bullington, and FSPL models via backend + +**P1-2. Wire Environment Params to CoverageClickHandler** +- File: `src/components/Map/MapContainer.jsx:509-522` +- Fix: Add `groundType`, `climate`, `calculateSensitivity` to rfContext prop +- Impact: User's ground type and climate selections will affect RF coverage + +**P1-3. Wire Environment Params to Recalculation Path** +- File: `src/components/Map/MapContainer.jsx:226-238` +- Fix: Import GROUND_TYPES, look up current groundType, add epsilon/sigma/climate to rfParams +- Impact: "Update Calculation" button respects environment settings + +**P1-4. Wire Full Params to RF Observer Drag Handler** +- File: `src/components/Map/MapContainer.jsx:621-631` +- Fix: Add txLoss, epsilon, sigma, climate, use nodeConfigs.B.antennaGain for rxGain +- Impact: Dragging the RF transmitter marker produces correct recalculations + +### Phase 2: Consistency & Accuracy (Short-term) + +**P2-1. Unify Sensitivity Calculation** +- Choose one canonical method (recommend the thermal noise floor approach from RFContext) +- Update calculateLinkBudget to use the same method or accept sensitivity as a parameter +- Eliminates the 1.5 dB discrepancy between link analysis and coverage tools + +**P2-2. Update LoRa Base Sensitivity to SX1262 Spec** +- Change `BASE_SENSITIVITY_SF7_125KHZ` from -123 to -124 in rfConstants.js +- Or better: replace flat 2.5 dB/step with per-SF lookup table matching SX1262 datasheet +- Impact: 1 dB more conservative (realistic) link budgets + +**P2-3. Harmonize FSPL Constant** +- Update rfMath.js and rf_physics.py to use 32.45 (matching ITU-R P.525-4 and C++ ITM code) +- Impact: Cosmetic consistency (0.01 dB) + +**P2-4. Add Fade Margin to Batch Processing** +- File: `src/components/Map/BatchProcessing.jsx:213-222` +- Pass `fadeMargin` to calculateLinkBudget call +- Impact: Batch reports include the safety margin users configured + +### Phase 3: Full Model Switching (Medium-term) + +**P3-1. Implement Client-Side Bullington/Hata/FSPL** +- Move Bullington diffraction and Hata calculations to JavaScript (already partially done in rfMath.js for Bullington) +- This eliminates the dependency on the Python backend for non-ITM models +- The backend can still serve as a fallback but the primary path should be client-side + +**P3-2. Apply Model Selection to RF Coverage Tool** +- Currently hardwired to WASM ITM +- Add model dispatch in useRFCoverageTool.js that uses FSPL-only or Hata for faster coverage maps +- ITM remains the default but users could choose faster/simpler models for quick surveys + +**P3-3. Integrate WASM ITM into Batch Processing** +- Currently FSPL-only; should use the same WASM ITM path as link analysis +- Requires fetching elevation profiles for each node pair (already done for link analysis) +- Will significantly improve batch report accuracy + +**P3-4. Per-Node Configs in Batch Processing** +- Currently uses GLOBAL proxy values for both TX and RX +- Should support per-node antenna heights, gains, and cable configurations +- CSV import could include optional height/device columns + +### Phase 4: Advanced Integration (Long-term) + +**P4-1. Server-Side ITM via itmlogic** +- `itmlogic` is listed in requirements.txt but never imported +- Implement as a true Python fallback for environments where WASM isn't available +- Enables server-side batch processing with full ITM accuracy + +**P4-2. COST 231 Hata Extension** +- Current Hata model covers 150-1500 MHz +- COST 231 extends to 2000 MHz for future higher-frequency mesh deployments +- Straightforward addition to rf_physics.py + +**P4-3. Clutter/Land Use Integration** +- Current clutter model is a uniform height applied everywhere +- Could integrate land cover data (NLCD, Corine) for per-pixel clutter classification +- Would significantly improve urban/forest coverage accuracy + +**P4-4. Antenna Pattern Support** +- All models currently assume omnidirectional antennas +- Yagi antenna preset has 11 dBi gain but no directional pattern +- Adding azimuth/elevation patterns would improve directional link predictions + +**P4-5. Multi-Hop Mesh Analysis** +- Current tools analyze point-to-point links +- A mesh planner would calculate end-to-end connectivity through relay chains +- Could use the batch processing infrastructure as a foundation + +--- + +## Appendix A: File Reference + +| File | Role | Key Lines | +|------|------|-----------| +| `rf-engine/server.py` | Backend API | 68-79 (BUG-1) | +| `rf-engine/rf_physics.py` | Python propagation models | 25-155 | +| `src/utils/rfMath.js` | JS propagation math | 10-368 | +| `src/utils/rfConstants.js` | Constants & thresholds | 1-45 | +| `src/utils/rfService.js` | API client | 32-56 | +| `src/context/RFContext.jsx` | Global state | 54-317 | +| `src/data/presets.js` | Hardware presets | 1-122 | +| `src/hooks/useWasmITM.js` | WASM ITM hook | 1-167 | +| `src/hooks/useRFCoverageTool.js` | Coverage tool | 102-228 | +| `src/components/Map/LinkLayer.jsx` | Link analysis UI | 71-139 | +| `src/components/Map/LinkAnalysisPanel.jsx` | Results panel + model selector | 404-528 | +| `src/components/Map/MapContainer.jsx` | Main map + param wiring | 211-243, 503-523, 614-634 | +| `src/components/Map/Controls/CoverageClickHandler.jsx` | Coverage click handler | 16-46 | +| `src/components/Map/BatchProcessing.jsx` | Batch mesh report | 179-253 | +| `src/components/Layout/Sidebar.jsx` | Parameter editing UI | 299-695 | +| `libmeshrf/src/meshrf_itm.cpp` | C++ ITM wrapper | 18-98 | +| `libmeshrf/src/meshrf_coverage.cpp` | C++ coverage engine | 7-111 | +| `libmeshrf/src/bindings.cpp` | WASM bindings | 14-75 | +| `libmeshrf/vendor/itm/` | NTIA ITM reference source | (40+ files) | + +## Appendix B: ITM Parameter Defaults + +| Parameter | Value | Source | +|-----------|-------|--------| +| Surface Refractivity (N_0) | 301.0 N-units | Standard atmosphere | +| Mode of Variability (mdvar) | 12 | Mobile + eliminate location variability | +| Time Percentage | 50% | Median (standard planning) | +| Location Percentage | 50% | Median | +| Situation Percentage | 50% | Median | +| Polarization | 1 (Vertical) | LoRa standard | +| Default Ground (epsilon) | 15.0 | Average ground | +| Default Ground (sigma) | 0.005 S/m | Average ground | +| Default Climate | 5 | Continental Temperate | + +## Appendix C: Math Verification Sources + +- ITU-R P.525-4 (FSPL) +- ITU-R P.526-14 (Knife-edge diffraction) +- M. Hata, IEEE Trans. Veh. Technol., Vol. VT-29, 1980 (Okumura-Hata) +- Semtech SX1261/2 Datasheet Rev 1.2 (LoRa sensitivity) +- NTIA/ITS ITM Reference Implementation (github.com/NTIA/itm) +- COST 231 Final Report (Hata extension, future reference) From a82b4c92389e5fbd0474ea7f52b37d69d1f082f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 03:48:06 +0000 Subject: [PATCH 2/2] fix: Phase 1 & 2 ITM integration fixes + adoption roadmap Phase 1 - Critical Bug Fixes: - Fix /calculate-link crash: remove duplicate rx_height positional arg (server.py) - Wire groundType, climate, calculateSensitivity to CoverageClickHandler - Add epsilon/sigma/climate to RF coverage recalculation path - Fix RF observer drag handler: add txLoss, env params, use per-node rxGain Phase 2 - Consistency & Accuracy: - Unify sensitivity: single calculateLoRaSensitivity() using SX1262 per-SF lookup table, called by both calculateLinkBudget and RFContext - Update LoRa sensitivity to SX1262 datasheet (-124 dBm SF7/125kHz) - Harmonize FSPL constant to 32.45 (ITU-R P.525-4) across JS and Python - Batch processing: per-node A/B configs, Bullington diffraction, fade margin Phase 3 & 4 roadmap captured in Documentation/ROADMAP.md https://claude.ai/code/session_012EMZvmghs9ofADggaGrF9Y --- Documentation/ROADMAP.md | 87 ++++++++++++++++++++++++++ rf-engine/rf_physics.py | 2 +- rf-engine/server.py | 9 ++- src/components/Map/BatchProcessing.jsx | 50 +++++++++------ src/components/Map/MapContainer.jsx | 27 ++++++-- src/context/RFContext.jsx | 21 +------ src/utils/rfConstants.js | 13 +++- src/utils/rfMath.js | 48 +++++++------- 8 files changed, 178 insertions(+), 79 deletions(-) create mode 100644 Documentation/ROADMAP.md diff --git a/Documentation/ROADMAP.md b/Documentation/ROADMAP.md new file mode 100644 index 0000000..e573d3b --- /dev/null +++ b/Documentation/ROADMAP.md @@ -0,0 +1,87 @@ +# MeshRF Propagation Engine Roadmap + +**Last Updated:** 2026-02-09 + +--- + +## Completed + +### Phase 1: Critical Bug Fixes + +- [x] **P1-1** Fixed backend `/calculate-link` crash -- removed duplicate `req.rx_height` positional argument in `server.py:68-78` that caused `TypeError` on every call. Hata, Bullington, and FSPL models via the backend now work. +- [x] **P1-2** Wired `groundType`, `climate`, and `calculateSensitivity` to `CoverageClickHandler` via the `rfContext` prop in `MapContainer.jsx`. User's ground type and climate zone selections now flow to initial RF coverage calculations. +- [x] **P1-3** Added `epsilon`, `sigma`, and `climate` to the RF coverage recalculation path (`MapContainer.jsx` recalcTimestamp effect). The "Update Calculation" button now respects environment settings. +- [x] **P1-4** Fixed RF observer drag handler to include `txLoss` (cable loss), `epsilon`, `sigma`, `climate`, and use `nodeConfigs.B.antennaGain` instead of hardcoded `2.15` for rxGain. + +### Phase 2: Consistency & Accuracy + +- [x] **P2-1** Unified sensitivity calculation -- created canonical `calculateLoRaSensitivity(sf, bw)` in `rfMath.js` using SX1262 per-SF lookup table. Both `calculateLinkBudget` and `RFContext.calculateSensitivity` now delegate to this single function. Eliminated the 1.5 dB discrepancy between link analysis and coverage tools. +- [x] **P2-2** Updated LoRa sensitivity to SX1262 datasheet values. Per-SF lookup table at 125kHz: SF7=-124, SF8=-127, SF9=-130, SF10=-133, SF11=-135.5, SF12=-137 dBm. Replaces the old `-123 + 2.5*step` approximation. +- [x] **P2-3** Harmonized FSPL constant to `32.45` across `rfMath.js` and `rf_physics.py`, matching ITU-R P.525-4 (exact speed of light) and the C++ ITM vendor code. +- [x] **P2-4** Batch processing now uses per-node A/B configs (antenna height, gain, device loss) instead of GLOBAL proxy values. Fade margin is now included in batch link budgets. Bullington diffraction is applied for terrain-aware path loss instead of pure FSPL. + +--- + +## Phase 3: Full Model Switching (Medium-term) + +### P3-1: Client-Side Hata/FSPL Models +Move Okumura-Hata and explicit FSPL calculations to JavaScript so model switching works without the Python backend. The Bullington diffraction model is already in `rfMath.js`. Adding Hata eliminates the backend dependency for non-ITM models. + +**Files:** `src/utils/rfMath.js`, `src/components/Map/LinkLayer.jsx` + +### P3-2: Model Selection for RF Coverage +Currently the RF coverage tool is hardwired to WASM ITM. Add a model dispatch in `useRFCoverageTool.js` that supports FSPL-only or Hata for faster coverage maps when full ITM precision isn't needed. ITM remains the default. + +**Files:** `src/hooks/useRFCoverageTool.js`, `src/components/Map/Controls/CoverageClickHandler.jsx` + +### P3-3: WASM ITM for Batch Processing +Batch mesh reports currently use FSPL + Bullington (frontend-only). Integrate the WASM ITM path (same as link analysis) for full terrain-aware batch reports. Requires fetching elevation profiles for each node pair. + +**Files:** `src/components/Map/BatchProcessing.jsx`, `src/hooks/useWasmITM.js` + +### P3-4: Per-Node Configs in Batch CSV +Allow CSV import to include optional per-node columns: antenna height, device type, antenna type. Currently all batch nodes use the global A/B config. Per-node overrides would enable realistic multi-device mesh planning. + +**Files:** `src/components/Map/BatchProcessing.jsx` + +--- + +## Phase 4: Advanced Integration (Long-term) + +### P4-1: Server-Side ITM via itmlogic +`itmlogic` is listed in `requirements.txt` but never imported. Implement as a true Python ITM fallback for server-side batch processing and environments where WASM isn't available. Enables Celery workers to run ITM asynchronously. + +**Files:** `rf-engine/rf_physics.py`, `rf-engine/tasks/` + +### P4-2: COST 231 Hata Extension +Current Hata model covers 150-1500 MHz. The COST 231 extension covers 1500-2000 MHz for future higher-frequency deployments (e.g., 2.4 GHz ISM). Straightforward formula addition. + +**Files:** `rf-engine/rf_physics.py`, `src/utils/rfMath.js` (if client-side Hata is added in P3-1) + +### P4-3: Clutter / Land-Use Integration +Current clutter model applies a uniform height everywhere. Integrating land cover data (NLCD for US, Corine for EU) would enable per-pixel clutter classification: forest canopy height, urban building density, open field. This would significantly improve coverage accuracy in mixed environments. + +**Dependencies:** Land cover tile server, clutter height lookup table + +### P4-4: Antenna Pattern Support +All models currently assume omnidirectional antennas. The Yagi preset has 11 dBi gain but no directional pattern. Adding azimuth/elevation radiation patterns would enable: +- Directional link predictions +- Coverage maps with beam patterns +- Tilt optimization for hilltop sites + +**Data needed:** Antenna pattern files (CSV or NEC2 format) + +### P4-5: Multi-Hop Mesh Analysis +Current tools analyze point-to-point links only. A mesh planner would: +- Calculate end-to-end connectivity through relay chains +- Identify single points of failure +- Suggest optimal relay placement +- Estimate end-to-end latency and throughput + +Could build on the batch processing infrastructure with graph analysis (Dijkstra/Floyd-Warshall for optimal paths). + +### P4-6: Probabilistic / Variability Modes +The ITM supports time/location/situation variability percentages (currently fixed at 50/50/50). Exposing these as user controls would enable: +- Worst-case planning (90/90/90 for reliability) +- Best-case estimation (10/10/10 for maximum range) +- Statistical coverage contours showing probability of reception diff --git a/rf-engine/rf_physics.py b/rf-engine/rf_physics.py index ccfd60a..61947c4 100644 --- a/rf-engine/rf_physics.py +++ b/rf-engine/rf_physics.py @@ -140,7 +140,7 @@ def calculate_path_loss(dist_m, elevs, freq_mhz, tx_h, rx_h, model='bullington', return calculate_hata_loss(dist_m, freq_mhz, tx_h, rx_h, environment) # 2. Free Space (FSPL) - implicitly used as base for others or explicit - fspl = 20 * math.log10(dist_km) + 20 * math.log10(freq_mhz) + 32.44 + fspl = 20 * math.log10(dist_km) + 20 * math.log10(freq_mhz) + 32.45 if model == 'fspl': return fspl diff --git a/rf-engine/server.py b/rf-engine/server.py index 91d26b3..fb05a29 100644 --- a/rf-engine/server.py +++ b/rf-engine/server.py @@ -66,11 +66,10 @@ def calculate_link_endpoint(req: LinkRequest): # Calculate Path Loss (ITM or FSPL) # Calculate Path Loss (Generic Dispatcher) path_loss_db = rf_physics.calculate_path_loss( - dist_m, - elevs, - req.frequency_mhz, - req.tx_height, - req.rx_height, + dist_m, + elevs, + req.frequency_mhz, + req.tx_height, req.rx_height, model=req.model, environment=req.environment, diff --git a/src/components/Map/BatchProcessing.jsx b/src/components/Map/BatchProcessing.jsx index e2bfee8..c5f938c 100644 --- a/src/components/Map/BatchProcessing.jsx +++ b/src/components/Map/BatchProcessing.jsx @@ -1,16 +1,16 @@ import React, { useState, useRef, useEffect } from 'react'; import { useRF } from '../../context/RFContext'; import { fetchElevationPath } from '../../utils/elevation'; -import { analyzeLinkProfile, calculateLinkBudget } from '../../utils/rfMath'; +import { analyzeLinkProfile, calculateLinkBudget, calculateBullingtonDiffraction } from '../../utils/rfMath'; +import { DEVICE_PRESETS } from '../../data/presets'; const BatchProcessing = () => { const { batchNodes, setBatchNodes, setShowBatchPanel, - freq, antennaHeight, antennaGain, - txPower, cableLoss, + freq, nodeConfigs, kFactor, clutterHeight, - sf, bw, + sf, bw, fadeMargin, isMobile, sidebarIsOpen } = useRF(); @@ -198,31 +198,41 @@ const BatchProcessing = () => { ); if (profile) { + const configA = nodeConfigs.A; + const configB = nodeConfigs.B; + const analysis = analyzeLinkProfile( - profile, - freq, - antennaHeight, - antennaHeight, + profile, + freq, + configA.antennaHeight, + configB.antennaHeight, kFactor, clutterHeight ); - + const distKm = profile[profile.length-1].distance; - - // Link Budget + + // Calculate Bullington diffraction for terrain-aware path loss + const diffraction = analysis.profileWithStats + ? calculateBullingtonDiffraction(analysis.profileWithStats, freq, configA.antennaHeight, configB.antennaHeight) + : 0; + + // Link Budget with per-node params and terrain diffraction const budget = calculateLinkBudget({ - txPower, - txGain: antennaGain, - txLoss: cableLoss, - rxGain: antennaGain, - rxLoss: cableLoss, - distanceKm: distKm, + txPower: configA.txPower, + txGain: configA.antennaGain, + txLoss: DEVICE_PRESETS[configA.device]?.loss || 0, + rxGain: configB.antennaGain, + rxLoss: DEVICE_PRESETS[configB.device]?.loss || 0, + distanceKm: distKm, freqMHz: freq, - sf, bw + sf, bw, + excessLoss: diffraction, + fadeMargin: fadeMargin, }); - + const status = analysis.isObstructed ? 'OBSTRUCTED' : (budget.margin > 10 ? 'GOOD' : 'MARGINAL'); - + csvContent += `${n1.name},${n2.name},${distKm.toFixed(3)},${status},${analysis.linkQuality},${budget.margin},${analysis.minClearance}\n`; } } catch (e) { diff --git a/src/components/Map/MapContainer.jsx b/src/components/Map/MapContainer.jsx index e7bea28..42b58ae 100644 --- a/src/components/Map/MapContainer.jsx +++ b/src/components/Map/MapContainer.jsx @@ -13,7 +13,7 @@ import L from "leaflet"; import LinkLayer from "./LinkLayer"; import LinkAnalysisPanel from "./LinkAnalysisPanel"; import OptimizationLayer from "./OptimizationLayer"; -import { useRF } from "../../context/RFContext"; +import { useRF, GROUND_TYPES } from "../../context/RFContext"; import { calculateLinkBudget } from "../../utils/rfMath"; import { DEVICE_PRESETS } from "../../data/presets"; import * as turf from "@turf/turf"; @@ -140,7 +140,9 @@ const MapComponent = () => { getAntennaHeightMeters, calculateSensitivity, rxHeight, - fadeMargin + fadeMargin, + groundType, + climate } = useRF(); // Wasm Viewshed Tool Hook @@ -223,18 +225,22 @@ const MapComponent = () => { ? calculateSensitivity() : -126; + const ground = GROUND_TYPES[groundType] || GROUND_TYPES['Average Ground']; const rfParams = { freq, txPower: proxyTx, txGain: proxyGain, - txLoss: cableLoss, // Task 1.1: Use calculated cable loss - rxLoss: 0, // Default 0 for coverage map until Cable Calculator (Task 1.5) + txLoss: cableLoss, + rxLoss: 0, rxGain: nodeConfigs.B.antennaGain || 2.15, rxSensitivity: currentSensitivity, bw, sf, cr, rxHeight, + epsilon: ground.epsilon, + sigma: ground.sigma, + climate: climate, }; console.log( `[RF Recalc] Height: ${currentHeight.toFixed(2)}m, Params:`, @@ -515,10 +521,13 @@ const MapComponent = () => { cr, antennaHeight, getAntennaHeightMeters, + calculateSensitivity, rxHeight, - txLoss: cableLoss, // Task 1.1 + txLoss: cableLoss, rxLoss: 0, rxAntennaGain: nodeConfigs.B.antennaGain, + groundType, + climate, }} /> { const currentSensitivity = calculateSensitivity ? calculateSensitivity() : -126; + const dragGround = GROUND_TYPES[groundType] || GROUND_TYPES['Average Ground']; const rfParams = { freq, txPower: proxyTx, txGain: proxyGain, - rxGain: 2.15, + txLoss: cableLoss, + rxLoss: 0, + rxGain: nodeConfigs.B.antennaGain || 2.15, rxSensitivity: currentSensitivity, bw, sf, cr, rxHeight, + epsilon: dragGround.epsilon, + sigma: dragGround.sigma, + climate: climate, }; runRFAnalysis(lat, lng, h, 25000, rfParams); diff --git a/src/context/RFContext.jsx b/src/context/RFContext.jsx index b140aac..7a5f1cc 100644 --- a/src/context/RFContext.jsx +++ b/src/context/RFContext.jsx @@ -5,6 +5,7 @@ import { ANTENNA_PRESETS, CABLE_TYPES, } from "../data/presets"; +import { calculateLoRaSensitivity } from "../utils/rfMath"; // ITM Environment Constants export const GROUND_TYPES = { @@ -291,25 +292,7 @@ export const RFProvider = ({ children }) => { return parseFloat(antennaHeight) || 0; // State is always in Meters }, calculateSensitivity: () => { - // LoRa Sensitivity Estimation - // S = -174 + 10log10(BW) + NF + SNR_limit - // NF ~ 6dB typically - const bwHz = (bw || 125) * 1000; - const noiseFloor = -174 + 10 * Math.log10(bwHz); - const nf = 6; - - // SNR Limits approx (Semtech datasheets) - const snrLimits = { - 7: -7.5, - 8: -10, - 9: -12.5, - 10: -15, - 11: -17.5, - 12: -20, - }; - const snrLimit = snrLimits[sf] || -20; - - return noiseFloor + nf + snrLimit; + return calculateLoRaSensitivity(sf, bw); }, }; diff --git a/src/utils/rfConstants.js b/src/utils/rfConstants.js index c4e5a92..b1daa69 100644 --- a/src/utils/rfConstants.js +++ b/src/utils/rfConstants.js @@ -30,10 +30,17 @@ export const RF_CONSTANTS = { } }, - // LoRa / Hardware Constants + // LoRa / Hardware Constants (Semtech SX1262 datasheet, power-saving RX mode) LORA: { - BASE_SENSITIVITY_SF7_125KHZ: -123, // dBm - SF_GAIN_PER_STEP: 2.5, // dB + // Per-SF sensitivity at 125kHz BW (dBm) - SX1262 datasheet Table 3-2 + SENSITIVITY_125KHZ: { + 7: -124, + 8: -127, + 9: -130, + 10: -133, + 11: -135.5, + 12: -137, + }, THERMAL_NOISE_DENSITY: -174, // dBm/Hz REF_BW_KHZ: 125, REF_SF: 7, diff --git a/src/utils/rfMath.js b/src/utils/rfMath.js index a4952de..7b898ed 100644 --- a/src/utils/rfMath.js +++ b/src/utils/rfMath.js @@ -9,8 +9,8 @@ import { RF_CONSTANTS } from "./rfConstants"; */ export const calculateFSPL = (distanceKm, freqMHz) => { if (distanceKm <= 0) return 0; - // FSPL(dB) = 20log10(d) + 20log10(f) + 32.44 - return 20 * Math.log10(distanceKm) + 20 * Math.log10(freqMHz) + 32.44; + // FSPL(dB) = 20log10(d) + 20log10(f) + 32.45 (ITU-R P.525-4) + return 20 * Math.log10(distanceKm) + 20 * Math.log10(freqMHz) + 32.45; }; /** @@ -35,20 +35,37 @@ export const calculateFresnelRadius = ( return RF_CONSTANTS.FRESNEL.CONST_METERS * Math.sqrt((d1 * d2) / (fGHz * distanceKm)); }; +/** + * Calculate LoRa Receiver Sensitivity (canonical, SX1262 datasheet) + * Uses per-SF lookup table at 125kHz, scaled for actual bandwidth. + * @param {number} sf - Spreading Factor (7-12) + * @param {number} bw - Bandwidth in kHz + * @returns {number} Sensitivity in dBm + */ +export const calculateLoRaSensitivity = (sf, bw) => { + const table = RF_CONSTANTS.LORA.SENSITIVITY_125KHZ; + const baseSensitivity = table[sf] !== undefined ? table[sf] : table[7]; + + // Scale for bandwidth: 10*log10(BW/125) -- doubling BW worsens by 3 dB + const bwFactor = 10 * Math.log10((bw || 125) / RF_CONSTANTS.LORA.REF_BW_KHZ); + + return parseFloat((baseSensitivity + bwFactor).toFixed(1)); +}; + /** * Calculate Link Budget * @param {Object} params * @param {number} params.txPower - TX Power in dBm * @param {number} params.txGain - TX Antenna Gain in dBi * @param {number} params.txLoss - TX Cable Loss in dB - * @param {number} params.rxGain - RX Antenna Gain in dBi (Assuming symmetric for now or user defined) + * @param {number} params.rxGain - RX Antenna Gain in dBi * @param {number} params.rxLoss - RX Cable Loss in dB * @param {number} params.distanceKm - Distance in Km * @param {number} params.freqMHz - Frequency in MHz * @param {number} params.sf - Spreading Factor (for sensitivity) * @param {number} params.bw - Bandwidth in kHz (for sensitivity) - * @param {number} [params.pathLossOverride=null] - Optional override for path loss in dB. If provided, FSPL is not calculated. - * @returns {Object} { rssi, fspl, snrLimit, linkMargin } + * @param {number} [params.pathLossOverride=null] - Optional override for path loss in dB + * @returns {Object} { rssi, fspl, sensitivity, margin } */ export const calculateLinkBudget = ({ txPower, @@ -67,29 +84,10 @@ export const calculateLinkBudget = ({ }) => { const fspl = pathLossOverride !== null ? pathLossOverride : calculateFSPL(distanceKm, freqMHz); - // Estimated RSSI at receiver // RSSI = Ptx + Gtx - Ltx - PathLoss - ExcessLoss - FadeMargin + Grx - Lrx const rssi = txPower + txGain - txLoss - fspl - excessLoss - fadeMargin + rxGain - rxLoss; - // Receiver Sensitivity Calculation (Semtech SX1262 approx) - // S = -174 + 10log10(BW) + NF + SNR_limit - // Standard LoRa sensitivity approximation: - // SF7/125kHz ~ -123dBm - // Rule of thumb: Higher SF = Lower (better) sensitivity. Double BW = 3dB worse. - - // Base sensitivity for SF7, 125kHz - let baseSensitivity = RF_CONSTANTS.LORA.BASE_SENSITIVITY_SF7_125KHZ; - - // Adjust for Bandwidth: 10 * log10(BW_meas / BW_ref) - // If BW goes 125 -> 250, noise floor rises by 3dB, sensitivity worsens by 3dB - const bwFactor = 10 * Math.log10(bw / RF_CONSTANTS.LORA.REF_BW_KHZ); - - // Adjust for Spreading Factor: Each step adds ~2.5dB of process gain - // SF7 is base. SF12 is 5 steps higher. - const sfFactor = (sf - RF_CONSTANTS.LORA.REF_SF) * -RF_CONSTANTS.LORA.SF_GAIN_PER_STEP; - - const sensitiveLimit = baseSensitivity + bwFactor + sfFactor; - + const sensitiveLimit = calculateLoRaSensitivity(sf, bw); const linkMargin = rssi - sensitiveLimit; return {