Files
RadioPropagationApi/API.md
T
2026-06-23 13:22:07 +03:00

503 lines
15 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, manual link budget, antenna sector/beam helpers.
- Частично работает: coverage/viewshed создают job-stub, но тяжёлый расчёт ещё не реализован.
- Пока не реализовано: vegetation attenuation по реальным данным, ITM/P.1812/P.452/P.676, реальные async results для coverage/viewshed.
## Общие Правила
- Формат запросов и ответов: JSON.
- Координаты входа/выхода: WGS84, `lat`/`lon` в градусах.
- GeoJSON coordinates: стандартный порядок `[lon, lat]`.
- Высота антенн: `height_agl`, метры над землёй.
- Частота в HTTP API: `frequency_mhz`.
- DEM берётся из `DEM_PATH`, по умолчанию `/data/dem`.
- Buildings берутся из PostGIS table `buildings`.
- Vegetation loss считается по формуле P.833 с коэффициентами из env:
`P833_GAMMA_DB_PER_M` и `P833_MAX_ATTENUATION_DB`.
## 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)}'
```
## 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`: высота кроны, пока всегда `0`.
- `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,
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_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"
}
```
## 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`
- `link_viable`
При `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` берётся из этого же анализа.
Модели `p452` и `itm` пока возвращают `501 Not Implemented`.
## Antenna
### `POST /api/v1/antenna/pattern`
Нормализует antenna pattern. Сейчас поддержаны `omni` и простая `sector`-модель.
```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"` пока не реализован.
### `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`: пока `unknown` для `tree_cover`/`mangroves`, `null` для остальных классов.
- `segments[].canopy_height_m`: средняя высота кроны на сегменте, если canopy raster доступен.
- `vegetation_depth_m`: суммарная длина участков `tree_cover`/`mangroves` по трассе.
Если landcover raster отсутствует или не покрывает трассу, endpoint возвращает `501 Not Implemented`.
## Viewshed
### `POST /api/v1/viewshed`
Создаёт job-stub. Реальный WhiteboxTools/GDAL viewshed ещё не подключён.
```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,
"target_height_agl": 2,
"surface": "dsm",
"k_factor": 1.333,
"format": "geojson"
}' | jq
```
Ответ:
```json
{
"job_id": "..."
}
```
## Coverage
### `POST /api/v1/coverage`
Создаёт job-stub. Реальный radial coverage расчёт ещё не подключён.
```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, если job был создан текущим процессом API. Сейчас job storage in-memory, поэтому после рестарта API ранее созданные job исчезают.
```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;"
```
## Частые Диагностические Команды
Проверить, что 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'
```