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
426 changes: 426 additions & 0 deletions Documentation/ITM-INTEGRATION-REVIEW.md

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions Documentation/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion rf-engine/rf_physics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions rf-engine/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 30 additions & 20 deletions src/components/Map/BatchProcessing.jsx
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 21 additions & 6 deletions src/components/Map/MapContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -140,7 +140,9 @@ const MapComponent = () => {
getAntennaHeightMeters,
calculateSensitivity,
rxHeight,
fadeMargin
fadeMargin,
groundType,
climate
} = useRF();

// Wasm Viewshed Tool Hook
Expand Down Expand Up @@ -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:`,
Expand Down Expand Up @@ -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,
}}
/>
<TileLayer
Expand Down Expand Up @@ -618,16 +627,22 @@ const MapComponent = () => {
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);
Expand Down
21 changes: 2 additions & 19 deletions src/context/RFContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ANTENNA_PRESETS,
CABLE_TYPES,
} from "../data/presets";
import { calculateLoRaSensitivity } from "../utils/rfMath";

// ITM Environment Constants
export const GROUND_TYPES = {
Expand Down Expand Up @@ -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);
},
};

Expand Down
13 changes: 10 additions & 3 deletions src/utils/rfConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading