Files
Aislo/resources/tester/test_b08_structure_template_parity.py
T
eomsangdonandClaude Opus 5 1375d8c290 feat(b08): 구조물도 탭에 양식 식을 실음 — 산출 기초·식·갈 곳·안 섬
- 양식이 있는 종류(찰쌓기)는 장마다 양식으로 줄을 다시 세움(L=1 m당) — 식·설명·반올림·
  갈 곳·출처·안 섬 까닭을 줄에 실음. 양식 없는 종류는 지금 전개 그대로(고정형 모양)
- 화면 표: 산출 근거 밑에 식 한 줄 · 갈 곳 칸 · 안 선 줄은 「안 섬」과 까닭
- 전개 결함 둘 고침(브레인 판정): 「실무 관행」 계수가 돌종류를 지우던 것 ·
  기초잡석이 서는데 「두께가 없어 안 섬」 사유가 함께 뜨던 것
- 검증 프로젝트 찰쌓기 15줄 — 양식 m당×10 = 원단위 탭 합계 전부 일치

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-13 17:23:12 +09:00

131 lines
5.8 KiB
Python

"""찰쌓기 양식 = 지금 전개 — 값이 한 줄도 안 갈리는지 (2026-09-13, PLAN 3장 ② ③).
판정 Ⓑ 「첫 양식 값은 지금 전개와 같게」. 양식(`resources/library_structure/masonry_wet.json`)을
식 풀이기(TS → Node)로 풀어 낸 값이 `build_table`(돌쌓기 전개 + 기초잡석)과 같아야 함.
어긋나면 **양식이 틀린 것**.
⚠ 전개는 부동소수, 양식은 분수 — 상대 1e-9 안이면 같은 값으로 봄.
⚠ 줄이 「안 서는」 경우(버림 안 넣음 · 채집석 구입)는 `when` 으로 — 안 선 줄은 전개에도 없어야 함.
⚠ 원문 「-」 칸은 일부러 다름(양식 `differs_from_engine`) — 여기서는 그 규격을 안 넣음.
"""
from __future__ import annotations
import itertools
import shutil
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets # noqa: E402
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import ( # noqa: E402
load_template,
template_sheet,
template_vars,
)
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402
build_table,
face_slope_ratio,
)
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def _structure(height: float, length: float, **options) -> dict:
return {
"structure_id": "s1",
"type_id": "masonry_wet",
"name": "돌쌓기(찰)",
"start_m": 100.0,
"end_m": 100.0 + length,
"length_m": length,
"height_m": height,
"options": {"height_m": height, "length_m": length, **options},
}
CASES = [
dict(height=h, length=10.0, back_len_cm=l3, stone_kind=kind, foundation=found)
for h, l3, kind, found in itertools.product(
(1.0, 1.5, 2.5, 3.5),
(35, 45, 55),
("", "야면석·호박돌", "깬돌"),
("기초유", "기초버림", ""),
)
] + [
# 사용자가 정한 칸 — 기울기 · 두께 · 실무 관행 계수 · 물구멍 면적 · 잡석 두께 · 짧은 연장
dict(height=2.0, length=7.5, back_len_cm=45, face_slope_ratio=0.35, foundation="기초유"),
dict(height=2.0, length=10.0, back_len_cm=55, thickness_top_m=0.9, foundation="기초버림"),
dict(height=2.0, length=10.0, back_len_cm=45, thickness_bottom_m="1.4", stone_kind="깬잡석"),
dict(height=2.0, length=10.0, stone_kind="야면석·호박돌", stone_coeff_basis="실무 관행"),
dict(height=1.5, length=10.0, back_len_cm=35, weep_hole_area_m2=3, foundation="기초유"),
dict(height=2.5, length=12.0, back_len_cm=60, stone_kind="견치돌", rubble=0.3),
# 줄이 안 서는 갈래 — `when`
dict(height=2.0, length=10.0, blinding_concrete="안 넣음", foundation="기초유"),
dict(height=2.0, length=10.0, stone_supply="구입", stone_kind="깬돌"),
dict(height=2.0, length=10.0, blinding_concrete="안 넣음", stone_supply="구입"),
dict(height=2.0, length=10.0, rubble=0.0),
# 규격 칸 — 강도 · 물구멍 지름
dict(height=2.0, length=10.0, fill_concrete_mpa="180", weep_hole_diameter_mm=75),
]
@pytest.mark.parametrize("case", CASES, ids=lambda c: "-".join(f"{v}" for v in c.values()))
def test_양식_값이_전개와_같다(case: dict) -> None:
case = dict(case)
rubble = case.pop("rubble", None)
height = case.pop("height")
length = case.pop("length")
structure = _structure(height, length, **case)
engine = build_table([structure], {"masonry_wet": "돌쌓기(찰)"}, None, None, rubble)
components = engine["structures"][0]["components"]
template = load_template("masonry_wet")
assert template is not None
slope, _basis = face_slope_ratio(structure["options"], wet=True, height_m=height)
settings = {} if rubble is None else {"rubble_base_thickness_m": rubble}
values = template_vars(template, structure, slope, settings)
solved = evaluate_sheets([template_sheet(template, values)])
assert solved is not None, "Node 풀이가 안 돌았다"
assert [row["error"] for row in solved[0]] == [None] * len(solved[0]), solved[0]
rows = [row for row in solved[0] if not row["skipped"]]
assert len(rows) == len(components), (
[r["name"] for r in rows],
[c["name"] for c in components],
)
# ⓘ 「실무 관행」 계수가 돌종류를 지우던 전개 결함은 2026-09-13 고침(브레인 판정) — 차이 없음.
for row, component in zip(rows, components):
if row["name"] == "돌":
# 명세 13장 Ⓒ — 양식은 이름 고정 + 규격에 종류, 전개는 종류를 이름으로 씀.
assert component["name"] == (row["spec"] or "돌")
else:
assert row["name"] == component["name"]
assert row["spec"] == component["spec"], (row, component)
assert template["rows"][row["seq"] - 1]["destination"] == component["destination"], row
assert float(row["amount"]) == pytest.approx(component["amount"], rel=1e-9, abs=1e-12), (
row["name"],
row["amount"],
component["amount"],
)
def test_실무_관측값은_대안_후보로_보인다() -> None:
"""판정 Ⓑ — 지금 전개가 기본, 실무 관측(물구멍 2.5㎡)은 칸 옆 대안 후보."""
hole = load_template("masonry_wet")["vars"]["HOLE_AREA"]
assert hole["default"] == 2
assert any(item["value"] == 2.5 for item in hole["candidates"])
def test_표는_키_오름차순() -> None:
"""근사 LOOKUP 의 전제(명세 13장) — 정확 일치만 쓰더라도 표 모양은 한 규칙으로."""
for name, table in load_template("masonry_wet")["tables"].items():
assert table["keys"] == sorted(table["keys"]), name
for column, cells in table["columns"].items():
assert len(cells) == len(table["keys"]), (name, column)