feat(concept): каталог типовых домов + program-placement (#1965 Stage 3a) #2029

Merged
bot-backend merged 2 commits from feat/concept-house-catalog-1965-stage3a into main 2026-06-27 21:40:11 +00:00
Showing only changes of commit e8b05d821e - Show all commits

View file

@ -51,31 +51,58 @@ def _payload(coords: list[list[float]], **overrides: object) -> ConceptInput:
return ConceptInput(**base) # type: ignore[arg-type]
# ── (a) backward-compat: building_program=None reproduces greedy output ─────────────
# ── (a) backward-compat: greedy output is byte-identical after the _Placer refactor ──
# GOLDEN PIN — детерминированный жадный выход на фиксированном участке _BIG_PARCEL
# (comfort / 9 эт. / mid_rise). Захвачен ПОСЛЕ извлечения _Placer и сверен ревьювером
# как идентичный до-рефактора. Любой будущий ДРЕЙФ жадной геометрии (число секций /
# пятно / GFA / квартиры) уронит этот тест — это и есть anti-regression guard, которого
# не давали инвариантные тесты в test_placement.py. Формат на стратегию:
# (features, built_area_sqm, total_floor_area_sqm, apartments_count).
_GREEDY_GOLDEN: dict[str, tuple[int, float, float, int]] = {
"max_area": (83, 35856.0, 358560.0, 4830),
"max_insolation": (70, 15120.0, 120960.0, 1629),
"balanced": (88, 27720.0, 249480.0, 3361),
}
def test_no_program_reproduces_greedy_output_unchanged() -> None:
# Тот же участок: с building_program=None раскладка ДОЛЖНА совпасть с жадной 1b
# (три стратегии, те же ТЭП/число секций) — program-ветка не трогает greedy-путь.
def test_greedy_output_matches_golden_pin() -> None:
# Реальный golden-pin: жадный путь (building_program не задан) должен выдать РОВНО
# замороженные литералы. Так фиксируем, что _Placer-рефактор не сдвинул раскладку.
payload = _payload(_BIG_PARCEL)
parcel = geometry.parse_parcel(payload)
variants = {v.strategy: v for v in placement.place_all_strategies(parcel, payload)}
assert set(variants) == set(_GREEDY_GOLDEN)
for name, (features, built, gfa, apts) in _GREEDY_GOLDEN.items():
v = variants[name]
assert len(v.buildings_geojson["features"]) == features, name
assert round(v.teap.built_area_sqm, 1) == built, name
assert round(v.teap.total_floor_area_sqm, 1) == gfa, name
assert v.teap.apartments_count == apts, name
# Greedy-режим не выставляет partial-fit сигнал (остаётся None).
assert v.placed_count is None
assert v.requested_count is None
def test_explicit_none_program_equals_default_greedy() -> None:
# building_program=None (явно) и дефолт (поле опущено) идут одним greedy-путём →
# должны совпасть покомпонентно. Дополняет golden-pin: ветка None не разветвляется.
payload_default = _payload(_BIG_PARCEL)
payload_none = _payload(_BIG_PARCEL, building_program=None)
parcel = geometry.parse_parcel(payload_default)
greedy = placement.place_all_strategies(parcel, payload_default)
none_branch = placement.place_all_strategies(geometry.parse_parcel(payload_none), payload_none)
assert {v.strategy for v in greedy} == {"max_area", "max_insolation", "balanced"}
assert {v.strategy for v in none_branch} == {"max_area", "max_insolation", "balanced"}
by_strategy_g = {v.strategy: v for v in greedy}
by_strategy_n = {v.strategy: v for v in none_branch}
for name, gv in by_strategy_g.items():
nv = by_strategy_n[name]
assert gv.teap == nv.teap
assert gv.financial == nv.financial
assert len(gv.buildings_geojson["features"]) == len(nv.buildings_geojson["features"])
# Greedy-режим не выставляет partial-fit сигнал (остаётся None).
assert gv.placed_count is None
assert gv.requested_count is None
variants_default = placement.place_all_strategies(
geometry.parse_parcel(payload_default), payload_default
)
variants_none = placement.place_all_strategies(
geometry.parse_parcel(payload_none), payload_none
)
by_default = {v.strategy: v for v in variants_default}
by_none = {v.strategy: v for v in variants_none}
assert set(by_default) == set(by_none)
for name, dv in by_default.items():
nv = by_none[name]
assert dv.teap == nv.teap
assert dv.financial == nv.financial
assert len(dv.buildings_geojson["features"]) == len(nv.buildings_geojson["features"])
# ── (b) 2-item program places EXACTLY the requested sections; TEAP reflects them ─────