Files
RadioPropagationApi/API.md
T
2026-06-24 09:01:11 +03:00

632 lines
20 KiB
Markdown
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.
# RF Propagation API
Документ описывает текущий HTTP API сервиса. Базовый префикс API: `/api/v1`.
Примеры ниже предполагают, что сервис опубликован на `http://localhost:5603`.
Если используется дефолтный порт compose, замени его на `8000`.
## Состояние Реализации
- Работает: healthcheck, OpenAPI, DEM elevation/profile, OSM buildings query, landcover/canopy path sampling, terrain LOS/Fresnel/diffraction с учётом DEM и buildings, link budget (`manual`, `itm`, `p452`) с P.676 atmospheric loss и P.833 vegetation, antenna omni/sector/file, async coverage/viewshed jobs через Redis + Celery.
- Частично работает: coverage raster export (`format=geotiff|png`), canopy bootstrap script (landcover загружается, canopy — вручную).
- Пока не реализовано: полный P.1812 с ITU digital maps (используется terrain-based ITM + clutter), WhiteboxTools viewshed (используется `gdal_viewshed`).
## Общие Правила
- Формат запросов и ответов: JSON.
- Координаты входа/выхода: WGS84, `lat`/`lon` в градусах.
- GeoJSON coordinates: стандартный порядок `[lon, lat]`.
- Высота антенн: `height_agl`, метры над землёй.
- Частота в HTTP API: `frequency_mhz`.
- Лимит `samples` для profile/LOS/landcover: `2..2048`.
- DEM берётся из `DEM_PATH`, по умолчанию `/data/dem`.
- Buildings берутся из PostGIS table `buildings`.
- Vegetation loss считается по формуле P.833 с коэффициентами из env:
`P833_GAMMA_DB_PER_M` и `P833_MAX_ATTENUATION_DB`.
Ошибки возвращаются в machine-friendly формате:
```json
{
"detail": {
"code": "NOT_IMPLEMENTED",
"detail": "..."
}
}
```
## Health
### `GET /health`
Проверка, что API поднят.
```bash
curl -s http://localhost:5603/health | jq
```
Ответ:
```json
{
"status": "ok"
}
```
## OpenAPI
### `GET /openapi.json`
Машиночитаемая схема FastAPI.
```bash
curl -s http://localhost:5603/openapi.json | jq '{title: .info.title, paths: (.paths | keys)}'
```
## Data Status
### `GET /api/v1/status/data`
Проверяет, какие источники данных доступны API.
```bash
curl -s http://localhost:5603/api/v1/status/data | jq
```
Пример ответа:
```json
{
"dem": {"configured": true, "path": "/data/dem", "files_count": 36},
"landcover": {"configured": true, "path": "/data/landcover", "files_count": 6},
"canopy": {"configured": false, "path": "/data/canopy", "files_count": 0},
"postgis": {"configured": true, "buildings_count": 2688698, "error": null}
}
```
Ключевые вычислительные ответы (`terrain/profile`, `terrain/los`, `landcover/path`, `link/budget`)
также возвращают поле `data_sources`, чтобы интегратор видел, какие слои реально участвовали
в расчёте.
## Terrain
### `GET /api/v1/terrain/elevation`
Возвращает высоту DEM в точке.
Query parameters:
- `lat`: широта.
- `lon`: долгота.
- `surface`: `dtm` или `dsm`. Сейчас оба читаются из DEM-растра; отдельный DSM-слой ещё не подключён.
```bash
curl -s "http://localhost:5603/api/v1/terrain/elevation?lat=59.9386&lon=30.3141&surface=dtm" | jq
```
Пример ответа:
```json
{
"lat": 59.9386,
"lon": 30.3141,
"elevation_m": 3.099534749984741,
"surface": "dtm"
}
```
### `POST /api/v1/terrain/profile`
Строит геодезический профиль между двумя точками. Если `include_buildings=true`, точки профиля, попавшие внутрь OSM building polygon, получают `building_m`.
Тело запроса:
```json
{
"start": {"lat": 59.935, "lon": 30.305},
"end": {"lat": 59.945, "lon": 30.325},
"samples": 64,
"include_buildings": true,
"include_canopy": false,
"k_factor": 1.333
}
```
Пример:
```bash
curl -s -X POST http://localhost:5603/api/v1/terrain/profile \
-H 'Content-Type: application/json' \
-d '{
"start": {"lat": 59.935, "lon": 30.305},
"end": {"lat": 59.945, "lon": 30.325},
"samples": 64,
"include_buildings": true,
"include_canopy": false,
"k_factor": 1.333
}' | jq '.distance_m, [.samples[] | select(.building_m > 0)] | length'
```
Ключевые поля ответа:
- `distance_m`: длина трассы.
- `samples`: массив точек профиля.
- `ground_m`: высота DEM.
- `building_m`: высота здания над землёй.
- `canopy_m`: высота кроны из `CANOPY_PATH`, если canopy raster доступен.
- `surface_m`: `ground_m + max(building_m, canopy_m)`.
- `path_geojson`: GeoJSON LineString.
### `POST /api/v1/terrain/los`
Проверяет LOS, первую зону Френеля и дифракционные потери по профилю.
Тело запроса:
```json
{
"tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30},
"rx": {"lat": 59.945, "lon": 30.325, "height_agl": 2},
"frequency_mhz": 433,
"fresnel_clearance": 0.6,
"include_buildings": true,
"include_canopy": false,
"include_vegetation": true,
"k_factor": 1.333,
"samples": 128
}
```
Пример:
```bash
curl -s -X POST http://localhost:5603/api/v1/terrain/los \
-H 'Content-Type: application/json' \
-d '{
"tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30},
"rx": {"lat": 59.945, "lon": 30.325, "height_agl": 2},
"frequency_mhz": 433,
"fresnel_clearance": 0.6,
"include_buildings": true,
"include_canopy": false,
"include_vegetation": true,
"k_factor": 1.333,
"samples": 128
}' | jq '{
los_clear,
geometric_los,
fresnel_violations_count,
geometric_obstructions_count,
building_obstructions_count,
worst_obstruction,
fresnel_profile: .fresnel_profile[0:3],
diffraction_loss_db,
vegetation_loss_db
}'
```
Ключевые поля ответа:
- `los_clear`: `true`, если нет нарушений требуемого Fresnel clearance.
- `geometric_los`: `true`, если геометрическая линия TX-RX не пересекает поверхность.
- `first_fresnel_clearance_pct`: минимальный процент доступного просвета относительно требуемого `fresnel_clearance * F1`.
- `obstructions`: все точки, где `clearance_m < required_clearance_m`.
- `geometric_obstructions`: только точки, где `clearance_m < 0`.
- `fresnel_profile`: полный профиль по всем `samples` для построения графика зоны Френеля.
- `fresnel_violations_count`: количество нарушений зоны Френеля.
- `geometric_obstructions_count`: количество геометрических пересечений.
- `building_obstructions_count`: количество геометрических препятствий типа `building`.
- `worst_obstruction`: худшая точка по запасу относительно требуемого Fresnel clearance.
- `diffraction_loss_db`: текущая Bullington-style оценка knife-edge loss.
- `vegetation_loss_db`: P.833 attenuation по `vegetation_depth_m` из WorldCover, если `include_vegetation=true`.
Поля obstruction:
```json
{
"distance_m": 1031.5257643738905,
"clearance_m": -11.086004102305118,
"fresnel_radius_m": 15.730079622922505,
"required_clearance_m": 9.438047773753503,
"type": "building"
}
```
Поля `fresnel_profile` для графика:
```json
{
"i": 12,
"lat": 59.93623,
"lon": 30.30746,
"distance_m": 312.5,
"surface_m": 18.4,
"path_height_m": 29.7,
"obstacle_height_m": 18.42,
"clearance_m": 11.28,
"fresnel_radius_m": 11.9,
"required_clearance_m": 7.14,
"fresnel_upper_60pct_m": 36.84,
"fresnel_upper_100pct_m": 41.6,
"fresnel_lower_60pct_m": 22.56,
"fresnel_lower_100pct_m": 17.8,
"type": "terrain"
}
```
Для отрисовки профиля обычно достаточно:
- `distance_m` по X.
- `surface_m` как земля/DSM.
- `path_height_m` как прямая TX-RX.
- `fresnel_lower_60pct_m` и `fresnel_upper_60pct_m` как требуемая свободная зона.
- `fresnel_lower_100pct_m` и `fresnel_upper_100pct_m` как полная первая зона Френеля.
- точки, где `clearance_m < required_clearance_m`, подсветить как нарушения.
### `POST /api/v1/terrain/fresnel-slice`
Возвращает горизонтальную GeoJSON-геометрию зоны Френеля и здания, которые
пересекают эту область. Endpoint нужен для визуального анализа потенциальных
отражающих объектов рядом с трассой.
```bash
curl -s -X POST http://localhost:5603/api/v1/terrain/fresnel-slice \
-H 'Content-Type: application/json' \
-d '{
"tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30},
"rx": {"lat": 59.945, "lon": 30.325, "height_agl": 2},
"frequency_mhz": 433,
"fresnel_fraction": 0.6,
"include_buildings": true,
"include_canopy": true,
"samples": 128,
"building_limit": 5000
}' | jq '.fresnel_geojson.geometry.type, (.buildings_geojson.features | length)'
```
Ключевые поля:
- `path_geojson`: линия TX-RX.
- `fresnel_geojson`: Polygon/MultiPolygon горизонтального среза зоны Френеля.
- `buildings_geojson`: здания PostGIS, пересекающие `fresnel_geojson`, с `intersection_area_m2`.
- `profile`: тот же набор точек Френеля, что удобно использовать для вертикального графика.
## Buildings
### `POST /api/v1/buildings/query`
Возвращает OSM buildings из PostGIS как GeoJSON FeatureCollection.
Вариант по bbox:
```bash
curl -s -X POST http://localhost:5603/api/v1/buildings/query \
-H 'Content-Type: application/json' \
-d '{"bbox":[30.30,59.93,30.33,59.95]}' | jq '.features | length'
```
Вариант по линии с буфером:
```bash
curl -s -X POST http://localhost:5603/api/v1/buildings/query \
-H 'Content-Type: application/json' \
-d '{
"path": {
"type": "LineString",
"coordinates": [[30.305, 59.935], [30.325, 59.945]]
},
"buffer_m": 50
}' | jq '.features | length'
```
Feature properties:
- `height_m`: высота здания в метрах.
- `levels`: этажность, если известна.
- `building_type`: значение OSM tag `building`.
- `source`: `measured`, если был tag `height`; иначе `estimated`.
Высота считается при импорте:
- `height`, если есть.
- иначе `building:levels * 3.0`.
- иначе дефолт по типу здания.
## Link Budget
### `POST /api/v1/link/budget`
Считает point-to-point budget. Сейчас рабочий режим: `model="manual"`.
Он учитывает FSPL, terrain/buildings diffraction по DEM/PostGIS profile,
и vegetation attenuation по WorldCover при `include_vegetation=true`.
```bash
curl -s -X POST http://localhost:5603/api/v1/link/budget \
-H 'Content-Type: application/json' \
-d '{
"tx": {"lat": 60.17, "lon": 24.94, "height_agl": 30, "power_dbm": 37, "gain_dbi": 8},
"rx": {"lat": 60.25, "lon": 25.10, "height_agl": 2, "gain_dbi": 2, "sensitivity_dbm": -110},
"frequency_mhz": 433,
"model": "manual",
"include_buildings": true,
"include_vegetation": true,
"k_factor": 1.333
}' | jq
```
Ключевые поля ответа:
- `distance_km`
- `fspl_db`
- `diffraction_db`
- `vegetation_db`
- `atmospheric_db`
- `total_loss_db`
- `rx_power_dbm`
- `fade_margin_db`
- `fresnel_clear`
- `fresnel_quality`: `clear`, `partial` или `blocked`
- `link_viable`
- `los_clear`
- `geometric_los`
- `first_fresnel_clearance_pct`
- `fresnel_violations_count`
- `geometric_obstructions_count`
- `building_obstructions_count`
- `worst_obstruction`
При `include_vegetation=true` API сэмплит WorldCover вдоль трассы и добавляет
P.833 vegetation attenuation в `vegetation_db`. Если landcover raster отсутствует,
значение остаётся `0`, чтобы link budget продолжал работать.
При `include_buildings=true` API также строит terrain surface profile с DEM и
OSM buildings, считает Fresnel/LOS и добавляет Bullington-style diffraction loss
в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа.
`link_viable` считается по фактическому `fade_margin_db > 0` после применённых
потерь. Состояние зоны Френеля не выключает линк напрямую, а отдаётся отдельным
качеством:
- `clear`: нет нарушений требуемых 60% первой зоны Френеля.
- `partial`: геометрическая LOS есть, но 60% зоны Френеля частично перекрыта.
- `blocked`: геометрическая линия TX-RX пересекает terrain/buildings/canopy.
Диагностические поля `geometric_los`, `worst_obstruction` и счётчики obstruction
помогают понять, почему качество Френеля ухудшилось.
Модели `p452` и `itm` используют ITU-R P.452 (pycraf) и Longley-Rice ITM (itmlogic) поверх DEM profile.
Поле `atmospheric_db` считается по ITU-R P.676 (itur). Флаг `include_canopy` учитывает canopy raster в surface profile.
## Antenna
### `POST /api/v1/antenna/pattern`
Нормализует antenna pattern. Поддержаны `omni`, `sector` и `file` (CSV: `azimuth_deg,elevation_deg,gain_dbi`).
```bash
curl -s -X POST http://localhost:5603/api/v1/antenna/pattern \
-H 'Content-Type: application/json' \
-d '{
"pattern": "sector",
"azimuth_deg": 90,
"tilt_deg": 0,
"gain_dbi": 8,
"beamwidth_h": 65,
"beamwidth_v": 15
}' | jq '.samples[:5]'
```
`pattern="file"` читает CSV с колонками `azimuth_deg,elevation_deg,gain_dbi` (относительный gain).
### `POST /api/v1/antenna/beam`
Возвращает GeoJSON polygon для визуализации сектора/луча.
```bash
curl -s -X POST http://localhost:5603/api/v1/antenna/beam \
-H 'Content-Type: application/json' \
-d '{
"origin": {"lat": 59.935, "lon": 30.305},
"radius_m": 5000,
"antenna": {
"pattern": "sector",
"azimuth_deg": 90,
"tilt_deg": 0,
"gain_dbi": 8,
"beamwidth_h": 65,
"beamwidth_v": 15
}
}' | jq '.geojson.geometry.type'
```
## Landcover
### `POST /api/v1/landcover/path`
Сэмплит landcover raster вдоль трассы и сворачивает точки в сегменты.
Canopy raster используется опционально: если файлов нет, endpoint всё равно
работает, но `canopy_height_m` будет `null`.
Ожидаемые данные:
- `LANDCOVER_PATH`: GeoTIFF/COG с кодами ESA WorldCover.
- `CANOPY_PATH`: GeoTIFF/COG с высотой кроны в метрах.
Поддерживаемые классы ESA WorldCover:
- `10`: `tree_cover`
- `20`: `shrubland`
- `30`: `grassland`
- `40`: `cropland`
- `50`: `built_up`
- `60`: `bare_sparse_vegetation`
- `70`: `snow_ice`
- `80`: `water`
- `90`: `herbaceous_wetland`
- `95`: `mangroves`
- `100`: `moss_lichen`
```bash
curl -s -X POST http://localhost:5603/api/v1/landcover/path \
-H 'Content-Type: application/json' \
-d '{
"start": {"lat": 59.935, "lon": 30.305},
"end": {"lat": 59.945, "lon": 30.325},
"samples": 256
}' | jq
```
Ключевые поля ответа:
- `segments`: участки с одинаковым landcover class.
- `segments[].class`: класс покрова.
- `segments[].forest_type`: маппится из WorldCover class для P.833 vegetation loss.
- `segments[].canopy_height_m`: средняя высота кроны на сегменте, если canopy raster доступен.
- `vegetation_depth_m`: суммарная длина участков `tree_cover`/`mangroves` по трассе.
Если landcover raster отсутствует или не покрывает трассу, endpoint возвращает `501 Not Implemented`.
## Viewshed
### `POST /api/v1/viewshed`
Ставит async job в Celery. Viewshed считается через `gdal_viewshed` по DEM,
результат — GeoJSON или GeoTIFF в `JOBS_OUTPUT_PATH`. `radius_m` остаётся
основным радиусом запроса, а опциональный `calc_radius_m` позволяет отдельно
задать радиус DEM-мозаики и параметр `gdal_viewshed -md`; если он не задан,
используется `radius_m`.
```bash
curl -s -X POST http://localhost:5603/api/v1/viewshed \
-H 'Content-Type: application/json' \
-d '{
"observer": {"lat": 59.935, "lon": 30.305, "height_agl": 30},
"radius_m": 15000,
"calc_radius_m": 20000,
"target_height_agl": 2,
"surface": "dsm",
"k_factor": 1.333,
"format": "geojson"
}' | jq
```
Ответ:
```json
{
"job_id": "..."
}
```
## Coverage
### `POST /api/v1/coverage`
Ставит async job в Celery. Radial coverage с моделями `fspl`, `itm`, `p1812` и antenna gain (omni/sector/file). Результат — GeoJSON contour polygons.
```bash
curl -s -X POST http://localhost:5603/api/v1/coverage \
-H 'Content-Type: application/json' \
-d '{
"tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30, "power_dbm": 40, "frequency_mhz": 433},
"antenna": {"pattern": "omni", "azimuth_deg": 0, "tilt_deg": 0, "gain_dbi": 8},
"rx": {"height_agl": 2, "sensitivity_dbm": -110, "gain_dbi": 2},
"model": "fspl",
"environment": "urban",
"radius_m": 20000,
"azimuth_step_deg": 1,
"range_step_m": 30,
"include_buildings": true,
"include_vegetation": false,
"levels_dbm": [-90, -100, -110],
"format": "geojson"
}' | jq
```
Ответ:
```json
{
"job_id": "..."
}
```
## Jobs
### `GET /api/v1/jobs/{job_id}`
Возвращает статус job из Redis. Jobs переживают рестарт API, TTL задаётся `JOB_TTL_SECONDS` (по умолчанию 7 дней).
```bash
curl -s http://localhost:5603/api/v1/jobs/<job_id> | jq
```
Статусы:
- `queued`
- `running`
- `done`
- `error`
## Данные И Обслуживание
### DEM
Загрузка DEM для СПб и Ленобласти:
```bash
python scripts/bootstrap_dem.py --bbox 27.3,58.4,35.8,61.4 --output-dir data/dem
docker compose restart api worker
```
### Buildings
Загрузка OSM PBF:
```bash
mkdir -p data/osm
wget -O data/osm/northwestern-fed-district-latest.osm.pbf \
https://download.geofabrik.de/russia/northwestern-fed-district-latest.osm.pbf
sh scripts/load_buildings.sh data/osm/northwestern-fed-district-latest.osm.pbf
docker compose restart api worker
```
Проверка количества зданий:
```bash
docker compose exec -T postgis psql -U radio -d radio \
-c "SELECT count(*) FROM buildings;"
```
## Частые Диагностические Команды
Полный smoke-check:
```bash
API_BASE=http://localhost:5603 sh scripts/smoke_api.sh
```
Проверить, что API видит PostGIS:
```bash
docker compose exec api python -c "
from sqlalchemy import create_engine, text
from app.config import get_settings
e = create_engine(get_settings().database_url)
with e.connect() as c:
print(c.execute(text('SELECT count(*) FROM buildings')).scalar())
"
```
Проверить, что DEM смонтирован в API container:
```bash
docker compose exec api sh -lc 'ls -lh /data/dem | head'
```
Проверить список путей OpenAPI:
```bash
curl -s http://localhost:5603/openapi.json | jq '.paths | keys'
```