Files
ruvnet--RuView/examples/research-sota/08-verticals/r10_foliage_attenuation.py
T
rUv 4e879bf62a chore: organise examples/research-sota/ into 9 thematic folders with READMEs (#744)
User request: organise examples/research-sota/ into folders with READMEs and main overview.

Moved 46 files into 9 thematic folders by thread family + research category:

01-physics-floor/      (R1, R6, R6.1) — bedrock primitives
02-placement/          (R6.2 family, 7 sub-ticks) — antenna placement
03-spatial-intelligence/ (R5, R7) — saliency + mincut
04-rssi/               (R8, R9) — RSSI-only sensing
05-cross-room-reid/    (R3 arc, 3 ticks) — cross-room identity
06-structure-detection/ (R12 arc, 3 ticks) — PABS + closed loop
07-negative-results/   (R13) — productive failure
08-verticals/          (R10, R11) — wildlife + maritime physics
09-quantum-fusion/     (R20.1) — ADR-114 quantum-classical demo

Each folder has its own README.md documenting:
- Scripts + headlines table
- Why this folder bounds / composes with others
- Sample output / honest scope
- Cross-references to related loop notes + ADRs

Main README.md at the top covers:
- Folder map with thread numbers
- Cross-folder dependency graph
- Headline findings table (8 entries)
- Reading order for newcomers (4 scripts in suggested order)
- Honest scope (synthetic-physics caveats)

All git mv operations preserve file history. Total: 46 files moved, 10
new READMEs (main + 9 sub) totalling ~1300 lines of organising
documentation.
2026-05-22 07:52:57 -04:00

168 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""R10 — through-foliage WiFi attenuation curves (ITU-R P.833 + per-species gait).
See docs/research/sota-2026-05-22/R10-through-foliage-wildlife.md.
Plots the ITU-R P.833 vegetation specific attenuation A_v over distance
for 2.4 GHz and 5 GHz CSI bands across three foliage densities. Compares
to a 1×1 SISO ESP32-S3's link budget to derive a maximum sensing range.
Pure NumPy, no plotting libs — emits a JSON file with the curves so a
downstream consumer can render them.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
def itu_p833_attenuation(freq_ghz: float, distance_m: float, foliage_density: str) -> float:
"""ITU-R P.833 specific-attenuation model for in-foliage propagation.
Simplified parameterisation:
A_max = max attenuation through dense canopy (dB)
gamma = decay coefficient (1/m)
A_v(d) = A_max * (1 - exp(-gamma * d))
Realistic A_max / gamma per density (calibrated against in-leaf summer
deciduous from ITU-R P.833-9 Table 1 + simulation studies):
sparse (orchard, savanna) A_max=20 dB, gamma=0.10
moderate (suburban tree cover) A_max=35 dB, gamma=0.20
dense (rainforest canopy) A_max=50 dB, gamma=0.35
The constant gets multiplied by sqrt(f_GHz / 1) for frequency scaling.
"""
params = {
"sparse": (20.0, 0.10),
"moderate": (35.0, 0.20),
"dense": (50.0, 0.35),
}
a_max, gamma = params[foliage_density]
freq_scaling = np.sqrt(freq_ghz) # higher freq → more attenuation
return a_max * freq_scaling * (1.0 - np.exp(-gamma * distance_m))
def esp32_link_budget(freq_ghz: float) -> dict[str, float]:
"""ESP32-S3 1x1 SISO link budget at 2.4 / 5 GHz.
Numbers from Espressif ESP32-S3 datasheet + standard WiFi specs:
Tx power (max regulatory) +20 dBm (100 mW, FCC Part 15)
Tx antenna gain (PCB) +2 dBi
Rx antenna gain (PCB) +2 dBi
Rx sensitivity (HT20, MCS0) -97 dBm
Total link budget (free-space) = (20 + 2 + 2) - (-97) = 121 dB
"""
return {
"tx_power_dbm": 20.0,
"tx_gain_dbi": 2.0,
"rx_gain_dbi": 2.0,
"rx_sensitivity_dbm": -97.0,
"link_budget_db": 121.0,
}
def fspl_db(freq_ghz: float, distance_m: float) -> float:
"""Free-space path loss in dB. FSPL = 20·log10(4π·d/λ)
With f in GHz + d in m: FSPL = 32.45 + 20·log10(f) + 20·log10(d)"""
if distance_m <= 0: return 0.0
return 32.45 + 20 * np.log10(freq_ghz) + 20 * np.log10(distance_m)
def max_sensing_range(freq_ghz: float, foliage_density: str, snr_margin_db: float = 10.0) -> float:
"""Distance at which FSPL + foliage_attenuation = link_budget - snr_margin.
Numerical solve by binary search. Returns metres."""
lb = esp32_link_budget(freq_ghz)
budget = lb["link_budget_db"] - snr_margin_db # require SNR > snr_margin
lo, hi = 0.1, 1000.0
for _ in range(60):
mid = (lo + hi) / 2
total_loss = fspl_db(freq_ghz, mid) + itu_p833_attenuation(freq_ghz, mid, foliage_density)
if total_loss < budget:
lo = mid
else:
hi = mid
return (lo + hi) / 2
def gait_frequency_band(species: str) -> dict[str, float]:
"""Approximate gait stride-frequency bands per species class, from
biomechanics literature (Schmitt 2003, Gambaryan 1974, Heglund 1988).
These are the temporal frequencies a CSI motion-band filter would
target — for context, human walking is ~1.7 Hz, jogging ~2.5 Hz."""
bands = {
"human-walking": {"min_hz": 1.2, "max_hz": 2.5},
"deer": {"min_hz": 1.8, "max_hz": 4.0},
"wolf": {"min_hz": 1.5, "max_hz": 3.5},
"bear": {"min_hz": 0.5, "max_hz": 1.5},
"fox": {"min_hz": 2.0, "max_hz": 4.5},
"squirrel": {"min_hz": 4.0, "max_hz": 10.0},
"mouse": {"min_hz": 5.0, "max_hz": 15.0},
"raccoon": {"min_hz": 1.5, "max_hz": 3.5},
"wild-boar": {"min_hz": 1.0, "max_hz": 2.5},
"elk": {"min_hz": 1.5, "max_hz": 3.0},
}
return bands.get(species, {"min_hz": 0.5, "max_hz": 10.0})
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--out", default="examples/research-sota/r10_foliage_results.json")
args = parser.parse_args()
distances = np.array([1, 2, 5, 10, 20, 50, 100, 200], dtype=np.float64)
freqs = [2.4, 5.0]
densities = ["sparse", "moderate", "dense"]
curves = {}
for freq in freqs:
curves[str(freq)] = {}
for density in densities:
atts = [float(itu_p833_attenuation(freq, d, density)) for d in distances]
fspls = [float(fspl_db(freq, d)) for d in distances]
curves[str(freq)][density] = {
"distance_m": distances.tolist(),
"foliage_attenuation_db": atts,
"fspl_db": fspls,
"total_loss_db": [a + f for a, f in zip(atts, fspls)],
}
# Max sensing range per (freq, density)
max_ranges = {}
for freq in freqs:
max_ranges[str(freq)] = {d: float(max_sensing_range(freq, d)) for d in densities}
species_gaits = {s: gait_frequency_band(s) for s in
["human-walking", "deer", "wolf", "bear", "fox",
"squirrel", "mouse", "raccoon", "wild-boar", "elk"]}
out = {
"model": "ITU-R P.833-9 specific-attenuation + free-space-path-loss",
"link_budget": esp32_link_budget(2.4),
"snr_margin_db": 10.0,
"curves": curves,
"max_sensing_range_m": max_ranges,
"species_gait_bands_hz": species_gaits,
}
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
Path(args.out).write_text(json.dumps(out, indent=2))
print("=== ESP32-S3 through-foliage sensing range (link budget 121 dB, 10 dB SNR margin) ===")
print(f"{'freq (GHz)':>10} {'sparse':>9} {'moderate':>11} {'dense':>9}")
for freq in freqs:
row = f"{freq:>10.1f} "
for d in densities:
row += f"{max_ranges[str(freq)][d]:>9.1f}m " if d != "moderate" else f"{max_ranges[str(freq)][d]:>11.1f}m "
print(row)
print()
print("=== Per-species gait frequency bands (Hz) ===")
for s, b in species_gaits.items():
print(f" {s:<16} {b['min_hz']:.1f} - {b['max_hz']:.1f} Hz")
print()
print(f"Wrote {args.out}")
if __name__ == "__main__":
main()