Files
2026-06-26 12:23:46 +03:00

605 lines
16 KiB
Markdown
Raw Permalink 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.
# Формулы и расчёты 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)