gendesign/backend/app/services/site_finder/network_obremenenie.py
Light1YT 3f245026ee
All checks were successful
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 2m12s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 9m0s
CI / openapi-codegen-check (pull_request) Successful in 1m50s
CI / backend-tests (pull_request) Successful in 8m49s
feat(site-finder): выделить сетевые обременения из ЗОУИТ (#1070, Refs #103)
cad_zouit содержит ~1930 сетевых охранных зон (тепло 548/электро 69/газ 23/связь 9/
инж.коммуникации 1281), но analyze отдавал их общими ЗОУИТ-overlaps без сетевого среза.
Pure-function classify_network_zone (type_zone→NetworkKind), сверен с prod distinct.
Overlap-строки cad_zouit несут is_network_zone/network_kind/network_kind_label; gate_verdict
даёт выделенный blocker ZOUIT_NETWORK_OBREMENENIE с видом сети (back-compat: классификация
на месте если флага нет; блокирующее поведение МКД сохранено). ЗСО санохраны источника
исключена из water (не сеть). 18 тестов.

#1070-предложение «сетевые обременения как analyze-слой» — реализовано через текущий cad_zouit.
ТУ-данные ООО Брик = needs-human (нет публичного API). engineering_networks loader = honest
blocked (источник публично пуст — QGIS-темплейт Минстроя 0 строк).

Closes #1070
2026-06-14 19:17:33 +05: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",
]