From 89ca1ee94580ff70acb5955fa5da4afaf8e9ad32 Mon Sep 17 00:00:00 2001 From: grigo Date: Tue, 23 Jun 2026 16:08:47 +0300 Subject: [PATCH] added fresnel zone --- API.md | 16 ++++-- .../__pycache__/propagation.cpython-313.pyc | Bin 2887 -> 2864 bytes api/app/core/propagation.py | 2 +- .../models/__pycache__/link.cpython-313.pyc | Bin 1956 -> 2033 bytes api/app/models/link.py | 1 + .../services/__pycache__/link.cpython-313.pyc | Bin 4775 -> 5127 bytes api/app/services/link.py | 9 ++++ .../test_api.cpython-313-pytest-9.0.3.pyc | Bin 21012 -> 21435 bytes api/tests/test_api.py | 1 + api/tests/test_vegetation_integration.py | 47 ++++++++++++++++++ 10 files changed, 72 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 11081d1..5c9afa2 100644 --- a/API.md +++ b/API.md @@ -337,6 +337,7 @@ curl -s -X POST http://localhost:5603/api/v1/link/budget \ - `rx_power_dbm` - `fade_margin_db` - `fresnel_clear` +- `fresnel_quality`: `clear`, `partial` или `blocked` - `link_viable` - `los_clear` - `geometric_los` @@ -352,9 +353,18 @@ 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` берётся из этого же анализа. Диагностические -поля `geometric_los`, `worst_obstruction` и счётчики obstruction помогают понять, -почему `link_viable=false`. +в `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` пока возвращают `501 Not Implemented`. diff --git a/api/app/core/__pycache__/propagation.cpython-313.pyc b/api/app/core/__pycache__/propagation.cpython-313.pyc index 66eb576b35380ca933cd64299293d00885c76a7f..63b9152819bafb0518ac4c4f0a0c152342885095 100644 GIT binary patch delta 44 zcmX>uwn2>dGcPX}0}!lgvC6XB$lJ!oIA!t_wuzg~*=-pa>n4|SlyCmX5yk`nCu$9@ delta 69 zcmdlWc3h12GcPX}0}wPTSY-ul 0 and fresnel_clear, + link_viable=margin > 0, ) diff --git a/api/app/models/__pycache__/link.cpython-313.pyc b/api/app/models/__pycache__/link.cpython-313.pyc index 0adeabd89bb3dfaf750b0ab4720a12bb10d279ce..16bb94c6744ccd278c22d3aeab49f86823403e92 100644 GIT binary patch delta 302 zcmZ3&|B;{fGcPX}0}yO#vC8V$$lK1$_+~OUquk`p%mMZ6!Q8AssTkH^9(54S24wNV zS$uF7KbXY=lobH8IDjlcI7bP-L-$z?3FbTpZ7u_ouFCKladFGwsZ$xO_-#h#RtpPZeVa*IE$D782*H7CBX zG%+W$q;j$V>lH@6$xm68rHhz>Cf#Dr%quD41hR@io+)Aj5w?@%*wh%?CcCm(bBoGc mkhQufYJHi-dU8G6Q%0A`sqA`!!i>(0GbFzlH@c$%<^sVz-zx^Gb?1fr^V*K?EC!u$&yvrp8z` hxsA#Ygj1H4GvFiy6F*-BOko?L3qKlM(>HsN3Fkk=x diff --git a/api/app/models/link.py b/api/app/models/link.py index 8f2dac6..ea4acbd 100644 --- a/api/app/models/link.py +++ b/api/app/models/link.py @@ -27,6 +27,7 @@ class LinkBudgetResponse(BaseModel): rx_power_dbm: float fade_margin_db: float fresnel_clear: bool + fresnel_quality: Literal["clear", "partial", "blocked"] link_viable: bool los_clear: bool geometric_los: bool diff --git a/api/app/services/__pycache__/link.cpython-313.pyc b/api/app/services/__pycache__/link.cpython-313.pyc index b61dcc93d8d08469e8def8f7f0a3c1584d1de77b..5c631a36a068c2ecaa03e86aafd468cd30afb976 100644 GIT binary patch delta 886 zcmZWmO-vI(6rR~GyL8*`ZlNs;0)j!xVuYYngMt-85KKczmr_k@np!FWR?u09KQYmR zMm;H+c;SRzgZiFLDD zz|J0>@~BE)fI%quNlORWI~ViknK&yZdZ-CqF+SRkUrW{a1Lt>#nEyqERP>hQKQR%; z-?(5XT-IWq5G?V;p(Y+^{Ree?bO6usb$EjJF^LiUgYUpEd9CJBxgar8W-(VZLQ0R} z`bsBs=36UQV3pI9ZlJ6&m(^3Z7t{Lm!acm<%)ubOD`41#-LAn}Q%?O;f=-c^=yX2r z>Vd*I;WF7qF2C&FhOJ&VR}|zIozFWh2jSn9w$-{VwXdM|t>51Y#|z>3SLI3(KNFW9 z`bpjb-U{y5PcPF*kYgJ-Lm|yJeASTvyK#b}c-9v=!hVuCf~bo+Q~S*R(-H~*eJOh! zErh%RG0;X;veG0o1!M59Gc)B-*-^71mC>_lWb(JtXkj|7oBVZsJ~Nx0M0A)cbLeGI zucjuXk$?uvaFr0lOW+aM=H;Rk*yepjNh!K~J46I%3{Ob!U_Tb+o=}1cbY>_?;TnYj z0xCpfBpCDu_WKXf9ECJ~AV&m5ZB#g_Hs!y_A=pcw8x0dM9mWDG-NyceUfM3UuW45H z-khIY)Uy|mP7<`g@dS5Qb)<$Mo$)fzTOoV}~4-5jVY(GB!jlS71A%!(>-MOVbdLeGVWZ6hwr92uC2H$ynqEq>4d4S11Yu z3A@0Eh{>A;eYidYxji3mKm7AQxm{ zarWdYVLew@Mn<*^l3E`aK#UFSJ5)CELD(0(V!na}KLjvwGO{g@ToH0n(eMKUki8-M OfXhWQkFOv$*bV@wpm1{l diff --git a/api/app/services/link.py b/api/app/services/link.py index 0e0b1f9..fadfeba 100644 --- a/api/app/services/link.py +++ b/api/app/services/link.py @@ -68,6 +68,7 @@ def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBu ] return LinkBudgetResponse( **budget.__dict__, + fresnel_quality=_fresnel_quality(los_result.los_clear, los_result.geometric_los), los_clear=los_result.los_clear, geometric_los=los_result.geometric_los, first_fresnel_clearance_pct=los_result.first_fresnel_clearance_pct, @@ -85,6 +86,14 @@ def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBu ) +def _fresnel_quality(los_clear: bool, geometric_los: bool) -> str: + if los_clear: + return "clear" + if geometric_los: + return "partial" + return "blocked" + + def _convert_obstruction(sample: LosSample | None) -> Obstruction | None: if sample is None: return None diff --git a/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc b/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc index 5ba29df56bd2cd4a4b773635f4d889d42a07fe86..f413cad54c335a403ad29d20f71a49138947a7bf 100644 GIT binary patch delta 310 zcmbQTgmL$BM&8f7yj%=G@VLb)Yx73lV0R`i_Q`h@wKn&-XR(NdvIT3f0%h~q4H<$p zvl;WvLz%4^CYMPWPS*EfW7L{_&hsFn_T~v*X^brC44OKdlYE^SU2pNH6{QyErRKyJ zmL}$8mQ>m?1I@U_nw*oGSagd$DJMTUJ2mANdqHARNoHb>CS#S5szyPjp=Pl{W}X6w zX`)#?d8uC`_bi~kbRaI)VA^cxU&|Q8e?dU;BDc~77S~H^u0ZmFn(IYw*Xt}$bO|WJ ztpwp-L1JG9tAs0qGl1fg*982L0ok*Xp-2x%73lzpTO2mI`6;D2sdhz+Cm#v);E-Tc H0?PsbFGyjX delta 104 zcmdnJoN>w$M&8f7yj%=G;9GB%m9vpI*q!MQ>*TwNTAO>^vsjpdH8xN1N@HY6XVBE# zT;S)-IQfTvBlk3*`g9;JzQVY None: data = response.json() assert data["fspl_db"] > 0 assert "link_viable" in data + assert data["fresnel_quality"] in {"clear", "partial", "blocked"} assert "data_sources" in data diff --git a/api/tests/test_vegetation_integration.py b/api/tests/test_vegetation_integration.py index f326843..03e35e3 100644 --- a/api/tests/test_vegetation_integration.py +++ b/api/tests/test_vegetation_integration.py @@ -97,7 +97,54 @@ def test_link_budget_includes_diffraction_loss(monkeypatch) -> None: 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.fresnel_quality == "blocked" 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 + + +def test_link_viable_depends_on_margin_not_fresnel_quality(monkeypatch) -> None: + monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 0.0) + + def fake_profile(points, *args, **kwargs): + midpoint = len(points) // 2 + return SurfaceProfile( + distance_m=points[-1].distance_m, + samples=[ + SurfaceSample( + i=index, + lat=point.lat, + lon=point.lon, + distance_m=point.distance_m, + ground_m=20.0 if index == midpoint else 0.0, + building_m=0, + canopy_m=0, + surface_m=20.0 if index == midpoint else 0.0, + ) + for index, point in enumerate(points) + ], + ) + + monkeypatch.setattr(link_service, "surface_profile_from_points", fake_profile) + request = LinkBudgetRequest( + tx=LinkTx(lat=60.17, lon=24.94, height_agl=0, power_dbm=70, gain_dbi=8), + rx=LinkRx( + lat=60.25, + lon=25.10, + height_agl=0, + gain_dbi=2, + sensitivity_dbm=-120, + ), + frequency_mhz=433, + model="manual", + include_buildings=False, + include_vegetation=False, + ) + + result = link_service.link_budget(request) + + assert result.fade_margin_db > 0 + assert result.fresnel_quality == "blocked" + assert result.fresnel_clear is False + assert result.link_viable is True