"""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) = пятно * этажность. * ``office_area_sqm`` — нежилое (встроенно-пристроенный 1-й этаж: коммерция/ офисы) = GFA * нормативная доля нежилого (зависит от класса жилья). Вырезается из GFA ПЕРЕД расчётом жилой → жилая ужимается на эту площадь (total 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, } # ── Доля нежилого (коммерция/офисы 1-го этажа) в GFA по классу ───────────────── # НОРМАТИВ (не догадка): городские МКД РФ почти всегда несут встроенно-пристроенные # нежилые помещения 1-го этажа (магазины/услуги/офисы). По обзорам застройки РФ их # доля в общей поэтажной площади (GFA) типично 3–8% для городских МКД (1-й этаж из # 9–25). Берём КОНСЕРВАТИВНЫЕ значения нижней границы диапазона: # econom 0.0 — массовый эконом часто строят БЕЗ коммерции (спальные районы, # "квартиры на 1-м этаже"); нулём не завышаем выручку. # comfort 0.05 — комфорт почти всегда имеет коммерческий стрит-ритейл 1-го этажа. # business 0.05 — бизнес: представительный 1-й этаж (лобби+коммерция), но та же # умеренная доля (бОльшая часть нижних уровней — лобби/МОП, а не # продаваемая коммерция) — консервативно не задираем. # Нежилое ВЫРЕЗАЕТСЯ из GFA до расчёта жилой (жилая ужимается ровно на эту площадь), # total GFA при этом не меняется — это и есть фикс реализма (раньше ВСЯ непаркинговая # GFA считалась жилой, чуть завышая жилую площадь). Источник доли — отраслевой обзор, # НЕ проектный факт → caveat в финмодели честно это помечает. _OFFICE_SHARE_OF_GFA: dict[HousingClass, float] = { "econom": 0.0, "comfort": 0.05, "business": 0.05, } 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) # Нежилое (коммерция/офисы 1-го этажа) вырезаем из GFA ДО расчёта жилой: жилая # считается только по ОСТАВШЕЙСЯ (непаркинговой, нежилой-исключённой) GFA. total # GFA не меняется — нежилое остаётся в нём (СМР по нему уже покрыт в финмодели). office_share = _OFFICE_SHARE_OF_GFA[housing_class] office_area = total_floor_area * office_share residential_gfa = total_floor_area - office_area efficiency = _EFFICIENCY_BY_CLASS[housing_class] residential_area = residential_gfa * 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), office_area_sqm=round(office_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 office=%.0f resid=%.0f apts=%d FAR=%.2f parking=%d", teap.built_area_sqm, teap.total_floor_area_sqm, teap.office_area_sqm, teap.residential_area_sqm, teap.apartments_count, teap.density, teap.parking_spaces, ) return teap def synthesize_teap_from_program( *, total_footprint_sqm: float, floors: int, site_area_sqm: float, housing_class: HousingClass, sections: int = 1, ) -> TEAP: """Свести АГРЕГИРОВАННУЮ массинг-программу (скалярное пятно × этажность) в :class:`TEAP`. Stage 2a (#1965): LIVE-пересчёт экономики из интерактивного 3D-массинга. Фронтовый ``computeModel`` отдаёт уже СВЁРНУТУЮ программу — суммарное пятно застройки (кв.м) и этажность, без покомпонентной геометрии секций. Здесь мы строим из этого тот же :class:`TEAP`, что и :func:`compute_teap`/``synthesize_teap_from_buildability``, чтобы прогнать его через готовый :func:`compute_financial`. Зеркалит :func:`synthesize_teap_from_buildability` ОДИН-В-ОДИН, только источник площадей — СКАЛЯРНОЕ пятно, а не ``max_far × area``: * ``built`` (пятно) = ``total_footprint_sqm`` (как есть, сумма по секциям). * ``GFA`` (total_floor) = ``total_footprint_sqm × floors``. * ``office`` (нежилое) = ``GFA × _OFFICE_SHARE_OF_GFA[class]`` — вырезается из GFA до расчёта жилой (жилая ужимается ровно на эту площадь, total GFA не меняется). * ``residential`` = ``(GFA − office) × _EFFICIENCY_BY_CLASS[class]``. * ``apartments`` = ``floor(residential / _AVG_APARTMENT_SQM[class])``. * ``parking`` = ``ceil(apartments × _PARKING_PER_APARTMENT[class])``. * ``density`` = ``GFA / site_area_sqm`` (FAR), защита от деления на ноль. Чистая функция — без БД / LLM / внешних API. Те же нормативные константы, что и остальные синтезаторы (single source of truth, без новых магических чисел). Args: total_footprint_sqm: суммарное пятно застройки всех секций, кв.м (скаляр). floors: этажность (общая для программы); вне диапазона → клампится к >= 0. site_area_sqm: площадь участка для плотности (FAR). housing_class: класс жилья — задаёт эффективность/средний лот/парковку/нежилое. sections: число секций — метаданные программы; на ТЭП НЕ влияет (площади уже свёрнуты во ``total_footprint_sqm``). Принимается для симметрии с контрактом фронта и будущей пер-секционной логики; здесь намеренно не используется. """ del sections # метаданные программы; площади уже агрегированы — на ТЭП не влияют built_area = max(0.0, total_footprint_sqm) total_floor_area = built_area * max(0, floors) # Нежилое (коммерция/офисы 1-го этажа) вырезаем из GFA ДО расчёта жилой — точно как # compute_teap / synthesize_teap_from_buildability (single source of truth по долям). office_share = _OFFICE_SHARE_OF_GFA[housing_class] office_area = total_floor_area * office_share residential_gfa = total_floor_area - office_area efficiency = _EFFICIENCY_BY_CLASS[housing_class] residential_area = residential_gfa * efficiency avg_apartment = _AVG_APARTMENT_SQM[housing_class] apartments_count = math.floor(residential_area / avg_apartment) if avg_apartment else 0 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) return TEAP( built_area_sqm=round(built_area, 1), total_floor_area_sqm=round(total_floor_area, 1), office_area_sqm=round(office_area, 1), residential_area_sqm=round(residential_area, 1), apartments_count=apartments_count, density=round(density, 3), parking_spaces=parking_spaces, ) __all__ = ["HousingClass", "compute_teap", "synthesize_teap_from_program"]