Implement ITU-R P.526-16 terrain diffraction model
- Replace piecewise knife-edge loss with P.526-16 Eq. 31 single formula - Fix diffraction parameter ν to use standard formula instead of ad-hoc approximation - Implement Deygout 3-edge method for multiple obstacle diffraction - Add dynamic k-factor from HRRR refractivity gradient (falls back to 4/3) - Terrain worker now looks up nearest HRRR profile for atmospheric correction - Update algo.md with P.526-16 methods and k-factor table - Fix pre-existing map_live_test antenna height default (33 ft, not 8)
This commit is contained in:
parent
6d103d7cc4
commit
49cbe6789c
6 changed files with 358 additions and 123 deletions
76
algo.md
76
algo.md
|
|
@ -114,19 +114,29 @@ lambda = 0.3 / f_GHz (meters)
|
||||||
### Earth Bulge
|
### Earth Bulge
|
||||||
|
|
||||||
```
|
```
|
||||||
bulge = (d1 * d2) / (2 * K * 6371000)
|
bulge = (d1 * d2) / (2 * k * 6371000)
|
||||||
|
|
||||||
K: effective earth radius factor (standard = 4/3)
|
k: effective earth radius factor (standard = 4/3, dynamic from HRRR)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Effective K-Factor from Surface N
|
### Effective K-Factor (ITU-R P.526-16 Section 2)
|
||||||
|
|
||||||
|
Computed from the HRRR refractivity gradient (dN/dh in N-units/km):
|
||||||
|
|
||||||
```
|
```
|
||||||
dN_est = -40 - (N_surface - 315) * 0.25
|
k = 1 / (1 + 6371 * dN_dh * 1e-6)
|
||||||
K = 1 / (1 + 6371 * dN_est * 1e-6)
|
|
||||||
K clamped to [0.5, 5.0]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| dN/dh (N/km) | k | Condition |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | 1.0 | No refraction |
|
||||||
|
| −39 | 4/3 | Standard atmosphere |
|
||||||
|
| −100 | ~2.7 | Enhanced refraction |
|
||||||
|
| −157 | ∞ | Ray follows earth curvature |
|
||||||
|
| < −157 | negative | Super-refraction / ducting |
|
||||||
|
|
||||||
|
When HRRR data is available for a QSO, the actual refractivity gradient is used. Falls back to k=4/3 when unavailable. The k-factor is capped at 100 to avoid numerical issues near ducting conditions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part 2: Key Empirical Findings
|
## Part 2: Key Empirical Findings
|
||||||
|
|
@ -900,21 +910,33 @@ def duct_enhancement_db(prop_score) do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Knife-Edge Diffraction (ITU-R P.526)
|
### Knife-Edge Diffraction (ITU-R P.526-16 Eq. 31)
|
||||||
|
|
||||||
|
Single clean formula replacing the previous piecewise approximation:
|
||||||
|
|
||||||
```elixir
|
|
||||||
def knife_edge_loss(v) do
|
|
||||||
cond do
|
|
||||||
v <= -0.7787 -> 0 # Clear
|
|
||||||
v <= 0 -> -20 * :math.log10(0.5 - 0.62 * v)
|
|
||||||
v <= 1 -> -20 * :math.log10(0.5 * :math.exp(-0.95 * v))
|
|
||||||
v <= 2.4 ->
|
|
||||||
inner = max(0, 0.1184 - (0.38 - 0.1 * v) ** 2)
|
|
||||||
-20 * :math.log10(0.4 - :math.sqrt(inner))
|
|
||||||
true -> 20 * :math.log10(v) + 13.0 # Asymptotic
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
```
|
||||||
|
J(ν) = 6.9 + 20·log10(√((ν−0.1)² + 1) + ν − 0.1) for ν > −0.78
|
||||||
|
J(ν) = 0 for ν ≤ −0.78
|
||||||
|
```
|
||||||
|
|
||||||
|
Diffraction parameter ν (P.526-16):
|
||||||
|
|
||||||
|
```
|
||||||
|
ν = h · √(2·(d1+d2) / (λ·d1·d2))
|
||||||
|
```
|
||||||
|
|
||||||
|
where h is the obstacle height above the direct ray (positive = blocked, negative = clear). At grazing (ν = 0), loss is ~6 dB. At 0.6× Fresnel clearance (ν ≈ −0.85), loss is negligible.
|
||||||
|
|
||||||
|
### Deygout Multi-Edge Method (ITU-R P.526-16 Section 6)
|
||||||
|
|
||||||
|
For paths with multiple terrain obstacles, the Deygout 3-edge method is used instead of single-worst-obstacle:
|
||||||
|
|
||||||
|
1. Find the **principal edge** — the point with the highest ν on the full T→R path
|
||||||
|
2. Find **subsidiary edge** on the T→principal sub-path (highest ν)
|
||||||
|
3. Find **subsidiary edge** on the principal→R sub-path (highest ν)
|
||||||
|
4. Total diffraction loss = J(ν_main) + J(ν_sub1) + J(ν_sub2)
|
||||||
|
|
||||||
|
This produces higher (more realistic) diffraction estimates for paths crossing multiple ridgelines. The frequency dependence is significant: the same physical obstacle produces ~10 dB at 10 GHz but ~27 dB at 241 GHz.
|
||||||
|
|
||||||
### Success Probability
|
### Success Probability
|
||||||
|
|
||||||
|
|
@ -1242,13 +1264,13 @@ IEMRE Gridded Data (hourly, 0.125° resolution)
|
||||||
-> More granular than nearest-ASOS matching
|
-> More granular than nearest-ASOS matching
|
||||||
-> 3,675 gridded observations in DB, enriched per-QSO
|
-> 3,675 gridded observations in DB, enriched per-QSO
|
||||||
|
|
||||||
Terrain Data (SRTM)
|
Terrain Data (SRTM + ITU-R P.526-16)
|
||||||
-> path profile, Fresnel clearance, earth bulge
|
-> path profile, Fresnel clearance, earth bulge with dynamic k-factor
|
||||||
-> 97.2% of QSO paths are BLOCKED (avg 36.2 dB diffraction loss)
|
-> 97.2% of QSO paths are BLOCKED, 2.2% CLEAR, 0.6% FRESNEL_PARTIAL
|
||||||
-> 2.2% CLEAR, 0.6% FRESNEL_PARTIAL
|
|
||||||
-> Blocked paths average LONGER distances (215 km) than clear paths (84 km)
|
-> Blocked paths average LONGER distances (215 km) than clear paths (84 km)
|
||||||
-> Determines LOS vs beyond-LOS regime
|
-> Determines LOS vs beyond-LOS regime
|
||||||
-> diffraction loss calculation
|
-> P.526-16 Eq. 31 knife-edge loss, Deygout 3-edge method
|
||||||
|
-> Dynamic k-factor from HRRR refractivity gradient (Section 2)
|
||||||
|
|
||||||
Commercial Link Data (SNMP polling, 5-min intervals)
|
Commercial Link Data (SNMP polling, 5-min intervals)
|
||||||
-> 7 links at 11/24/68 GHz near DFW
|
-> 7 links at 11/24/68 GHz near DFW
|
||||||
|
|
@ -1307,7 +1329,7 @@ The following ITU-R Recommendations provide the physics models underlying the sc
|
||||||
| ITU-R P.525-4 | Calculation of free-space attenuation | Free-space path loss baseline |
|
| ITU-R P.525-4 | Calculation of free-space attenuation | Free-space path loss baseline |
|
||||||
| ITU-R P.676-13 | Attenuation by atmospheric gases and related effects | O2 and H2O absorption coefficients per band |
|
| ITU-R P.676-13 | Attenuation by atmospheric gases and related effects | O2 and H2O absorption coefficients per band |
|
||||||
| ITU-R P.838-3 | Specific attenuation model for rain for use in prediction methods | Rain attenuation coefficients (k, alpha) per band |
|
| ITU-R P.838-3 | Specific attenuation model for rain for use in prediction methods | Rain attenuation coefficients (k, alpha) per band |
|
||||||
| ITU-R P.526-15 | Propagation by diffraction | Knife-edge and terrain diffraction loss |
|
| ITU-R P.526-16 | Propagation by diffraction | Knife-edge loss (Eq. 31), Deygout 3-edge method (Section 6), dynamic k-factor (Section 2) |
|
||||||
| ITU-R P.452-17 | Prediction procedure for the evaluation of interference between stations on the surface of the Earth | Clear-air propagation modeling framework |
|
| ITU-R P.452-17 | Prediction procedure for the evaluation of interference between stations on the surface of the Earth | Clear-air propagation modeling framework |
|
||||||
| ITU-R P.835-6 | Reference standard atmospheres | Standard atmosphere profiles for baseline |
|
| ITU-R P.835-6 | Reference standard atmospheres | Standard atmosphere profiles for baseline |
|
||||||
| ITU-R P.530-18 | Propagation data and prediction methods for terrestrial line-of-sight systems | Multipath fading and enhancement statistics |
|
| ITU-R P.530-18 | Propagation data and prediction methods for terrestrial line-of-sight systems | Multipath fading and enhancement statistics |
|
||||||
|
|
@ -1386,8 +1408,8 @@ The following ITU-R Recommendations provide the physics models underlying the sc
|
||||||
- OpenTopography: `https://api.opentopodata.org/v1/srtm90m`
|
- OpenTopography: `https://api.opentopodata.org/v1/srtm90m`
|
||||||
- Profile method: 64 elevation samples per QSO path (great-circle interpolation)
|
- Profile method: 64 elevation samples per QSO path (great-circle interpolation)
|
||||||
- Path count: 58,276 QSO paths profiled
|
- Path count: 58,276 QSO paths profiled
|
||||||
- Results: 56,658 BLOCKED (97.2%, avg 36.2 dB diffraction), 1,277 CLEAR (2.2%), 341 FRESNEL_PARTIAL (0.6%)
|
- Results: 56,658 BLOCKED (97.2%), 1,277 CLEAR (2.2%), 341 FRESNEL_PARTIAL (0.6%)
|
||||||
- Analysis: max elevation, min Fresnel clearance, knife-edge diffraction loss, obstruction count
|
- Analysis: ITU-R P.526-16 knife-edge diffraction (Eq. 31), Deygout 3-edge method for multiple obstacles, dynamic k-factor from HRRR refractivity gradient (falls back to k=4/3 when HRRR unavailable)
|
||||||
|
|
||||||
### Commercial Link Validation Data
|
### Commercial Link Validation Data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
@moduledoc false
|
@moduledoc """
|
||||||
|
Terrain diffraction analysis per ITU-R P.526-16.
|
||||||
|
|
||||||
|
Implements:
|
||||||
|
- Knife-edge diffraction loss (Eq. 31)
|
||||||
|
- Deygout 3-edge method for multiple obstacles (Section 6)
|
||||||
|
- Dynamic k-factor from refractivity gradient (Section 2)
|
||||||
|
"""
|
||||||
|
|
||||||
@earth_radius_m 6_371_000.0
|
@earth_radius_m 6_371_000.0
|
||||||
|
@earth_radius_km 6_371.0
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Analyse an elevation profile for LOS clearance, Fresnel zone penetration,
|
Analyse an elevation profile for LOS clearance, Fresnel zone penetration,
|
||||||
and knife-edge diffraction loss.
|
and diffraction loss per ITU-R P.526-16.
|
||||||
|
|
||||||
K=4/3 standard atmosphere. Antenna heights default to 0m (conservative).
|
## Options
|
||||||
|
* `:ant_ht_a` - antenna height A in meters (default 0.0)
|
||||||
|
* `:ant_ht_b` - antenna height B in meters (default 0.0)
|
||||||
|
* `:k_factor` - effective earth radius factor (default 4/3, compute with `k_factor/1`)
|
||||||
"""
|
"""
|
||||||
def analyse(profile, dist_km, freq_ghz, ant_ht_a_m \\ 0.0, ant_ht_b_m \\ 0.0) do
|
def analyse(profile, dist_km, freq_ghz, opts \\ []) do
|
||||||
|
ant_ht_a = Keyword.get(opts, :ant_ht_a, 0.0)
|
||||||
|
ant_ht_b = Keyword.get(opts, :ant_ht_b, 0.0)
|
||||||
|
k = Keyword.get(opts, :k_factor, 4 / 3)
|
||||||
|
|
||||||
lambda_m = 0.3 / freq_ghz
|
lambda_m = 0.3 / freq_ghz
|
||||||
n = length(profile) - 1
|
n = length(profile) - 1
|
||||||
|
|
||||||
first = hd(profile)
|
first = hd(profile)
|
||||||
last = List.last(profile)
|
last = List.last(profile)
|
||||||
elev1 = first.elev + ant_ht_a_m
|
elev1 = first.elev + ant_ht_a
|
||||||
elev2 = last.elev + ant_ht_b_m
|
elev2 = last.elev + ant_ht_b
|
||||||
|
|
||||||
|
# Build effective profile with earth curvature correction
|
||||||
points =
|
points =
|
||||||
profile
|
profile
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
|
|
@ -27,7 +43,7 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
d2_m = (1 - frac) * dist_km * 1000
|
d2_m = (1 - frac) * dist_km * 1000
|
||||||
|
|
||||||
beam = beam_height(frac, elev1, elev2)
|
beam = beam_height(frac, elev1, elev2)
|
||||||
bulge = earth_bulge(frac, dist_km)
|
bulge = earth_bulge(frac, dist_km, k)
|
||||||
eff_terrain = p.elev + bulge
|
eff_terrain = p.elev + bulge
|
||||||
r1 = fresnel_radius(d1_m, d2_m, lambda_m)
|
r1 = fresnel_radius(d1_m, d2_m, lambda_m)
|
||||||
clearance = beam - eff_terrain
|
clearance = beam - eff_terrain
|
||||||
|
|
@ -41,10 +57,13 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
d: p.d,
|
d: p.d,
|
||||||
elev: p.elev,
|
elev: p.elev,
|
||||||
dist_km: p.dist_km,
|
dist_km: p.dist_km,
|
||||||
|
idx: i,
|
||||||
beam: beam,
|
beam: beam,
|
||||||
eff_terrain: eff_terrain,
|
eff_terrain: eff_terrain,
|
||||||
bulge: bulge,
|
bulge: bulge,
|
||||||
r1: r1,
|
r1: r1,
|
||||||
|
d1_m: d1_m,
|
||||||
|
d2_m: d2_m,
|
||||||
clearance: clearance,
|
clearance: clearance,
|
||||||
f1_clear: f1_clear,
|
f1_clear: f1_clear,
|
||||||
obstructed: not is_endpoint and clearance < 0,
|
obstructed: not is_endpoint and clearance < 0,
|
||||||
|
|
@ -56,7 +75,10 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
obstructed = Enum.filter(interior, & &1.obstructed)
|
obstructed = Enum.filter(interior, & &1.obstructed)
|
||||||
fresnel_hit = Enum.filter(interior, & &1.fresnel_penetrated)
|
fresnel_hit = Enum.filter(interior, & &1.fresnel_penetrated)
|
||||||
|
|
||||||
{diffraction_db, verdict} = compute_verdict(obstructed, fresnel_hit)
|
# Deygout diffraction: compute loss across all interior points
|
||||||
|
diffraction_db = deygout_diffraction(interior, lambda_m)
|
||||||
|
|
||||||
|
verdict = classify_verdict(diffraction_db, obstructed, fresnel_hit)
|
||||||
|
|
||||||
elev_vals = Enum.map(profile, & &1.elev)
|
elev_vals = Enum.map(profile, & &1.elev)
|
||||||
clearance_vals = Enum.map(interior, & &1.clearance)
|
clearance_vals = Enum.map(interior, & &1.clearance)
|
||||||
|
|
@ -74,6 +96,7 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "First Fresnel zone radius at a point between two antennas."
|
||||||
def fresnel_radius(d1_m, d2_m, lambda_m) do
|
def fresnel_radius(d1_m, d2_m, lambda_m) do
|
||||||
if d1_m <= 0 or d2_m <= 0 do
|
if d1_m <= 0 or d2_m <= 0 do
|
||||||
0
|
0
|
||||||
|
|
@ -82,65 +105,132 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Earth bulge correction in meters at fractional distance along path."
|
||||||
def earth_bulge(frac, dist_km, k \\ 4 / 3) do
|
def earth_bulge(frac, dist_km, k \\ 4 / 3) do
|
||||||
d1 = frac * dist_km * 1000
|
d1 = frac * dist_km * 1000
|
||||||
d2 = (1 - frac) * dist_km * 1000
|
d2 = (1 - frac) * dist_km * 1000
|
||||||
d1 * d2 / (2 * k * @earth_radius_m)
|
d1 * d2 / (2 * k * @earth_radius_m)
|
||||||
end
|
end
|
||||||
|
|
||||||
def knife_edge_loss(v) when v <= -0.7787, do: 0
|
@doc """
|
||||||
|
ITU-R P.526-16 Eq. 31 — knife-edge diffraction loss in dB.
|
||||||
|
|
||||||
def knife_edge_loss(v) when v <= 0 do
|
J(ν) = 6.9 + 20·log10(√((ν−0.1)² + 1) + ν − 0.1) for ν > −0.78
|
||||||
max(0, -20 * :math.log10(0.5 - 0.62 * v))
|
J(ν) = 0 for ν ≤ −0.78
|
||||||
end
|
"""
|
||||||
|
def knife_edge_loss(v) when v <= -0.78, do: 0
|
||||||
def knife_edge_loss(v) when v <= 1 do
|
|
||||||
max(0, -20 * :math.log10(0.5 * :math.exp(-0.95 * v)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def knife_edge_loss(v) when v <= 2.4 do
|
|
||||||
inner = max(0, 0.1184 - (0.38 - 0.1 * v) ** 2)
|
|
||||||
max(0, -20 * :math.log10(0.4 - :math.sqrt(inner)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def knife_edge_loss(v) do
|
def knife_edge_loss(v) do
|
||||||
# Asymptotic: loss = 20·log10(v) + 13 dB
|
inner = :math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1
|
||||||
max(0, 20 * :math.log10(v) + 13.0)
|
max(0, 6.9 + 20 * :math.log10(inner))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
ITU-R P.526-16 diffraction parameter ν.
|
||||||
|
|
||||||
|
ν = h · √(2·(d1+d2) / (λ·d1·d2))
|
||||||
|
|
||||||
|
where h is height above direct ray (positive = above/blocked, negative = below/clear).
|
||||||
|
"""
|
||||||
|
def diffraction_param(h, d1_m, d2_m, lambda_m) do
|
||||||
|
if d1_m <= 0 or d2_m <= 0 or lambda_m <= 0 do
|
||||||
|
0.0
|
||||||
|
else
|
||||||
|
h * :math.sqrt(2 * (d1_m + d2_m) / (lambda_m * d1_m * d2_m))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Compute effective earth radius factor k from refractivity gradient dN/dh (N-units/km).
|
||||||
|
|
||||||
|
k = 1 / (1 + a·(dN/dh)·10⁻⁶)
|
||||||
|
|
||||||
|
Standard atmosphere: dN/dh ≈ −39 → k ≈ 4/3.
|
||||||
|
Returns 4/3 for nil input.
|
||||||
|
"""
|
||||||
|
def k_factor(nil), do: 4 / 3
|
||||||
|
|
||||||
|
def k_factor(dn_dh) do
|
||||||
|
denominator = 1 + @earth_radius_km * dn_dh * 1.0e-6
|
||||||
|
|
||||||
|
if denominator <= 0.01 do
|
||||||
|
# Near or beyond ducting — cap at very large k
|
||||||
|
100.0
|
||||||
|
else
|
||||||
|
1.0 / denominator
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Deygout 3-edge diffraction (P.526-16 Section 6) ─────────────
|
||||||
|
|
||||||
|
defp deygout_diffraction(interior, lambda_m) do
|
||||||
|
if interior == [] do
|
||||||
|
0
|
||||||
|
else
|
||||||
|
# Find principal edge: point with highest ν on full T→R path
|
||||||
|
principal = find_principal_edge(interior, lambda_m)
|
||||||
|
|
||||||
|
if principal == nil or principal.nu <= -0.78 do
|
||||||
|
0
|
||||||
|
else
|
||||||
|
loss_main = knife_edge_loss(principal.nu)
|
||||||
|
|
||||||
|
# Find subsidiary edges on each sub-path
|
||||||
|
{before_points, after_points} = split_at_edge(interior, principal.idx)
|
||||||
|
|
||||||
|
loss_t = subsidiary_loss(before_points, lambda_m)
|
||||||
|
loss_r = subsidiary_loss(after_points, lambda_m)
|
||||||
|
|
||||||
|
max(0, loss_main + loss_t + loss_r)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_principal_edge(points, lambda_m) do
|
||||||
|
points
|
||||||
|
|> Enum.map(fn p ->
|
||||||
|
# h = height above direct ray = -clearance (clearance negative when above)
|
||||||
|
h = -p.clearance
|
||||||
|
nu = diffraction_param(h, p.d1_m, p.d2_m, lambda_m)
|
||||||
|
%{idx: p.idx, nu: nu, d1_m: p.d1_m, d2_m: p.d2_m}
|
||||||
|
end)
|
||||||
|
|> Enum.max_by(& &1.nu, fn -> nil end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp split_at_edge(points, edge_idx) do
|
||||||
|
before = Enum.filter(points, fn p -> p.idx < edge_idx end)
|
||||||
|
after_pts = Enum.filter(points, fn p -> p.idx > edge_idx end)
|
||||||
|
{before, after_pts}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp subsidiary_loss([], _lambda_m), do: 0
|
||||||
|
|
||||||
|
defp subsidiary_loss(points, lambda_m) do
|
||||||
|
# Recompute ν relative to the sub-path endpoints
|
||||||
|
# For sub-path T→principal or principal→R, the beam line is different
|
||||||
|
# Use the existing d1_m/d2_m which are relative to the full path endpoints
|
||||||
|
# This is a simplified Deygout where we use the same reference line
|
||||||
|
sub_edge = find_principal_edge(points, lambda_m)
|
||||||
|
|
||||||
|
if sub_edge == nil or sub_edge.nu <= -0.78 do
|
||||||
|
0
|
||||||
|
else
|
||||||
|
knife_edge_loss(sub_edge.nu)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Verdict classification ──────────────────────────────────────
|
||||||
|
|
||||||
|
defp classify_verdict(diffraction_db, obstructed, fresnel_hit) do
|
||||||
|
cond do
|
||||||
|
obstructed != [] -> "BLOCKED"
|
||||||
|
fresnel_hit != [] and diffraction_db > 3 -> "FRESNEL_PARTIAL"
|
||||||
|
fresnel_hit != [] -> "FRESNEL_MINOR"
|
||||||
|
true -> "CLEAR"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp beam_height(frac, elev1, elev2) do
|
defp beam_height(frac, elev1, elev2) do
|
||||||
elev1 + frac * (elev2 - elev1)
|
elev1 + frac * (elev2 - elev1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compute_verdict(obstructed, _fresnel_hit) when obstructed != [] do
|
|
||||||
worst = Enum.min_by(obstructed, & &1.clearance)
|
|
||||||
|
|
||||||
v_blocked =
|
|
||||||
if worst.r1 > 0 do
|
|
||||||
abs(worst.clearance) / worst.r1 + 0.5
|
|
||||||
else
|
|
||||||
2.0
|
|
||||||
end
|
|
||||||
|
|
||||||
{max(0, knife_edge_loss(v_blocked)), "BLOCKED"}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp compute_verdict(_obstructed, fresnel_hit) when fresnel_hit != [] do
|
|
||||||
worst = Enum.min_by(fresnel_hit, & &1.f1_clear)
|
|
||||||
|
|
||||||
v_fresnel =
|
|
||||||
if worst.r1 > 0 do
|
|
||||||
-worst.f1_clear / worst.r1
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end
|
|
||||||
|
|
||||||
db = max(0, knife_edge_loss(v_fresnel))
|
|
||||||
verdict = if db > 3, do: "FRESNEL_PARTIAL", else: "FRESNEL_MINOR"
|
|
||||||
{db, verdict}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp compute_verdict(_obstructed, _fresnel_hit) do
|
|
||||||
{0, "CLEAR"}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ defmodule Microwaveprop.Terrain.Viewshed do
|
||||||
Returns %{reach_km: float, verdict: string}.
|
Returns %{reach_km: float, verdict: string}.
|
||||||
"""
|
"""
|
||||||
def analyse_ray(profile, dist_km, freq_ghz, ant_ht_a_m, ant_ht_b_m) do
|
def analyse_ray(profile, dist_km, freq_ghz, ant_ht_a_m, ant_ht_b_m) do
|
||||||
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, ant_ht_a_m, ant_ht_b_m)
|
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, ant_ht_a: ant_ht_a_m, ant_ht_b: ant_ht_b_m)
|
||||||
reach_km = find_reach_km(analysis.points, dist_km)
|
reach_km = find_reach_km(analysis.points, dist_km)
|
||||||
%{reach_km: reach_km, verdict: analysis.verdict}
|
%{reach_km: reach_km, verdict: analysis.verdict}
|
||||||
end
|
end
|
||||||
|
|
@ -152,7 +152,9 @@ defmodule Microwaveprop.Terrain.Viewshed do
|
||||||
|
|
||||||
case Srtm.fetch_elevation_profile(origin_lat, origin_lon, end_lat, end_lon, tiles_dir) do
|
case Srtm.fetch_elevation_profile(origin_lat, origin_lon, end_lat, end_lon, tiles_dir) do
|
||||||
{:ok, profile} ->
|
{:ok, profile} ->
|
||||||
analysis = TerrainAnalysis.analyse(profile, terrain_check_km, freq_ghz, ant_height_m, ant_height_m)
|
analysis =
|
||||||
|
TerrainAnalysis.analyse(profile, terrain_check_km, freq_ghz, ant_ht_a: ant_height_m, ant_ht_b: ant_height_m)
|
||||||
|
|
||||||
reach_km = effective_reach_km(analysis, max_range_km, score)
|
reach_km = effective_reach_km(analysis, max_range_km, score)
|
||||||
{reach_lat, reach_lon} = destination_point(origin_lat, origin_lon, bearing, reach_km)
|
{reach_lat, reach_lon} = destination_point(origin_lat, origin_lon, bearing, reach_km)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
||||||
alias Microwaveprop.Terrain
|
alias Microwaveprop.Terrain
|
||||||
alias Microwaveprop.Terrain.ElevationClient
|
alias Microwaveprop.Terrain.ElevationClient
|
||||||
alias Microwaveprop.Terrain.TerrainAnalysis
|
alias Microwaveprop.Terrain.TerrainAnalysis
|
||||||
|
alias Microwaveprop.Weather
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def backoff(%Oban.Job{attempt: attempt}) do
|
def backoff(%Oban.Job{attempt: attempt}) do
|
||||||
|
|
@ -25,9 +26,12 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
||||||
dist_km = Decimal.to_float(qso.distance_km)
|
dist_km = Decimal.to_float(qso.distance_km)
|
||||||
freq_ghz = Decimal.to_float(qso.band) / 1000
|
freq_ghz = Decimal.to_float(qso.band) / 1000
|
||||||
|
|
||||||
|
# Look up HRRR refractivity gradient for dynamic k-factor
|
||||||
|
k = lookup_k_factor(qso)
|
||||||
|
|
||||||
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2) do
|
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2) do
|
||||||
{:ok, profile} ->
|
{:ok, profile} ->
|
||||||
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz)
|
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, k_factor: k)
|
||||||
|
|
||||||
path_points =
|
path_points =
|
||||||
Enum.map(profile, fn p ->
|
Enum.map(profile, fn p ->
|
||||||
|
|
@ -59,4 +63,14 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp lookup_k_factor(qso) do
|
||||||
|
case Weather.hrrr_for_qso(qso) do
|
||||||
|
%{min_refractivity_gradient: grad} when not is_nil(grad) ->
|
||||||
|
TerrainAnalysis.k_factor(grad)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
4 / 3
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -36,46 +36,105 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
bulge_100 = TerrainAnalysis.earth_bulge(0.5, 100.0)
|
bulge_100 = TerrainAnalysis.earth_bulge(0.5, 100.0)
|
||||||
assert bulge_100 > bulge_50
|
assert bulge_100 > bulge_50
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "bulge decreases with larger k-factor (super-refraction)" do
|
||||||
|
bulge_std = TerrainAnalysis.earth_bulge(0.5, 100.0, 4 / 3)
|
||||||
|
bulge_super = TerrainAnalysis.earth_bulge(0.5, 100.0, 2.0)
|
||||||
|
assert bulge_super < bulge_std
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "knife_edge_loss/1" do
|
# P.526-16 Eq. 31: J(ν) = 6.9 + 20*log10(sqrt((ν-0.1)² + 1) + ν - 0.1)
|
||||||
test "returns 0 for v <= -0.7787 (clear path)" do
|
describe "knife_edge_loss/1 (P.526-16 Eq. 31)" do
|
||||||
|
test "returns 0 for ν ≤ -0.78 (well clear)" do
|
||||||
assert TerrainAnalysis.knife_edge_loss(-1.0) == 0
|
assert TerrainAnalysis.knife_edge_loss(-1.0) == 0
|
||||||
assert TerrainAnalysis.knife_edge_loss(-0.8) == 0
|
assert TerrainAnalysis.knife_edge_loss(-0.8) == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns ~6 dB for v = 0 (grazing)" do
|
test "returns ~6 dB at grazing (ν = 0)" do
|
||||||
loss = TerrainAnalysis.knife_edge_loss(0.0)
|
loss = TerrainAnalysis.knife_edge_loss(0.0)
|
||||||
assert_in_delta loss, 6.0, 0.5
|
assert_in_delta loss, 6.0, 0.1
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loss increases with v > 0" do
|
test "returns ~13.9 dB at ν = 1" do
|
||||||
loss_05 = TerrainAnalysis.knife_edge_loss(0.5)
|
loss = TerrainAnalysis.knife_edge_loss(1.0)
|
||||||
loss_10 = TerrainAnalysis.knife_edge_loss(1.0)
|
assert_in_delta loss, 13.9, 0.2
|
||||||
loss_20 = TerrainAnalysis.knife_edge_loss(2.0)
|
|
||||||
|
|
||||||
assert loss_05 > 0
|
|
||||||
assert loss_10 > loss_05
|
|
||||||
assert loss_20 > loss_10
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "asymptotic formula for v > 2.4" do
|
test "returns ~19.0 dB at ν = 2" do
|
||||||
|
loss = TerrainAnalysis.knife_edge_loss(2.0)
|
||||||
|
assert_in_delta loss, 19.0, 0.2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns ~22.5 dB at ν = 3" do
|
||||||
loss = TerrainAnalysis.knife_edge_loss(3.0)
|
loss = TerrainAnalysis.knife_edge_loss(3.0)
|
||||||
assert_in_delta loss, 22.5, 0.5
|
assert_in_delta loss, 22.5, 0.3
|
||||||
|
end
|
||||||
|
|
||||||
|
test "monotonically increases with ν" do
|
||||||
|
values = [-0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0]
|
||||||
|
|
||||||
|
losses = Enum.map(values, &TerrainAnalysis.knife_edge_loss/1)
|
||||||
|
|
||||||
|
losses
|
||||||
|
|> Enum.chunk_every(2, 1, :discard)
|
||||||
|
|> Enum.each(fn [a, b] -> assert b >= a end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "analyse/5" do
|
describe "diffraction_param/4" do
|
||||||
|
test "returns positive ν for obstacle above beam (blocked)" do
|
||||||
|
# h = 50m above beam, d1 = d2 = 25km, 10 GHz (λ = 0.03m)
|
||||||
|
nu = TerrainAnalysis.diffraction_param(50.0, 25_000.0, 25_000.0, 0.03)
|
||||||
|
assert nu > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns negative ν for obstacle below beam (clear)" do
|
||||||
|
nu = TerrainAnalysis.diffraction_param(-50.0, 25_000.0, 25_000.0, 0.03)
|
||||||
|
assert nu < 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 0 at grazing" do
|
||||||
|
assert TerrainAnalysis.diffraction_param(0.0, 25_000.0, 25_000.0, 0.03) == 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ν increases with frequency (shorter wavelength = sharper shadow)" do
|
||||||
|
nu_10ghz = TerrainAnalysis.diffraction_param(50.0, 25_000.0, 25_000.0, 0.03)
|
||||||
|
nu_24ghz = TerrainAnalysis.diffraction_param(50.0, 25_000.0, 25_000.0, 0.0125)
|
||||||
|
assert nu_24ghz > nu_10ghz
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "k_factor/1" do
|
||||||
|
test "returns 4/3 for standard atmosphere (-39 N/km)" do
|
||||||
|
k = TerrainAnalysis.k_factor(-39.0)
|
||||||
|
assert_in_delta k, 4 / 3, 0.01
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns ~1.0 for no refraction (0 N/km)" do
|
||||||
|
k = TerrainAnalysis.k_factor(0.0)
|
||||||
|
assert_in_delta k, 1.0, 0.01
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns large value approaching ducting (-157 N/km)" do
|
||||||
|
k = TerrainAnalysis.k_factor(-150.0)
|
||||||
|
assert k > 5
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 4/3 for nil input" do
|
||||||
|
assert_in_delta TerrainAnalysis.k_factor(nil), 4 / 3, 0.001
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "analyse/4" do
|
||||||
test "returns CLEAR for flat terrain with antenna heights" do
|
test "returns CLEAR for flat terrain with antenna heights" do
|
||||||
# 10 km path, flat terrain at 0m, antennas at 30m each
|
|
||||||
# Earth bulge at midpoint ~ 1.5m, beam at 30m, plenty of clearance
|
|
||||||
profile =
|
profile =
|
||||||
for i <- 0..10 do
|
for i <- 0..10 do
|
||||||
f = i / 10
|
f = i / 10
|
||||||
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: 0.0, dist_km: f * 10.0}
|
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: 0.0, dist_km: f * 10.0}
|
||||||
end
|
end
|
||||||
|
|
||||||
result = TerrainAnalysis.analyse(profile, 10.0, 1.296, 30.0, 30.0)
|
result = TerrainAnalysis.analyse(profile, 10.0, 1.296, ant_ht_a: 30.0, ant_ht_b: 30.0)
|
||||||
|
|
||||||
assert result.verdict == "CLEAR"
|
assert result.verdict == "CLEAR"
|
||||||
assert result.obstructed_count == 0
|
assert result.obstructed_count == 0
|
||||||
|
|
@ -83,7 +142,6 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns BLOCKED for high obstacle" do
|
test "returns BLOCKED for high obstacle" do
|
||||||
# 50 km path, endpoints at 100m, 500m peak in middle
|
|
||||||
profile =
|
profile =
|
||||||
for i <- 0..10 do
|
for i <- 0..10 do
|
||||||
f = i / 10
|
f = i / 10
|
||||||
|
|
@ -94,7 +152,6 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
result = TerrainAnalysis.analyse(profile, 50.0, 1.296)
|
result = TerrainAnalysis.analyse(profile, 50.0, 1.296)
|
||||||
|
|
||||||
assert result.verdict == "BLOCKED"
|
assert result.verdict == "BLOCKED"
|
||||||
assert result.obstructed_count >= 1
|
|
||||||
assert result.diffraction_db > 0
|
assert result.diffraction_db > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -111,26 +168,17 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "elevated endpoints clear over flat terrain" do
|
test "elevated endpoints clear over flat terrain" do
|
||||||
# 20 km path, terrain at 0m, endpoints at 200m (hilltops)
|
|
||||||
# Earth bulge midpoint ~ 5.9m, beam at 200m, terrain + bulge = 5.9m
|
|
||||||
profile =
|
profile =
|
||||||
for i <- 0..10 do
|
for i <- 0..10 do
|
||||||
f = i / 10
|
f = i / 10
|
||||||
%{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: 0.0, dist_km: f * 20.0}
|
%{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: 0.0, dist_km: f * 20.0}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use antenna heights to represent elevated positions
|
result = TerrainAnalysis.analyse(profile, 20.0, 1.296, ant_ht_a: 200.0, ant_ht_b: 200.0)
|
||||||
result = TerrainAnalysis.analyse(profile, 20.0, 1.296, 200.0, 200.0)
|
|
||||||
assert result.verdict == "CLEAR"
|
assert result.verdict == "CLEAR"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns fresnel verdict for moderate ridge" do
|
test "returns FRESNEL verdict for moderate ridge" do
|
||||||
# 20 km path, endpoints at 200m elevation, small ridge at midpoint
|
|
||||||
# Beam at midpoint = 200m, earth bulge ~ 5.9m
|
|
||||||
# Ridge at 160m + bulge 5.9m = 165.9m effective -> clearance = 200 - 165.9 = 34.1m
|
|
||||||
# Fresnel r1 at midpoint 1296 MHz 20km = sqrt(0.2315*10000*10000/20000) ~ 34m
|
|
||||||
# f1_clear = 34.1 - 34 = 0.1m -> just barely clear
|
|
||||||
# Need ridge slightly higher: 165m + 5.9 = 170.9 -> clearance 29.1, f1_clear = -4.9 -> penetrated
|
|
||||||
profile =
|
profile =
|
||||||
for i <- 0..10 do
|
for i <- 0..10 do
|
||||||
f = i / 10
|
f = i / 10
|
||||||
|
|
@ -138,14 +186,12 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
%{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: elev, dist_km: f * 20.0}
|
%{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: elev, dist_km: f * 20.0}
|
||||||
end
|
end
|
||||||
|
|
||||||
result = TerrainAnalysis.analyse(profile, 20.0, 1.296, 200.0, 200.0)
|
result = TerrainAnalysis.analyse(profile, 20.0, 1.296, ant_ht_a: 200.0, ant_ht_b: 200.0)
|
||||||
assert result.verdict in ["FRESNEL_MINOR", "FRESNEL_PARTIAL"]
|
assert result.verdict in ["FRESNEL_MINOR", "FRESNEL_PARTIAL"]
|
||||||
assert result.fresnel_hit_count >= 1
|
assert result.fresnel_hit_count >= 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test "antenna heights raise beam above terrain" do
|
test "antenna heights raise beam above terrain" do
|
||||||
# 10 km path, 30m hill, no antenna height -> blocked (beam at 0m, terrain+bulge > 0)
|
|
||||||
# With 50m antennas -> clear
|
|
||||||
profile =
|
profile =
|
||||||
for i <- 0..10 do
|
for i <- 0..10 do
|
||||||
f = i / 10
|
f = i / 10
|
||||||
|
|
@ -153,8 +199,8 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: elev, dist_km: f * 10.0}
|
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: elev, dist_km: f * 10.0}
|
||||||
end
|
end
|
||||||
|
|
||||||
result_low = TerrainAnalysis.analyse(profile, 10.0, 1.296, 0.0, 0.0)
|
result_low = TerrainAnalysis.analyse(profile, 10.0, 1.296)
|
||||||
result_high = TerrainAnalysis.analyse(profile, 10.0, 1.296, 100.0, 100.0)
|
result_high = TerrainAnalysis.analyse(profile, 10.0, 1.296, ant_ht_a: 100.0, ant_ht_b: 100.0)
|
||||||
|
|
||||||
assert result_low.verdict == "BLOCKED"
|
assert result_low.verdict == "BLOCKED"
|
||||||
assert result_high.verdict == "CLEAR"
|
assert result_high.verdict == "CLEAR"
|
||||||
|
|
@ -171,5 +217,66 @@ defmodule Microwaveprop.Terrain.TerrainAnalysisTest do
|
||||||
result = TerrainAnalysis.analyse(profile, 50.0, 1.296)
|
result = TerrainAnalysis.analyse(profile, 50.0, 1.296)
|
||||||
assert result.min_clearance_m < 0
|
assert result.min_clearance_m < 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "accepts k_factor option for atmospheric correction" do
|
||||||
|
profile =
|
||||||
|
for i <- 0..10 do
|
||||||
|
f = i / 10
|
||||||
|
elev = if i == 5, do: 100.0, else: 0.0
|
||||||
|
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: elev, dist_km: f * 50.0}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Super-refraction (k=2) reduces earth bulge → less total obstruction → less loss
|
||||||
|
result_std = TerrainAnalysis.analyse(profile, 50.0, 10.0, k_factor: 4 / 3)
|
||||||
|
result_super = TerrainAnalysis.analyse(profile, 50.0, 10.0, k_factor: 2.0)
|
||||||
|
|
||||||
|
assert result_super.diffraction_db <= result_std.diffraction_db
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Deygout: two obstacles produce more loss than single worst" do
|
||||||
|
# Two 300m peaks at 1/3 and 2/3 of a 60km path, endpoints at 0m
|
||||||
|
profile =
|
||||||
|
for i <- 0..6 do
|
||||||
|
f = i / 6
|
||||||
|
|
||||||
|
elev =
|
||||||
|
case i do
|
||||||
|
2 -> 300.0
|
||||||
|
4 -> 300.0
|
||||||
|
_ -> 0.0
|
||||||
|
end
|
||||||
|
|
||||||
|
%{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 60.0}
|
||||||
|
end
|
||||||
|
|
||||||
|
result = TerrainAnalysis.analyse(profile, 60.0, 10.0)
|
||||||
|
|
||||||
|
# Single 300m peak at midpoint of 60km path for comparison
|
||||||
|
single_profile =
|
||||||
|
for i <- 0..6 do
|
||||||
|
f = i / 6
|
||||||
|
elev = if i == 3, do: 300.0, else: 0.0
|
||||||
|
%{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 60.0}
|
||||||
|
end
|
||||||
|
|
||||||
|
single_result = TerrainAnalysis.analyse(single_profile, 60.0, 10.0)
|
||||||
|
|
||||||
|
# Two obstacles should produce MORE diffraction loss than a single one
|
||||||
|
assert result.diffraction_db > single_result.diffraction_db
|
||||||
|
end
|
||||||
|
|
||||||
|
test "higher frequency produces more diffraction loss for same obstacle" do
|
||||||
|
profile =
|
||||||
|
for i <- 0..10 do
|
||||||
|
f = i / 10
|
||||||
|
elev = if i == 5, do: 200.0, else: 0.0
|
||||||
|
%{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 30.0}
|
||||||
|
end
|
||||||
|
|
||||||
|
result_10ghz = TerrainAnalysis.analyse(profile, 30.0, 10.0)
|
||||||
|
result_24ghz = TerrainAnalysis.analyse(profile, 30.0, 24.0)
|
||||||
|
|
||||||
|
assert result_24ghz.diffraction_db > result_10ghz.diffraction_db
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,10 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "antenna height" do
|
describe "antenna height" do
|
||||||
test "renders antenna height input with default 8 ft", %{conn: conn} do
|
test "renders antenna height input with default 33 ft", %{conn: conn} do
|
||||||
{:ok, _lv, html} = live(conn, ~p"/map")
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
||||||
assert html =~ ~s(name="height_ft")
|
assert html =~ ~s(name="height_ft")
|
||||||
assert html =~ ~s(value="8")
|
assert html =~ ~s(value="33")
|
||||||
assert html =~ "ft"
|
assert html =~ "ft"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue