From 0c9fc22d22664075ec25abc2321f9da9908874bb Mon Sep 17 00:00:00 2001 From: grigo Date: Tue, 23 Jun 2026 13:28:25 +0300 Subject: [PATCH] added vegetation --- API.md | 11 +++++- .../models/__pycache__/link.cpython-313.pyc | Bin 1469 -> 1815 bytes api/app/models/link.py | 8 +++++ .../services/__pycache__/link.cpython-313.pyc | Bin 2746 -> 4176 bytes api/app/services/link.py | 34 ++++++++++++++++-- api/tests/test_vegetation_integration.py | 7 ++++ 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index 7f4880c..61d9694 100644 --- a/API.md +++ b/API.md @@ -268,6 +268,13 @@ curl -s -X POST http://localhost:5603/api/v1/link/budget \ - `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 отсутствует, @@ -275,7 +282,9 @@ P.833 vegetation attenuation в `vegetation_db`. Если landcover raster от При `include_buildings=true` API также строит terrain surface profile с DEM и OSM buildings, считает Fresnel/LOS и добавляет Bullington-style diffraction loss -в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа. +в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа. Диагностические +поля `geometric_los`, `worst_obstruction` и счётчики obstruction помогают понять, +почему `link_viable=false`. Модели `p452` и `itm` пока возвращают `501 Not Implemented`. diff --git a/api/app/models/__pycache__/link.cpython-313.pyc b/api/app/models/__pycache__/link.cpython-313.pyc index d81f050296a1ecddc87cbf7568e67ac2309b6033..ae9ad9f2f337e5ab067afce7eed874169e58bc01 100644 GIT binary patch delta 814 zcmY*Xzi-n(6uyfuKb$nb6D3VbiKC>ED-o$fL4v44EJ%P_2{WNcR^m9RiDO4+hcF;z zU;wd29_WY|_y?F-*^v>lPIN>S8&bo>!kyEk<%aLR_r3SNd%pAM*tcVNo-1nQ)DWVa$m!iE%&i%8LGal@%zV$O^Pe| zxn}AecAM12oESFQx^yuw?|PLjcPEk^ z24x0Y47M3uV{n}U;WWd>5f)Y$utw}ZM3u4H3fOH}Lkh2f=MZ%3z(sj4nSFKSWeYAU zc*2g0cD!xDMS@qYyQuC7H5b*qLiPQVuLbct5|^&b@KD@vgy7v5B-0s>nQ>WM zG45!_auol^@X!8#9ujHM@`MSV0RG;!@U!J0qC~K OrQz<$<{v;ee~5oxw9sw< delta 444 zcmbQvx0jpmGcPX}0}$vbSY_>Jn#d=?7&B4b&_IGgi6NLpiXoUahS`)Eq=12;hyyIj z1{7sS66H+i(Bzyr#f&j|qM+!+_lp=gCQo9tX5^oIj?tZwcd{5$hBF^f_bt}6oczR+ zA|a4TAQOv(L82^4`T03TA|SpPh%Xu+UtE${l9?QzSW;4ynN(VmS{xr=BtH2dlPMP` zNEwLWn8X~#rU2$mPGFW~teW^iZt@J~NV#C1BKCA%O}<+~KACyhPNga7sU<hsg&D3So_mjn?~AVL~M$btwN5FrO5+~#4Qe+-29Z%oK(9aBcOUlkU45V;sY}y oBja5Lspkx;&l&W-u(2~5&fxsQ0HnT%a50L_ko?L3q`(#d03E1Y{{R30 diff --git a/api/app/models/link.py b/api/app/models/link.py index 83a4f4d..7b26428 100644 --- a/api/app/models/link.py +++ b/api/app/models/link.py @@ -3,6 +3,7 @@ from typing import Literal from pydantic import BaseModel, Field from app.models.coverage import LinkRx, LinkTx +from app.models.terrain import Obstruction class LinkBudgetRequest(BaseModel): @@ -26,3 +27,10 @@ class LinkBudgetResponse(BaseModel): fade_margin_db: float fresnel_clear: bool link_viable: bool + los_clear: bool + geometric_los: bool + first_fresnel_clearance_pct: float + fresnel_violations_count: int + geometric_obstructions_count: int + building_obstructions_count: int + worst_obstruction: Obstruction | None diff --git a/api/app/services/__pycache__/link.cpython-313.pyc b/api/app/services/__pycache__/link.cpython-313.pyc index 4236a879168b417eac3604912fe4db33c315cd47..7e193b4eb9fed74887cf29e3dc8cc218fb4df3ac 100644 GIT binary patch delta 1932 zcmZuyL2T1z6n=JMJBjTiP14Y$q@>`Mrj)cBX;q+Qz`B;Xz*w`QX>^N9;#MxHQ+_*4 z<3P3>oVLoECQX{eZtS*+3xdmbf@8gEN@lfUn#6f29hx|?|96@-XpQ90@4fH6_y6zz z-*X97I7!_Dr|QArx|C?{rv>pa_=RvizKw73Qd{?1WnNRS>d&~eE`_T&kT{sx5X15?nJilM8AXe!?C$@ju&mOTJT0nY_~trRkXU zr@0Aggp%-?CGn&23oTMAXihOY6*M0)Z-#K)fZ_x?F{EhJX})52d=4A!&IJ|eMs5Kqx~3ExKJ!{$$*trKyQ68op_YpC zH9Qdu+X{kvI8yZz8$hYh>4xpwYj3X)K#2C{RTT`8%W zq2sGr;JPpM5VF^?Uo96F6~ks5;dXTI6Sdp-JW8FEolGXp@3<*`63gUR^t1H+zqspE zr5=Z_zG@=#Ya+AlqPti-k@;$*Ix1VE@{{+i(TiWQRw7e(0n=FrOmF#M-_dQxonZfr z4&0us@oiQ9pv51&yJYc0|1jVU{2m>sGr+{_fSvyKfzY)DUV*Lof?p2a8LYdo{6}bi zHPmZ`dTY^2vKmcW(R58pS0&k!^ekfNOJ>ttt1^bRqP0etKrT)?V6f#GwgapD)ZYJuZfTh#omqG`ygg@zk$05dtLi^!WudE Jg2>JW{sn|I+|B?1 delta 686 zcmYk2&ubGw6vtIo>@F?6m`4tB@-4w8l-ME_v$)WbY?;cL7Fvp}Qq|U|mc^u{+XgiqqMOjtq_D9wXLlSj#fqed>_-IFPX+6bT@HI950T8XaKS&dOLnJN=(dj z9I)58GxaIAcZi?mmLIG@JuF9UpnLLN>ooeGxA8SxN2bOKB*r@1AyI=g$GYX*X;$}#C1)DYEY{KvJs$hDh#7wki>jYOI|p8 z0nB=qe>Cd(UJ}DN?7h6HtaAY!<5zg^SbmDl&YQK(CS9ivwSb2+3g8iHL9~BuS*SZpF}+`jIA&jY*UG6smxv)RjjagM(y;9 z(3xHYTm`Igq~ZquZxnh>+TSXjoZ Z LinkBu vegetation_db=vegetation_db, fresnel_clear=los_result.los_clear, ) - return LinkBudgetResponse(**budget.__dict__) + geometric_obstructions = [ + sample for sample in los_result.obstructions if sample.clearance_m < 0 + ] + building_obstructions = [ + sample + for sample in geometric_obstructions + if sample.obstruction_type == "building" + ] + return LinkBudgetResponse( + **budget.__dict__, + los_clear=los_result.los_clear, + geometric_los=los_result.geometric_los, + first_fresnel_clearance_pct=los_result.first_fresnel_clearance_pct, + fresnel_violations_count=len(los_result.obstructions), + geometric_obstructions_count=len(geometric_obstructions), + building_obstructions_count=len(building_obstructions), + worst_obstruction=_convert_obstruction(los_result.worst_sample), + ) + + +def _convert_obstruction(sample: LosSample | None) -> Obstruction | None: + if sample is None: + return None + return Obstruction( + distance_m=sample.distance_m, + clearance_m=sample.clearance_m, + fresnel_radius_m=sample.fresnel_radius_m, + required_clearance_m=sample.required_clearance_m, + type=sample.obstruction_type, + ) diff --git a/api/tests/test_vegetation_integration.py b/api/tests/test_vegetation_integration.py index 9ac7729..f326843 100644 --- a/api/tests/test_vegetation_integration.py +++ b/api/tests/test_vegetation_integration.py @@ -45,6 +45,8 @@ def test_link_budget_includes_vegetation_loss(monkeypatch) -> None: result = link_service.link_budget(request) assert result.vegetation_db == 5.0 + assert result.los_clear is result.fresnel_clear + assert result.fresnel_violations_count >= 0 assert isclose( result.total_loss_db, result.fspl_db + result.diffraction_db + 5.0, @@ -94,3 +96,8 @@ def test_link_budget_includes_diffraction_loss(monkeypatch) -> None: assert result.diffraction_db > 0 assert isclose(result.total_loss_db, result.fspl_db + result.diffraction_db) assert result.fresnel_clear is False + assert result.los_clear is False + assert result.geometric_los is False + assert result.fresnel_violations_count > 0 + assert result.geometric_obstructions_count > 0 + assert result.worst_obstruction is not None