gendesign/backend/app/services/generative/teap.py
Light1YT 3b79533f9f
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (push) Has been cancelled
feat(generative): движок концепций Stage 1a/1b/1c (#54 #55 #56)
Заменяет generative stubs детерминированным end-to-end пайплайном:
- 1a geometry: WGS84-parse, метрическая AEQD-репроекция, setback-буфер, placement grid
- 1b placement: greedy section-fill + STRtree коллизии (full-gap), 3 стратегии, coverage cap
- 1c teap/financial: площади/квартиры/FAR/паркинг; выручка/затраты/маржа + IRR-proxy
- exporters: ezdxf DXF (геометрия) + WeasyPrint ТЭП/фин PDF (lazy import)
generate() заменил generate_stub в route (422 на degenerate). mypy-strict + ruff clean,
31 generative-тест + full suite 3078 passed. ConceptVariant заполняется реальными числами.

Closes #54
Closes #55
Closes #56
2026-06-13 21:32:32 +05:00

106 lines
4.5 KiB
Python
Raw Permalink 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.

"""Generative Design — Stage 1c: ТЭП (technical-economic indicators).
From the Stage 1b placement (rectangular section footprints + floor count) we derive
the ``TEAP`` contract block with *real* numbers:
* ``built_area_sqm`` — пятно застройки (сумма площадей footprint-ов).
* ``total_floor_area_sqm`` — общая поэтажная площадь (GFA) = пятно * этажность.
* ``residential_area_sqm`` — продаваемая жилая = GFA * коэффициент эффективности
(вычет МОП/лестниц/тех.помещений; зависит от класса жилья).
* ``apartments_count`` — жилая / средняя площадь квартиры (зависит от класса).
* ``density`` — плотность застройки = FAR = GFA / площадь участка.
* ``parking_spaces`` — машиноместа по упрощённой норме (мест на квартиру).
Все коэффициенты — упрощённые нормативные proxy для MVP (см. константы ниже).
Детерминированно, без LLM / внешних API / БД.
"""
from __future__ import annotations
import logging
import math
from typing import Literal
from shapely.geometry import Polygon
from app.schemas.concept import TEAP
logger = logging.getLogger(__name__)
HousingClass = Literal["econom", "comfort", "business"]
# ── Коэффициент эффективности площади (residential / GFA), доля ────────────────
# Доля продаваемой жилой в общей поэтажной (остальное — МОП, лестницы, тех.этаж).
# Бизнес-класс «тратит» больше на МОП/лобби -> ниже эффективность.
_EFFICIENCY_BY_CLASS: dict[HousingClass, float] = {
"econom": 0.82,
"comfort": 0.78,
"business": 0.72,
}
# ── Средняя площадь квартиры (кв.м) по классу — выше класс -> крупнее лот ──────
_AVG_APARTMENT_SQM: dict[HousingClass, float] = {
"econom": 42.0,
"comfort": 55.0,
"business": 78.0,
}
# ── Норма парковки (машиномест на квартиру) по классу ─────────────────────────
_PARKING_PER_APARTMENT: dict[HousingClass, float] = {
"econom": 0.8,
"comfort": 1.0,
"business": 1.5,
}
def compute_teap(
*,
footprints: list[Polygon],
floors: int,
site_area_sqm: float,
housing_class: HousingClass,
) -> TEAP:
"""Свести footprints + этажность в :class:`TEAP`.
Args:
footprints: метрические пятна секций (кв.м берётся из ``.area``).
floors: этажность (общая для всех секций варианта).
site_area_sqm: площадь участка для плотности (FAR).
housing_class: класс жилья — задаёт эффективность/средний лот/парковку.
"""
built_area = float(sum(fp.area for fp in footprints))
total_floor_area = built_area * max(0, floors)
efficiency = _EFFICIENCY_BY_CLASS[housing_class]
residential_area = total_floor_area * efficiency
avg_apartment = _AVG_APARTMENT_SQM[housing_class]
apartments_count = math.floor(residential_area / avg_apartment) if avg_apartment else 0
# Плотность застройки = FAR (GFA / площадь участка). Защита от деления на ноль.
density = total_floor_area / site_area_sqm if site_area_sqm > 0 else 0.0
parking_norm = _PARKING_PER_APARTMENT[housing_class]
parking_spaces = math.ceil(apartments_count * parking_norm)
teap = TEAP(
built_area_sqm=round(built_area, 1),
total_floor_area_sqm=round(total_floor_area, 1),
residential_area_sqm=round(residential_area, 1),
apartments_count=apartments_count,
density=round(density, 3),
parking_spaces=parking_spaces,
)
logger.info(
"TEAP: built=%.0f GFA=%.0f resid=%.0f apts=%d FAR=%.2f parking=%d",
teap.built_area_sqm,
teap.total_floor_area_sqm,
teap.residential_area_sqm,
teap.apartments_count,
teap.density,
teap.parking_spaces,
)
return teap
__all__ = ["HousingClass", "compute_teap"]