605 lines
16 KiB
Markdown
605 lines
16 KiB
Markdown
# Формулы и расчёты RadioApi
|
||
|
||
Документ описывает расчётные модели, формулы и ограничения, реализованные в **RadioApi**.
|
||
|
||
**Единицы по умолчанию**
|
||
|
||
| Величина | Единица |
|
||
|----------|---------|
|
||
| Координаты | градусы WGS84 (`lat`, `lon`) |
|
||
| Расстояние | м или км |
|
||
| Высота | м AGL или м AMSL |
|
||
| Частота | МГц в API, Гц/ГГц во внутренних моделях |
|
||
| Мощность | дБм |
|
||
| Усиление | дБи |
|
||
| Потери | дБ |
|
||
|
||
Префикс API: `/api/v1`.
|
||
|
||
## 1. Геометрия и профиль пути
|
||
|
||
Файл: `api/app/core/geo.py`
|
||
|
||
### Haversine
|
||
|
||
Приближённое расстояние по сфере:
|
||
|
||
$$
|
||
a = \sin^2\frac{\Delta\varphi}{2}
|
||
+ \cos\varphi_1 \cos\varphi_2 \sin^2\frac{\Delta\lambda}{2}
|
||
$$
|
||
|
||
$$
|
||
d = 2 R_e \arcsin(\sqrt{a})
|
||
$$
|
||
|
||
где $R_e = 6\,371\,000$ м.
|
||
|
||
Функция: `haversine(a, b)` → расстояние в метрах.
|
||
|
||
### Геодезическая линия WGS84
|
||
|
||
Функция: `sample_path(start, end, n)`.
|
||
|
||
Точки строятся через `pyproj.Geod` на эллипсоиде WGS84:
|
||
|
||
$$
|
||
d_i =
|
||
\begin{cases}
|
||
i \cdot D/(n-1), & i < n-1 \\
|
||
D, & i = n-1
|
||
\end{cases}
|
||
$$
|
||
|
||
где $D$ — полная геодезическая длина.
|
||
|
||
### DEM
|
||
|
||
Файл: `api/app/core/dem.py`
|
||
|
||
Высоты выбираются из GeoTIFF/COG:
|
||
|
||
- `elevation_at(lat, lon)` — одна точка;
|
||
- `elevations_along(points)` — массив точек.
|
||
|
||
Используется в `/terrain/elevation`, `/terrain/profile`, `/terrain/los`, `/terrain/fresnel-slice`, `/link/budget`, `/coverage`.
|
||
|
||
## 2. Профиль поверхности
|
||
|
||
Файл: `api/app/core/surface.py`
|
||
|
||
Для каждой точки пути:
|
||
|
||
$$
|
||
h_{surface} = h_{ground} + \max(h_{building}, h_{canopy})
|
||
$$
|
||
|
||
| Поле | Описание |
|
||
|------|----------|
|
||
| `ground_m` | DEM, м AMSL |
|
||
| `building_m` | высота здания, м |
|
||
| `canopy_m` | высота кроны из `CANOPY_PATH`, м |
|
||
| `surface_m` | итоговая высота препятствия |
|
||
|
||
Canopy является optional layer: если `include_canopy=false`, файлов нет или отдельные точки не покрыты canopy-тайлами, соответствующая высота кроны считается `0.0`.
|
||
|
||
Тип препятствия (`dominant_obstruction`): `building` → `canopy` → `terrain`.
|
||
|
||
API: `/terrain/profile`, `/terrain/los`, `/terrain/fresnel-slice`, `/link/budget`.
|
||
|
||
## 3. FSPL
|
||
|
||
Файл: `api/app/core/propagation.py`
|
||
|
||
$$
|
||
L_{FS} = 32.44 + 20 \log_{10}(f_{MHz}) + 20 \log_{10}(d_{km})
|
||
$$
|
||
|
||
Входы: $f_{MHz} > 0$, $d_{km} > 0$.
|
||
|
||
Используется в `/coverage` (`model=fspl`) и как базовая составляющая `/link/budget`.
|
||
|
||
## 4. Бюджет радиолинии
|
||
|
||
Файлы: `api/app/core/propagation.py`, `api/app/services/link.py`.
|
||
|
||
$$
|
||
P_{rx} = P_{tx} + G_{tx} + G_{rx} - L_{total}
|
||
$$
|
||
|
||
$$
|
||
fade\_margin = P_{rx} - P_{sens}
|
||
$$
|
||
|
||
$$
|
||
link\_viable = (fade\_margin > 0)
|
||
$$
|
||
|
||
### Активные слагаемые по модели
|
||
|
||
В ответе API есть отдельные диагностические поля:
|
||
|
||
| Поле | Смысл |
|
||
|------|-------|
|
||
| `propagation_model_loss_db` | полная потеря выбранной модели распространения до vegetation/atmosphere |
|
||
| `propagation_excess_db` | избыток выбранной модели над FSPL |
|
||
| `knife_edge_diffraction_db` | Bullington/P.526 diagnostic loss по surface profile |
|
||
| `diffraction_db` | active diffraction-like term, оставлен для обратной совместимости |
|
||
|
||
| `model` | Суммарные потери `total_loss_db` | Что не добавляется отдельно |
|
||
|---------|----------------------------------|------------------------------|
|
||
| `manual` | $L_{FS} + L_{diff} + L_{veg} + L_{atm}$ | нет |
|
||
| `itm` | $L_{ITM} + L_{veg} + L_{atm}$ | отдельный Bullington/P.526 |
|
||
| `p452` | $L_{P.452} + L_{veg}$ | отдельный Bullington/P.526 и отдельный P.676 |
|
||
|
||
Для `manual`:
|
||
|
||
$$
|
||
propagation\_model\_loss = L_{FS} + L_{diff}
|
||
$$
|
||
|
||
$$
|
||
propagation\_excess = L_{diff}
|
||
$$
|
||
|
||
Для `itm`:
|
||
|
||
$$
|
||
L_{total} = L_{FS} + (L_{ITM} - L_{FS}) + L_{veg} + L_{atm}
|
||
= L_{ITM} + L_{veg} + L_{atm}
|
||
$$
|
||
|
||
Для `p452`:
|
||
|
||
$$
|
||
L_{total} = L_{FS} + (L_{P.452} - L_{FS}) + L_{veg}
|
||
= L_{P.452} + L_{veg}
|
||
$$
|
||
|
||
Для `manual`:
|
||
|
||
$$
|
||
L_{total} = L_{FS} + L_{diff} + L_{veg} + L_{atm}
|
||
$$
|
||
|
||
API: `POST /api/v1/link/budget`.
|
||
|
||
## 5. ITM / Longley-Rice
|
||
|
||
Файл: `api/app/core/itm.py`
|
||
|
||
Библиотека: [`edwardoughton/itmlogic`](https://github.com/edwardoughton/itmlogic).
|
||
|
||
Это Longley-Rice / ITM. В проекте он **не** является реализацией ITU-R P.530.
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| `climate` | 5 по умолчанию |
|
||
| `ipol` | 0 |
|
||
| `eps` | 15 |
|
||
| `sgm` | 0.005 См/м |
|
||
| `ens` | 314 |
|
||
| `gma` | $157 \times 10^{-9}$ |
|
||
| `wn` | $f_{MHz} / 47.7$ |
|
||
|
||
Коды климата:
|
||
|
||
| Среда | `climate` |
|
||
|-------|-----------|
|
||
| `urban` | 5 |
|
||
| `suburban` | 6 |
|
||
| `rural` | 7 |
|
||
|
||
Профиль PFL содержит DEM-высоты вдоль пути. Высоты концов дополняются AGL:
|
||
|
||
$$
|
||
h_0 = h_{DEM,tx} + h_{AGL,tx}
|
||
$$
|
||
|
||
$$
|
||
h_N = h_{DEM,rx} + h_{AGL,rx}
|
||
$$
|
||
|
||
Шаг профиля:
|
||
|
||
$$
|
||
\Delta s = \frac{D_{km} \cdot 1000}{N - 1}
|
||
$$
|
||
|
||
Итоговая величина в коде:
|
||
|
||
$$
|
||
L_{ITM} = 8.685890 \cdot \ln(2 w_n d) + avar(0, 0, 0)
|
||
$$
|
||
|
||
Здесь $8.685890 \approx 20 / \ln(10)$, а `avar` — статистическая поправка `itmlogic` для медианного случая.
|
||
|
||
API: `/coverage` (`model=itm`), `/link/budget` (`model=itm`).
|
||
|
||
## 6. P.1812-подобная упрощённая модель
|
||
|
||
Файл: `api/app/core/itm.py`
|
||
|
||
Это **не полная** ITU-R P.1812. Реализация:
|
||
|
||
$$
|
||
L_{p1812\_like} = L_{ITM}(climate) + L_{clutter}
|
||
$$
|
||
|
||
| Среда | `climate` | $L_{clutter}$ |
|
||
|-------|-----------|---------------|
|
||
| `urban` | 5 | 8 дБ |
|
||
| `suburban` | 6 | 4 дБ |
|
||
| `rural` | 7 | 0 дБ |
|
||
|
||
API: `/coverage` (`model=p1812`).
|
||
|
||
## 7. ITU-R P.452
|
||
|
||
Файл: `api/app/core/p452.py`
|
||
|
||
Библиотека: [`bwinkel/pycraf`](https://github.com/bwinkel/pycraf).
|
||
|
||
Параметры `PathProp`:
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| Частота | `freq_mhz * u.MHz` |
|
||
| Температура | 293 K |
|
||
| Давление | 1013 гПа |
|
||
| Время | 50% |
|
||
| Разрешение профиля | $\max(100\text{ м}, 10 \cdot d_{km})$ |
|
||
| Clutter zone | URBAN / SUBURBAN / SPARSE |
|
||
|
||
Итог:
|
||
|
||
$$
|
||
L_{P.452} = loss\_complete(PathProp)[0]
|
||
$$
|
||
|
||
Полные уравнения P.452 находятся внутри `pycraf`.
|
||
|
||
API: `/link/budget` (`model=p452`).
|
||
|
||
## 8. ITU-R P.676
|
||
|
||
Файл: `api/app/core/atmosphere.py`
|
||
|
||
Библиотека: [`inigodelportillo/ITU-Rpy`](https://github.com/inigodelportillo/ITU-Rpy).
|
||
|
||
$$
|
||
f_{GHz} = \frac{f_{Hz}}{10^9}
|
||
$$
|
||
|
||
Вызов:
|
||
|
||
```python
|
||
itu676.gaseous_attenuation_terrestrial_path(
|
||
distance_km,
|
||
freq_ghz,
|
||
0,
|
||
7.5,
|
||
1013,
|
||
288,
|
||
"exact",
|
||
)
|
||
```
|
||
|
||
Параметры: высота 0 км, водяной пар 7.5 г/м³, давление 1013 гПа, температура 288 K, метод `exact`.
|
||
|
||
API: `/link/budget` для `manual` и `itm`. Для `p452` отдельный P.676 не добавляется.
|
||
|
||
## 9. ITU-R P.526: дифракция
|
||
|
||
Файл: `api/app/core/diffraction.py`
|
||
|
||
Параметр Френеля-Кирхгофа:
|
||
|
||
$$
|
||
v = h \sqrt{\frac{2(d_1+d_2)}{\lambda d_1 d_2}}
|
||
$$
|
||
|
||
Потери острого края:
|
||
|
||
$$
|
||
J(v) =
|
||
\begin{cases}
|
||
0, & v \le -0.78 \\
|
||
6.9 + 20\log_{10}\left(\sqrt{(v-0.1)^2 + 1} + v - 0.1\right), & v > -0.78
|
||
\end{cases}
|
||
$$
|
||
|
||
Bullington-style equivalent edge:
|
||
|
||
$$
|
||
h_{path}(d_1) = h_{tx} + (h_{rx}-h_{tx})\frac{d_1}{D}
|
||
$$
|
||
|
||
$$
|
||
h_i = h_{surface,i} - h_{path}(d_1)
|
||
$$
|
||
|
||
$$
|
||
L_{diff} = J(\max_i v_i)
|
||
$$
|
||
|
||
Как и LOS-модуль, Bullington-style diffraction добавляет выпуклость Земли:
|
||
|
||
$$
|
||
h_i = h_{surface,i} + bulge(d_1,d_2,k) - h_{path}(d_1)
|
||
$$
|
||
|
||
Функция `deygout(...)` сейчас является compatibility-wrapper над `bullington_loss(...)`; многоэкранный Deygout не реализован.
|
||
|
||
API: `/terrain/los`, `/link/budget` только для `model=manual`.
|
||
|
||
## 10. Френель и LOS
|
||
|
||
Файл: `api/app/core/fresnel.py`
|
||
|
||
Длина волны:
|
||
|
||
$$
|
||
\lambda = \frac{c}{f}
|
||
$$
|
||
|
||
Радиус n-й зоны Френеля:
|
||
|
||
$$
|
||
r_n = \sqrt{\frac{n \lambda d_1 d_2}{d_1+d_2}}
|
||
$$
|
||
|
||
Выпуклость Земли:
|
||
|
||
$$
|
||
bulge = \frac{d_1 d_2}{2 k R_e}
|
||
$$
|
||
|
||
По умолчанию $k = 1.333$.
|
||
|
||
LOS-анализ:
|
||
|
||
$$
|
||
h_{path}(d_1) = h_{tx} + (h_{rx}-h_{tx})\frac{d_1}{D}
|
||
$$
|
||
|
||
$$
|
||
h_{obstacle} = h_{surface} + bulge(d_1,d_2,k)
|
||
$$
|
||
|
||
$$
|
||
clearance = h_{path} - h_{obstacle}
|
||
$$
|
||
|
||
$$
|
||
required = clearance\_fraction \cdot r_1
|
||
$$
|
||
|
||
По умолчанию `clearance_fraction = 0.6`.
|
||
|
||
| Поле | Условие |
|
||
|------|---------|
|
||
| `geometric_los` | нет внутренних точек с `clearance < 0` |
|
||
| `los_clear` | нет внутренних точек с `clearance < required` |
|
||
| `first_fresnel_clearance_pct` | минимум `clearance / required * 100%` |
|
||
|
||
API: `/terrain/los`, `/terrain/fresnel-slice`, `/link/budget`.
|
||
|
||
## 11. P.833-подобная растительность
|
||
|
||
Файл: `api/app/core/vegetation.py`
|
||
|
||
Это упрощённая экспоненциальная модель, вдохновлённая P.833, но **не полная ITU-R P.833**, потому что частота в формуле не используется:
|
||
|
||
$$
|
||
A_{veg} = A_{max}\left(1 - e^{-(d\gamma)/A_{max}}\right)
|
||
$$
|
||
|
||
| Класс WorldCover | $\gamma$, дБ/м | $A_{max}$, дБ |
|
||
|------------------|----------------|---------------|
|
||
| `tree_cover` | 0.20 | 30 |
|
||
| `mangroves` | 0.22 | 32 |
|
||
| `shrubland` | 0.10 | 12 |
|
||
| `grassland` | 0.05 | 6 |
|
||
| `cropland` | 0.04 | 5 |
|
||
| `herbaceous_wetland` | 0.08 | 10 |
|
||
| `unknown` | 0.15 | 25 |
|
||
|
||
Глубина растительности:
|
||
|
||
$$
|
||
d_{veg} = \sum \Delta s_i
|
||
$$
|
||
|
||
Суммируются только сегменты `tree_cover` и `mangroves`.
|
||
|
||
API: `/terrain/los`, `/link/budget`, `/coverage`, `/landcover/path`.
|
||
|
||
## 12. Диаграммы направленности антенн
|
||
|
||
Файл: `api/app/core/antenna.py`
|
||
|
||
Omni:
|
||
|
||
$$
|
||
G(\varphi,\theta) = G_{dBi}
|
||
$$
|
||
|
||
Sector:
|
||
|
||
$$
|
||
\sigma = \frac{BW}{2.355}
|
||
$$
|
||
|
||
$$
|
||
normalized = e^{-0.5(\Delta/\sigma)^2}
|
||
$$
|
||
|
||
$$
|
||
drop = 10(normalized - 1)
|
||
$$
|
||
|
||
$$
|
||
A = \max(A_{floor}, drop_h + drop_v)
|
||
$$
|
||
|
||
Если $|\Delta az| > 90^\circ$:
|
||
|
||
$$
|
||
A = \min(A, -A_{F/B})
|
||
$$
|
||
|
||
$$
|
||
G = G_{dBi} + A
|
||
$$
|
||
|
||
Важно: при такой формуле `beamwidth_h`/`beamwidth_v` **не являются HPBW** в обычном смысле.
|
||
|
||
File pattern:
|
||
|
||
$$
|
||
G = G_{dBi} + G_{relative}
|
||
$$
|
||
|
||
API: `/antenna/pattern`, `/antenna/beam`, `/coverage`.
|
||
|
||
## 13. Coverage
|
||
|
||
Файл: `api/app/core/coverage.py`
|
||
|
||
$$
|
||
EIRP = P_{tx} + G_{ant}(azimuth, 0^\circ)
|
||
$$
|
||
|
||
$$
|
||
P_{rx} = EIRP - L_{path} - L_{veg} - L_{surface} + G_{rx}
|
||
$$
|
||
|
||
`L_path` выбирается по `model`: FSPL, ITM или p1812-like.
|
||
|
||
Surface obstruction (`L_surface`) считается через Bullington/P.526 по профилю
|
||
`ground + max(building, canopy)`:
|
||
|
||
- для `fspl`: добавляется полный Bullington loss по surface profile;
|
||
- для `itm` и `p1812`: добавляется только excess от buildings/canopy поверх
|
||
terrain-only профиля:
|
||
|
||
$$
|
||
L_{surface} = \max(0,\ L_{surface\_profile} - L_{terrain\_profile})
|
||
$$
|
||
|
||
Это снижает риск двойного учёта terrain diffraction, уже входящей в ITM/p1812-like path loss.
|
||
|
||
Алгоритм контура:
|
||
|
||
1. Для каждого азимута от 0 до 360 градусов.
|
||
2. Идти от TX с шагом `range_step_m`.
|
||
3. Пока $P_{rx} \ge level\_dbm$, точка считается покрытой.
|
||
4. Последняя покрытая точка образует радиальный контур.
|
||
|
||
Raster export:
|
||
|
||
1. Вокруг TX строится UTM-сетка с пикселем `range_step_m`.
|
||
2. Размер bbox: $2R \times 2R$, где $R = radius_m$.
|
||
3. Для центра каждой ячейки считается расстояние и азимут от TX.
|
||
4. Если расстояние больше `radius_m`, пишется NoData `-9999`.
|
||
5. Иначе в ячейку пишется $P_{rx}$ в дБм.
|
||
|
||
GeoTIFF хранит float32 `rx_power_dbm` с CRS/transform. PNG хранит 8-bit preview,
|
||
масштабированный по min/max конечных значений:
|
||
|
||
$$
|
||
png = 1 + \frac{P_{rx} - P_{min}}{P_{max} - P_{min}} \cdot 254
|
||
$$
|
||
|
||
Нулевое значение PNG зарезервировано под NoData.
|
||
|
||
API: `/coverage` → Celery job → `/jobs/{id}`.
|
||
|
||
## 14. Viewshed
|
||
|
||
Файл: `api/app/core/viewshed.py`
|
||
|
||
Используется GDAL `gdal_viewshed`:
|
||
|
||
1. DEM-тайлы мозаичатся в UTM.
|
||
2. Передаются `-oz`, `-tz`, `-md`.
|
||
3. Видимые пиксели (`255`) векторизуются в GeoJSON.
|
||
|
||
`k_factor` из `ViewshedRequest` в `gdal_viewshed` сейчас не передаётся.
|
||
|
||
API: `/viewshed`.
|
||
|
||
## 15. Здания
|
||
|
||
Файл: `api/app/services/buildings.py`
|
||
|
||
Для точки профиля:
|
||
|
||
$$
|
||
h_{building}(point) = \max_{polygon \ni point} height_m
|
||
$$
|
||
|
||
Оценка высоты в `db/sql/normalize_buildings.sql`:
|
||
|
||
```text
|
||
height_m =
|
||
OSM tags.height если > 0
|
||
иначе levels * 3.0 если levels > 0
|
||
иначе по типу:
|
||
garage/shed -> 3 м
|
||
industrial/warehouse -> 8 м
|
||
church -> 12 м
|
||
default -> 9 м
|
||
```
|
||
|
||
API: `/buildings/query`, косвенно terrain/link.
|
||
|
||
## 16. Сводная таблица
|
||
|
||
| Модель | Файл | API |
|
||
|--------|------|-----|
|
||
| Haversine / WGS84 | `core/geo.py` | почти все расчёты |
|
||
| DEM sampling | `core/dem.py` | terrain, link, coverage |
|
||
| Surface profile | `core/surface.py`, `services/terrain.py` | terrain, link |
|
||
| FSPL | `core/propagation.py` | coverage, link |
|
||
| ITM / Longley-Rice | `core/itm.py` | coverage, link |
|
||
| p1812-like | `core/itm.py` | coverage |
|
||
| P.452 | `core/p452.py` | link |
|
||
| P.526 / Bullington-style | `core/diffraction.py` | terrain/los, manual link |
|
||
| P.676 | `core/atmosphere.py` | manual/itm link |
|
||
| p833-like vegetation | `core/vegetation.py` | terrain/los, link, coverage |
|
||
| Fresnel / earth bulge | `core/fresnel.py` | terrain/los, fresnel-slice, link |
|
||
| Antenna | `core/antenna.py` | antenna, coverage |
|
||
| GDAL viewshed | `core/viewshed.py` | viewshed |
|
||
|
||
## 17. Эталонные значения из тестов
|
||
|
||
| Расчёт | Вход | Ожидаемый результат |
|
||
|--------|------|---------------------|
|
||
| FSPL | $f=433$ МГц, $d=12.45$ км | $107.08$ дБ |
|
||
| F1 | $f=433$ МГц, $d_1=d_2=6225$ м | $46.45$ м |
|
||
| Earth bulge | $d_1=d_2=6225$ м, $k=1.333$ | $2.28$ м |
|
||
| Knife-edge | $v=-0.79$ | $0$ дБ |
|
||
| Knife-edge | $v=0$ | $6.03$ дБ |
|
||
| Knife-edge | $v=1$ | $13.93$ дБ |
|
||
| Vegetation | $d=100$ м, $\gamma=0.2$, $A_{max}=30$ | $14.60$ дБ |
|
||
|
||
## 18. Ограничения реализации
|
||
|
||
1. ITM — Longley-Rice через `itmlogic`, не ITU-R P.530.
|
||
2. `p1812` в coverage — упрощённая p1812-like модель: ITM + clutter.
|
||
3. Vegetation — p833-like модель без частотной зависимости.
|
||
4. P.452, ITM и P.676 делегированы внешним библиотекам.
|
||
5. `deygout()` оставлен для совместимости, но новый код использует явное `bullington_equivalent_loss()`.
|
||
6. `link_viable` зависит только от `fade_margin_db > 0`, не от качества Френеля.
|
||
7. Coverage с buildings/canopy требует surface profile на лучах/ячейках и может быть дорогим на мелком `range_step_m`.
|
||
8. Viewshed не использует `k_factor`.
|
||
9. `diffraction_db` в `/link/budget` оставлен для обратной совместимости; для новой интеграции лучше читать `propagation_excess_db` и `knife_edge_diffraction_db`.
|
||
|
||
## Связанные документы
|
||
|
||
- [API.md](API.md)
|
||
- [README.md](README.md)
|
||
- [SPEC.md](SPEC.md)
|