# 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 формате: ```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`: высота кроны, пока всегда `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, 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`, подсветить как нарушения. ## 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` - `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` берётся из этого же анализа. Диагностические поля `geometric_los`, `worst_obstruction` и счётчики obstruction помогают понять, почему `link_viable=false`. Модели `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/ | 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' ```