18 KiB
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. - Лимит
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 формате:
{
"detail": {
"code": "NOT_IMPLEMENTED",
"detail": "..."
}
}
Health
GET /health
Проверка, что API поднят.
curl -s http://localhost:5603/health | jq
Ответ:
{
"status": "ok"
}
OpenAPI
GET /openapi.json
Машиночитаемая схема FastAPI.
curl -s http://localhost:5603/openapi.json | jq '{title: .info.title, paths: (.paths | keys)}'
Data Status
GET /api/v1/status/data
Проверяет, какие источники данных доступны API.
curl -s http://localhost:5603/api/v1/status/data | jq
Пример ответа:
{
"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-слой ещё не подключён.
curl -s "http://localhost:5603/api/v1/terrain/elevation?lat=59.9386&lon=30.3141&surface=dtm" | jq
Пример ответа:
{
"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.
Тело запроса:
{
"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
}
Пример:
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, первую зону Френеля и дифракционные потери по профилю.
Тело запроса:
{
"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
}
Пример:
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:
{
"distance_m": 1031.5257643738905,
"clearance_m": -11.086004102305118,
"fresnel_radius_m": 15.730079622922505,
"required_clearance_m": 9.438047773753503,
"type": "building"
}
Поля fresnel_profile для графика:
{
"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, подсветить как нарушения.
Buildings
POST /api/v1/buildings/query
Возвращает OSM buildings из PostGIS как GeoJSON FeatureCollection.
Вариант по bbox:
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'
Вариант по линии с буфером:
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 tagbuilding.source:measured, если был tagheight; иначе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.
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_kmfspl_dbdiffraction_dbvegetation_dbatmospheric_dbtotal_loss_dbrx_power_dbmfade_margin_dbfresnel_clearlink_viablelos_cleargeometric_losfirst_fresnel_clearance_pctfresnel_violations_countgeometric_obstructions_countbuilding_obstructions_countworst_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 берётся из этого же анализа. Диагностические
поля geometric_los, worst_obstruction и счётчики obstruction помогают понять,
почему link_viable=false.
Модели p452 и itm пока возвращают 501 Not Implemented.
Antenna
POST /api/v1/antenna/pattern
Нормализует antenna pattern. Сейчас поддержаны omni и простая sector-модель.
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 для визуализации сектора/луча.
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_cover20:shrubland30:grassland40:cropland50:built_up60:bare_sparse_vegetation70:snow_ice80:water90:herbaceous_wetland95:mangroves100:moss_lichen
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 ещё не подключён.
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
Ответ:
{
"job_id": "..."
}
Coverage
POST /api/v1/coverage
Создаёт job-stub. Реальный radial coverage расчёт ещё не подключён.
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
Ответ:
{
"job_id": "..."
}
Jobs
GET /api/v1/jobs/{job_id}
Возвращает статус job, если job был создан текущим процессом API. Сейчас job storage in-memory, поэтому после рестарта API ранее созданные job исчезают.
curl -s http://localhost:5603/api/v1/jobs/<job_id> | jq
Статусы:
queuedrunningdoneerror
Данные И Обслуживание
DEM
Загрузка DEM для СПб и Ленобласти:
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:
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
Проверка количества зданий:
docker compose exec -T postgis psql -U radio -d radio \
-c "SELECT count(*) FROM buildings;"
Частые Диагностические Команды
Полный smoke-check:
API_BASE=http://localhost:5603 sh scripts/smoke_api.sh
Проверить, что API видит PostGIS:
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:
docker compose exec api sh -lc 'ls -lh /data/dem | head'
Проверить список путей OpenAPI:
curl -s http://localhost:5603/openapi.json | jq '.paths | keys'