gendesign/backend/app/services/generative/catalog.py
bot-backend cdf493f345
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

141 lines
7.2 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.

"""Generative Design — Stage 3a (#1965): каталог типовых домов (house-type catalog).
Контракт Stage 3a: вместо max-FAR жадной раскладки (Stage 1b) пользователь выбирает
ТИПОВЫЕ дома и говорит, сколько секций каждого типа поставить. Этот модуль — справочник
таких типов: для каждого ``section_type`` он несёт габариты пятна секции (ширина × глубина,
метры), дефолтную этажность и подходящий класс жилья.
ИСТОЧНИК: это РАЗУМНЫЙ ДЕФОЛТНЫЙ каталог (sane-default), а не выгрузка из БД. Габариты —
типовые размеры секций массового жилья РФ (панель/монолит/башня/малоэтажка), округлённые
до реалистичных значений. Каталог намеренно захардкожен в коде на Stage 3a: миграции БД
сейчас нет (см. эпик #1953). Когда понадобится редактируемый застройщиком каталог — его
ПРОДВИГАЮТ в БД-таблицу с тем же контрактом (``section_type`` → footprint/floors/class),
а этот модуль станет seed'ом/фолбэком. До тех пор — single source of truth по типам.
Детерминированно, без LLM / внешних API / БД.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Literal
logger = logging.getLogger(__name__)
# Класс жилья — то же Literal-множество, что у ConceptInput / compute_teap (single
# source of truth по допустимым значениям; рассинхрон тут = ошибка типов в mypy-strict).
HousingClass = Literal["econom", "comfort", "business"]
@dataclass(frozen=True)
class HouseType:
"""Один типовой дом каталога (тип секции МКД / малоэтажки).
section_type — стабильный машинный КЛЮЧ типа (латиница, snake_case); это и есть
значение, которое фронт кладёт в ``BuildingProgramItem.section_type``.
label_ru — человекочитаемый русский лейбл для UI (Stage 3b его показывает).
footprint_w_m — ширина пятна секции, метры.
footprint_d_m — глубина пятна секции, метры.
default_floors — дефолтная этажность типа (UI подставляет, пользователь может менять
в пределах контракта BuildingProgramItem [1, 40]).
housing_class — подходящий класс жилья (драйвит ТЭП/финмодель ниже по конвейеру).
"""
section_type: str
label_ru: str
footprint_w_m: float
footprint_d_m: float
default_floors: int
housing_class: HousingClass
@property
def footprint_sqm(self) -> float:
"""Площадь пятна секции, кв.м (ширина × глубина)."""
return self.footprint_w_m * self.footprint_d_m
# ── Каталог типовых домов (sane-default, см. модульный docstring про источник) ──────
# Покрываем основные форматы массового жилья РФ:
# * панель-эконом — длинная неглубокая секция, средняя этажность, эконом-класс;
# * монолит-комфорт— чуть шире/глубже, комфорт-класс, типовая «свечка» 14 этажей;
# * башня-бизнес — компактное квадратное пятно, высотная (точечная) застройка;
# * малоэтажка-комфорт — широкая невысокая секция (3 этажа), низкоплотная застройка;
# * таунхаус — узкое неглубокое пятно блокированной застройки, 3 этажа.
# Габариты — реалистичные типовые размеры; этажность — характерная для формата.
HOUSE_TYPES: tuple[HouseType, ...] = (
HouseType(
section_type="panel_econom",
label_ru="Панельная секция (эконом)",
footprint_w_m=24.0,
footprint_d_m=15.0,
default_floors=9,
housing_class="econom",
),
HouseType(
section_type="monolith_comfort",
label_ru="Монолитная секция (комфорт)",
footprint_w_m=21.0,
footprint_d_m=18.0,
default_floors=14,
housing_class="comfort",
),
HouseType(
section_type="tower_business",
label_ru="Башня (бизнес)",
footprint_w_m=18.0,
footprint_d_m=18.0,
default_floors=25,
housing_class="business",
),
HouseType(
section_type="lowrise_comfort",
label_ru="Малоэтажная секция (комфорт)",
footprint_w_m=30.0,
footprint_d_m=14.0,
default_floors=3,
housing_class="comfort",
),
HouseType(
section_type="townhouse",
label_ru="Таунхаус",
footprint_w_m=12.0,
footprint_d_m=10.0,
default_floors=3,
housing_class="comfort",
),
)
# Индекс по ключу для O(1)-лукапа. Построен один раз при импорте; ключи уникальны
# (assert ниже ловит дубликат типа на старте, а не молча затирает запись).
_BY_KEY: dict[str, HouseType] = {ht.section_type: ht for ht in HOUSE_TYPES}
assert len(_BY_KEY) == len(HOUSE_TYPES), "duplicate section_type key in HOUSE_TYPES"
def get_house_type(section_type: str) -> HouseType:
"""Найти тип дома по ключу ``section_type``. Бросает :class:`KeyError`, если нет.
Вызывающий слой (placement) обязан валидировать ключи программы заранее (см.
:func:`available_section_types`) — неизвестный ключ здесь это программная ошибка,
а не пользовательский ввод, поэтому KeyError, а не тихий None.
"""
try:
return _BY_KEY[section_type]
except KeyError as exc:
raise KeyError(
f"unknown house type {section_type!r}; available: {', '.join(sorted(_BY_KEY))}"
) from exc
def available_section_types() -> frozenset[str]:
"""Множество допустимых ключей ``section_type`` каталога (для валидации программы)."""
return frozenset(_BY_KEY)
__all__ = [
"HOUSE_TYPES",
"HouseType",
"HousingClass",
"available_section_types",
"get_house_type",
]