Extend the concept generator so ConceptInput can carry an optional
building_program (list of typed houses from a catalog). When present,
placement lays out EXACTLY that program — for each item, place `count`
sections of the catalog footprint at the item's floors — instead of the
greedy max-FAR coverage-cap sweep. When absent, the existing greedy
behavior is unchanged (byte-for-byte backward-compatible).
- catalog.py: hardcoded HOUSE_TYPES (panel_econom, monolith_comfort,
tower_business, lowrise_comfort, townhouse) — sane-default catalog,
promote to DB later; get_house_type / available_section_types lookups.
- schema: additive BuildingProgramItem {section_type, floors, count} and
ConceptInput.building_program (default None -> greedy). ConceptVariant
gains optional placed_count / requested_count (partial-fit signal).
- placement: shared _Placer (collision/STRtree/setback machine extracted
from greedy sweep, reused — no duplication); place_program +
place_program_variant; branch in place_all_strategies on
building_program. Mixed-floor TEAP via exact per-floor-group aggregation
(GFA = sum(area_i * floors_i), no rounding drift).
- partial fit: when the parcel can't fit all sections, place as many as
fit and report placed_count < requested_count (no hard-422); zero-fit
still raises ParcelGeometryError (-> 422).
- API: validate program section_type keys against the catalog (unknown ->
422) before placement.
- tests: catalog integrity, greedy backward-compat, exact 2-item program +
TEAP reflection, over-packed partial placement, API program path.
- regenerate frontend api-types.ts (OpenAPI codegen gate stays green).
56 lines
2.1 KiB
Python
56 lines
2.1 KiB
Python
"""Stage 3a tests (#1965) — house-type catalog integrity + lookup.
|
|
|
|
Asserts the hardcoded ``HOUSE_TYPES`` catalog is well-formed (unique keys, sane
|
|
footprints, valid housing classes) and the lookup helpers behave (hit / miss).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.services.generative import catalog
|
|
|
|
|
|
def test_catalog_non_empty_and_keys_unique() -> None:
|
|
assert len(catalog.HOUSE_TYPES) >= 4
|
|
keys = [ht.section_type for ht in catalog.HOUSE_TYPES]
|
|
assert len(keys) == len(set(keys)), "section_type keys must be unique"
|
|
|
|
|
|
def test_catalog_fields_are_sane() -> None:
|
|
valid_classes = {"econom", "comfort", "business"}
|
|
for ht in catalog.HOUSE_TYPES:
|
|
assert ht.section_type and ht.section_type.isascii()
|
|
assert ht.label_ru, "RU label must be present"
|
|
assert ht.footprint_w_m > 0 and ht.footprint_d_m > 0
|
|
assert 1 <= ht.default_floors <= 40
|
|
assert ht.housing_class in valid_classes
|
|
# footprint_sqm helper == w * d.
|
|
assert ht.footprint_sqm == ht.footprint_w_m * ht.footprint_d_m
|
|
|
|
|
|
def test_catalog_covers_expected_formats() -> None:
|
|
# Каталог должен покрывать основные форматы массового жилья РФ.
|
|
keys = {ht.section_type for ht in catalog.HOUSE_TYPES}
|
|
assert {"panel_econom", "monolith_comfort", "tower_business"} <= keys
|
|
# Есть и высотный (>=20 этажей), и малоэтажный (<=4) формат.
|
|
floors = [ht.default_floors for ht in catalog.HOUSE_TYPES]
|
|
assert max(floors) >= 20
|
|
assert min(floors) <= 4
|
|
|
|
|
|
def test_get_house_type_hit() -> None:
|
|
ht = catalog.get_house_type("monolith_comfort")
|
|
assert ht.section_type == "monolith_comfort"
|
|
assert ht.housing_class == "comfort"
|
|
|
|
|
|
def test_get_house_type_miss_raises_keyerror() -> None:
|
|
with pytest.raises(KeyError):
|
|
catalog.get_house_type("does_not_exist")
|
|
|
|
|
|
def test_available_section_types_matches_catalog() -> None:
|
|
assert catalog.available_section_types() == frozenset(
|
|
ht.section_type for ht in catalog.HOUSE_TYPES
|
|
)
|