All checks were successful
CI / changes (pull_request) Successful in 21s
CI Trade-In / changes (pull_request) Successful in 22s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 2m52s
CI / openapi-codegen-check (pull_request) Successful in 3m44s
CI / backend-tests (pull_request) Successful in 15m40s
Охранная зона инженерной сети = коридор, где физически проходит сеть → «теплотрасса в 24 м, врезка дешёвая» — решающий сигнал §3 (особенно heat, у которого не было гео-привязки). Новое поле nearby_network_zones в connection-capacity: до 2 ближайших сетевых охранных зон cad_zouit на net_kind в радиусе 300 м (GIST-префильтр в градусах + точный geography, ~70 мс, statement_timeout 4s с явным сбросом), гуманизированные подписи (кад.номер трубопровода / напряжение ВЛ + ТП, ≤1 кВ помечен как last-mile). general «инженерные коммуникации» дочищается по name_by_doc (на проде это реально тепло/газ). Пересекающие участок зоны — distance_m=0.
168 lines
9.2 KiB
Python
168 lines
9.2 KiB
Python
"""Гуманизация «сетевых охранных зон» ЗОУИТ для §3 ПТИЦА (#2119 C3).
|
||
|
||
Позитив-разрез охранных зон инженерных сетей: рядом с участком физически ПРОХОДИТ
|
||
сеть (охранная зона = коридор сети) → «теплотрасса в 24 м — врезка дешёвая». Pure /
|
||
без БД: на вход ``type_zone`` + ``name_by_doc`` (детальные имена из cad_zouit), на
|
||
выход — уточнённый ``net_kind`` + человекочитаемый ``human_label`` для финдиректора.
|
||
|
||
Зачем отдельно от ``network_obremenenie.classify_network_zone``:
|
||
- ``classify_network_zone`` смотрит ТОЛЬКО на ``type_zone`` (грубый вид: thermal /
|
||
general / …). На проде (2026-07-03) большинство «Охранная зона инженерных
|
||
коммуникаций» (``general``) — по ``name_by_doc`` РЕАЛЬНО теплосети/газопроводы
|
||
(«Трубопровод (тепловые сети) с кадастровым номером 66:41:…», «Газопровод
|
||
низкого давления…»). Оставить их ``general`` = потерять решающий тепло-сигнал.
|
||
- Поэтому здесь ``refine_net_kind`` дочищает ``general`` по ``name_by_doc``, а
|
||
``human_label`` вытаскивает кад.номер трубопровода / напряжение ВЛ / номер ТП
|
||
regex-ами. Классификатор type_zone НЕ дублируем — переиспользуем как базу.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from app.services.site_finder.network_obremenenie import NetworkKind, classify_network_zone
|
||
|
||
# Кад.номер РФ: 2:2:до7цифр:до-длинная последняя секция (напр. 66:41:0108079:76).
|
||
# Последняя секция бывает с ведущими нулями («0036») — берём как есть, не нормализуем.
|
||
_CAD_NUM_RE = re.compile(r"\b\d{2}:\d{2}:\d{6,7}:\d+\b")
|
||
|
||
# «ВЛ 0,4 кВ» / «КЛ 10кВ» / «ВЛИ-0,4 кВ» — тип линии + напряжение. Запятая-десятичный
|
||
# разделитель (0,4) и точка (0.4) оба валидны; пробел перед «кВ» опционален.
|
||
_VL_KV_RE = re.compile(
|
||
r"\b(ВЛИ|ВЛЗ|ВЛ|КЛ|КВЛ)\b[\s-]*?(\d+(?:[.,]\d+)?)\s*кВ",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
# Номер ТП/РП/КТП: «ТП 3164», «ТП 32505», «ТП 3736-3747», «КТП №589». Берём первый
|
||
# «якорный» ТП (источник питания ввода). Номер — цифры, опционально с дефисом-диапазоном.
|
||
_TP_RE = re.compile(
|
||
r"\b(ТП|РП|КТП|КТПН|БКТП|РТП)\b\s*(?:п/?№|№|N)?\s*(\d+(?:-\d+)?)",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
# Маркеры «на самом деле тепло» в name_by_doc для general-зон (инженерные коммуникации).
|
||
_THERMAL_NEEDLES: tuple[str, ...] = (
|
||
"теплоснабж",
|
||
"тепловая сеть",
|
||
"тепловые сети",
|
||
"тепловых сетей",
|
||
"теплосет",
|
||
"теплотрасс",
|
||
)
|
||
# Маркеры «на самом деле газ» в name_by_doc для general-зон.
|
||
_GAS_NEEDLES: tuple[str, ...] = (
|
||
"газопров",
|
||
"газораспредел",
|
||
"газоснаб",
|
||
"догазифик",
|
||
)
|
||
# Маркеры электро (ВЛ/КЛ/электросет/подстанция) в name_by_doc для general-зон.
|
||
# «трансформаторная подстанция» / «распределительный пункт» — объект электросети без
|
||
# явного «ВЛ N кВ» (прод-кейс 66:41:0404019:333). ТП здесь = трансформаторная (электро),
|
||
# не путать с тепловой ТК (тепловая камера) — та ловится _THERMAL_NEEDLES раньше.
|
||
_ELECTRICITY_NEEDLES: tuple[str, ...] = (
|
||
"электросет",
|
||
"электроснабж",
|
||
"электросетевого",
|
||
"кабельн",
|
||
"трансформаторн",
|
||
"распределительн",
|
||
)
|
||
|
||
|
||
def refine_net_kind(type_zone: str | None, name_by_doc: str | None) -> NetworkKind | None:
|
||
"""Уточнить вид сети: база по ``type_zone``, дочистка ``general`` по ``name_by_doc``.
|
||
|
||
Возвращает ``None``, если зона вообще не сетевая (как ``classify_network_zone``).
|
||
Для ``general`` («инженерные коммуникации») пытается распознать реальный вид по
|
||
детальному имени: тепло → thermal, газ → gas, электро → electricity. Если вид не
|
||
распознан — остаётся ``general`` («вид не уточнён»).
|
||
|
||
Специфичные type_zone (thermal / electricity / gas / …) НЕ переопределяем именем —
|
||
для них type_zone уже точный, а name_by_doc лишь обогащает подпись.
|
||
"""
|
||
base = classify_network_zone(type_zone)
|
||
if base != "general":
|
||
return base
|
||
name = (name_by_doc or "").lower()
|
||
if any(n in name for n in _THERMAL_NEEDLES):
|
||
return "thermal"
|
||
if any(n in name for n in _GAS_NEEDLES):
|
||
return "gas"
|
||
# ВЛ/КЛ N кВ в имени → электро (regex поймает даже без слова «электросеть»).
|
||
if any(n in name for n in _ELECTRICITY_NEEDLES) or _VL_KV_RE.search(name_by_doc or ""):
|
||
return "electricity"
|
||
return "general"
|
||
|
||
|
||
def _fmt_voltage(raw: str) -> str:
|
||
"""«0,4» / «0.4» / «10» → нормализованная строка напряжения («0,4», «10»)."""
|
||
return raw.replace(".", ",")
|
||
|
||
|
||
def _is_last_mile_voltage(raw: str) -> bool:
|
||
"""Напряжение ≤1 кВ (0,4 кВ и т.п.) — last-mile ввод, не магистраль."""
|
||
try:
|
||
return float(raw.replace(",", ".")) <= 1.0
|
||
except ValueError:
|
||
return False
|
||
|
||
|
||
def human_label(net_kind: NetworkKind | None, name_by_doc: str | None) -> str:
|
||
"""Человекочитаемая подпись сетевой зоны для §3 (финдиректор-facing).
|
||
|
||
thermal → «Тепловая сеть (по охранной зоне)» + кад.номер трубопровода, если есть.
|
||
electricity → «Ввод N кВ от ТП M (по охранной зоне)» (regex ВЛ/КЛ + ТП);
|
||
напряжение ≤1 кВ подаём как «(ввод 0,4 кВ, last-mile)» — второстепенно.
|
||
gas/pipeline → «Трубопровод (газо-/нефтепровод) по охранной зоне».
|
||
general → «Инженерные коммуникации (вид не уточнён)».
|
||
None → «Охранная зона» (fallback, не должно доходить для сетевых).
|
||
"""
|
||
name = name_by_doc or ""
|
||
|
||
if net_kind == "thermal":
|
||
cad = _CAD_NUM_RE.search(name)
|
||
base = "Тепловая сеть (по охранной зоне)"
|
||
if cad:
|
||
return f"{base}, кад. № {cad.group(0)}"
|
||
return base
|
||
|
||
if net_kind == "electricity":
|
||
vl = _VL_KV_RE.search(name)
|
||
tp = _TP_RE.search(name)
|
||
if vl:
|
||
voltage = _fmt_voltage(vl.group(2))
|
||
last_mile = _is_last_mile_voltage(vl.group(2))
|
||
if tp:
|
||
tp_num = tp.group(2)
|
||
if last_mile:
|
||
# ≤1 кВ — last-mile, подаём как второстепенный факт (уточнение в скобках).
|
||
return f"Ввод от ТП {tp_num} (по охранной зоне, {voltage} кВ — last-mile)"
|
||
return f"Ввод {voltage} кВ от ТП {tp_num} (по охранной зоне)"
|
||
if last_mile:
|
||
return f"Электроввод {voltage} кВ — last-mile (по охранной зоне)"
|
||
return f"Ввод {voltage} кВ (по охранной зоне)"
|
||
if tp:
|
||
return f"Электросеть от ТП {tp.group(2)} (по охранной зоне)"
|
||
return "Объект электросетевого хозяйства (по охранной зоне)"
|
||
|
||
if net_kind in ("gas", "pipeline"):
|
||
cad = _CAD_NUM_RE.search(name)
|
||
base = "Трубопровод (газо-/нефтепровод) по охранной зоне"
|
||
if cad:
|
||
return f"{base}, кад. № {cad.group(0)}"
|
||
return base
|
||
|
||
if net_kind == "water":
|
||
return "Сеть водоснабжения/водоотведения (по охранной зоне)"
|
||
|
||
if net_kind == "communication":
|
||
return "Линия связи (по охранной зоне)"
|
||
|
||
if net_kind == "general":
|
||
return "Инженерные коммуникации (вид не уточнён)"
|
||
|
||
return "Охранная зона"
|
||
|
||
|
||
__all__ = ["human_label", "refine_net_kind"]
|