Заменяет 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
106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
"""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"]
|