gendesign/backend/app/services/site_finder/network_obremenenie.py
bot-backend e5e0ccb283
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m36s
Deploy / build-worker (push) Successful in 2m40s
Deploy / deploy (push) Successful in 1m12s
feat(site-finder): сетевые обременения из ЗОУИТ (#1070, Refs #103) (#1328)
2026-06-14 14:49:32 +00:00

129 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Классификация «сетевых обременений» из ЗОУИТ (#1070).
Pure-function классификатор: по тексту ``type_zone`` ЗОУИТ (cad_zouit / НСПД-дамп)
определяет, является ли зона *охранной зоной инженерной сети* — и если да, к какому
виду сети относится (тепло/электро/газ/водоснабжение/связь/общая инженерная).
Контекст (#1070 ООО «Брик»): теплосеть 66:41:0000000:90310 проходит через пятно
застройки → реальное обременение. Эпик #1067 (слой ИРД для analyze) НЕ содержал явного
среза «сетевые обременения». ЗОУИТ-данные cad_zouit уже текут в analyze
(``quarter_dump_lookup._get_cad_zouit_overlaps``, #232/#243) и классифицируются в
gate_verdict, но как ОБЩИЕ ЗОУИТ-overlaps без выделенного «сетевого» среза.
Этот модуль выделяет сетевые охранные зоны в отдельную типизированную категорию, чтобы
analyze/фронт могли показать «сетевое обременение (теплосеть)» как самостоятельный флаг.
VERIFY против PROD (cad_zouit, 2026-06-14): сетевые охранные зоны РЕАЛЬНО присутствуют —
~1930 строк из 3485:
• «Охранная зона инженерных коммуникаций» — 1281
• «Охранная зона тепловых сетей» — 548
• «Охранная зона объектов электроэнергетики …» — 69
• «Охранная зона трубопроводов (газопроводов …)» — 23
• «Охранная зона линий и сооружений связи» — 9
Зачем pure-function (без БД): тестируемо без фикстур, переиспользуемо и в
``_get_cad_zouit_overlaps`` (теггинг overlap-строк), и в ``gate_verdict`` (выделенный
warning-код), без дублирования keyword-таблиц.
"""
from __future__ import annotations
from typing import Literal
# Виды сетевого обременения. 'general' — «инженерные коммуникации» без указания вида сети.
NetworkKind = Literal[
"thermal", # теплоснабжение
"electricity", # электросети / объекты электроэнергетики
"gas", # газопроводы (входит в «трубопроводы …»)
"water", # водоснабжение / водоотведение / канализация
"communication", # линии и сооружения связи
"pipeline", # трубопроводы (нефте-/нефтепродукто-/аммиако- — не газ, но сетевое)
"general", # «охранная зона инженерных коммуникаций» (вид сети не уточнён)
]
# Человекочитаемые подписи (RU) для UI/детали обременения.
NETWORK_KIND_LABELS: dict[str, str] = {
"thermal": "теплоснабжение",
"electricity": "электроснабжение",
"gas": "газоснабжение",
"water": "водоснабжение/водоотведение",
"communication": "связь",
"pipeline": "трубопровод",
"general": "инженерные коммуникации",
}
# Упорядоченные правила «подстрока type_zone → вид сети». Порядок ВАЖЕН: более
# специфичные виды проверяются раньше «general» (инженерные коммуникации).
# Все подстроки lowercase; матч по `substring in type_zone.lower()`.
# Сверено с distinct type_zone из cad_zouit PROD (2026-06-14).
_NETWORK_RULES: tuple[tuple[NetworkKind, tuple[str, ...]], ...] = (
("thermal", ("тепл",)), # «… тепловых сетей»
("electricity", ("электроэнерг", "электросет", "лэп", "линий электропередач")),
("gas", ("газопров", "газоснаб")),
("water", ("водопров", "водоснаб", "водоотвед", "канализ")),
("communication", ("линий и сооружений связи", "сооружений связи", "линий связи")),
# «Охранная зона трубопроводов (газопроводов, нефтепроводов …)» — если газ не
# сматчился выше, относим к pipeline (нефте-/нефтепродукто-/аммиако-проводы).
("pipeline", ("трубопров", "нефтепров", "нефтепродуктопров", "аммиакопров")),
# Самое общее — «охранная зона инженерных коммуникаций» (вид сети не уточнён).
("general", ("инженерных коммуникаций", "инженерной инфраструктур", "инженерных сет")),
)
# Зона должна быть именно ОХРАННОЙ (обременение), а не, например, «зона публичного
# сервитута» без сетевой привязки. Требуем маркер охранной/защитной зоны ИЛИ явный
# сетевой вид в тексте — иначе «инженерные коммуникации» в произвольной зоне дало бы
# ложные срабатывания. На практике cad_zouit использует «Охранная зона …».
_PROTECTION_MARKERS: tuple[str, ...] = ("охранная зона", "охранн")
def classify_network_zone(type_zone: str | None) -> NetworkKind | None:
"""Определить вид сетевого обременения по тексту ``type_zone`` ЗОУИТ.
Args:
type_zone: текст типа зоны (cad_zouit.type_zone / НСПД-дамп). None/пусто → None.
Returns:
``NetworkKind`` если зона — охранная зона инженерной сети; иначе ``None``
(зона не сетевая: СЗЗ, ЗРЗ, сервитут без сети, природные ЗОУИТ и т.п.).
"""
if not type_zone:
return None
tz = type_zone.lower()
is_protection = any(m in tz for m in _PROTECTION_MARKERS)
# ЗСО (зона санитарной охраны ИСТОЧНИКА водоснабжения) — это зона вокруг водозабора,
# НЕ охранная зона водопроводной СЕТИ → не сетевое обременение. Иначе water-needles
# («водоснаб») ложно отнесли бы ЗСО к сети и флипнули verdict на blocker.
is_sanitary_source = "санитарн" in tz
for kind, needles in _NETWORK_RULES:
if any(n in tz for n in needles):
# «general» (инженерные коммуникации) принимаем только при маркере охранной
# зоны — иначе слишком широкая ловля. Специфичные виды (тепло/газ/…) сами по
# себе достаточно сетевые, но в cad_zouit они тоже всегда «Охранная зона …».
if kind == "general" and not is_protection:
continue
# water-зона санитарной охраны источника — не сеть, пропускаем.
if kind == "water" and is_sanitary_source:
continue
return kind
return None
def network_kind_label(kind: NetworkKind | None) -> str | None:
"""RU-подпись вида сети для UI/детали (None → None)."""
if kind is None:
return None
return NETWORK_KIND_LABELS.get(kind, kind)
def is_network_obremenenie(type_zone: str | None) -> bool:
"""True если ЗОУИТ — сетевое обременение (любой вид сети)."""
return classify_network_zone(type_zone) is not None
__all__ = [
"NETWORK_KIND_LABELS",
"NetworkKind",
"classify_network_zone",
"is_network_obremenenie",
"network_kind_label",
]