Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
222 lines
9.7 KiB
Python
222 lines
9.7 KiB
Python
"""구조물도 상단 그림 — 그림 치수가 **수량표와 같은 한 벌**인가 (PLAN 12장).
|
||
|
||
⚠ 옛 B07 그림은 두께 식을 따로 적어, 상부·하부 두께를 넣어도 **그림만 옛 두께**였음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import ( # noqa: E402
|
||
expand_observed,
|
||
load_observed_table,
|
||
)
|
||
from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402
|
||
attach_figures,
|
||
build_figure,
|
||
figure_reason,
|
||
)
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402
|
||
back_length_default_note,
|
||
stone_masonry,
|
||
)
|
||
|
||
|
||
def _sheet(**options) -> dict:
|
||
return {
|
||
"title": "돌쌓기(메)",
|
||
"type_id": "masonry_dry",
|
||
"height_m": 2.0,
|
||
"options": {"height_m": 2.0, **options},
|
||
}
|
||
|
||
|
||
def _paths(shapes: list[dict]) -> list[dict]:
|
||
return [shape for shape in shapes if shape["kind"] == "path"]
|
||
|
||
|
||
def _area(points: list[list[float]]) -> float:
|
||
return abs(sum(x0 * y1 - x1 * y0 for (x0, y0), (x1, y1) in zip(points, points[1:]))) / 2
|
||
|
||
|
||
def test_벽_두께는_사용자가_넣은_값을_따른다() -> None:
|
||
wall = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[0]["points"]
|
||
# 식: 상부 0.35+0.30=0.65 · 하부 0.65+0.30×(2−1)=0.95 · 기울기 0.3 → 앞면 위 끝 0.6
|
||
assert wall[:4] == [[0.0, 0.0], [0.6, 2.0], [1.25, 2.0], [0.95, 0.0]]
|
||
given = _paths(
|
||
build_figure(
|
||
_sheet(
|
||
back_len_cm=35, face_slope_ratio=0.3, thickness_top_m=0.5, thickness_bottom_m=1.2
|
||
)
|
||
)
|
||
)[0]["points"]
|
||
assert given[2] == [1.1, 2.0] and given[3] == [1.2, 0.0]
|
||
|
||
|
||
def test_터파기_넓이는_수량식과_같다() -> None:
|
||
dig = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[1]
|
||
assert dig["dash"]
|
||
assert abs(_area(dig["points"]) - 2.0 * ((0.65 + 0.95) / 2 + 0.2)) < 1e-9
|
||
|
||
|
||
def test_못_그리는_장은_그림_대신_까닭이_실린다() -> None:
|
||
payload = {
|
||
"sheets": [
|
||
_sheet(),
|
||
{**_sheet(stone_cm="60~80"), "type_id": "boulder_masonry"},
|
||
_sheet(back_len_cm=45),
|
||
]
|
||
}
|
||
attach_figures(payload)
|
||
no_back, boulder, drawn = payload["sheets"]
|
||
# 뒷길이 미정은 기본값이 있어 **그림이 섬**(2026-09-14 브레인 판정) — 까닭은 직경 축만.
|
||
assert no_back["figure_reason"] is None and no_back["figure"]
|
||
assert boulder["figure"] is None and "직경" in boulder["figure_reason"]
|
||
assert drawn["figure_reason"] is None and drawn["figure"]
|
||
|
||
|
||
def _texts(shapes: list[dict]) -> list[str]:
|
||
return [shape["text"] for shape in shapes if shape["kind"] == "text"]
|
||
|
||
|
||
def test_기본값으로_그린_장은_표_사유와_같은_말을_적는다() -> None:
|
||
options = {"height_m": 2.0}
|
||
_rows, table_notes = stone_masonry(2.0, 1.0, options, wet=False)
|
||
table_default = [note for note in table_notes if "뒷길이를 안 골라" in note]
|
||
# 2026-09-14 — 기본은 뒷길이 표준 하한(메 ≤3m 36~45㎝ → 45) · 표·그림이 같은 말.
|
||
assert table_default == [back_length_default_note(options, 45, wet=False, height_m=2.0)]
|
||
assert "하한 36㎝ 는 규격 일곱" in table_default[0]
|
||
|
||
texts = _texts(build_figure(_sheet()))
|
||
assert table_default[0] in texts
|
||
assert "뒷길이 ℓ₃ = 45㎝ (기본)" in texts
|
||
# 두께도 기본 45 로 — 상부 0.45+0.30
|
||
assert "상부 0.75 m" in texts
|
||
|
||
chosen = _texts(build_figure(_sheet(back_len_cm=45)))
|
||
assert "뒷길이 ℓ₃ = 45㎝" in chosen and not any("안 골라" in text for text in chosen)
|
||
assert back_length_default_note({"back_len_cm": 45}, 45) is None
|
||
|
||
|
||
def _wall_sheet(**options) -> dict:
|
||
return {
|
||
"title": "옹벽 H=2",
|
||
"type_id": "retaining_wall",
|
||
"height_m": 2.0,
|
||
"options": {"height_m": 2.0, "form": "반중력식", **options},
|
||
}
|
||
|
||
|
||
def test_옹벽_단면_치수가_표의_관측_수량을_그대로_낸다() -> None:
|
||
"""표 = 그림 — 그림이 쓰는 단면으로 표 수량(라이브러리 반올림)이 다시 나와야 함."""
|
||
entry = load_observed_table().find("retaining_wall", {"form": "반중력식", "height_m": 2.0})
|
||
section = entry["section"]
|
||
amounts = {item["name"]: item["amount"] for item in entry["components"]}
|
||
footing, wall, key, blinding = (section[k] for k in ("footing", "wall", "key", "blinding"))
|
||
bottom = wall["top_m"] + wall["front_batter_m"] + wall["back_batter_m"]
|
||
assert abs(bottom - 0.45) < 1e-9
|
||
concrete = (
|
||
footing["width_m"] * footing["thickness_m"]
|
||
+ key["width_m"] * key["depth_m"]
|
||
+ (wall["top_m"] + bottom) / 2 * wall["height_m"]
|
||
)
|
||
blind = blinding["thickness_m"] * (
|
||
footing["width_m"] + 2 * blinding["overhang_m"] - key["width_m"]
|
||
)
|
||
form = math.hypot(wall["height_m"], wall["back_batter_m"]) + math.hypot(
|
||
wall["height_m"], wall["front_batter_m"]
|
||
)
|
||
pipe = wall["height_m"] / 2.0 * section["weep"]["pipe_length_m"]
|
||
# 원문 값 그대로(2026-09-14 브레인 판정 — 라이브러리 반올림 1.35·0.15·3.2 에서 바꿈)
|
||
assert abs(concrete - amounts["콘크리트"]) < 1e-9 # 1.345
|
||
assert abs(blind - amounts["버림콘크리트"]) < 1e-9 # 0.145
|
||
assert abs(form - amounts["유로폼"]) < 5e-4 # 3.2047 → 3.205
|
||
assert abs(pipe - amounts["물구멍관"]) < 1e-9 # 0.32 (제원 칸 비었을 때)
|
||
assert footing["thickness_m"] + wall["height_m"] == 2.0
|
||
|
||
|
||
def _wall_table(length: float = 10.0, rubble=None, **options) -> dict:
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
|
||
|
||
structure = {
|
||
"structure_id": "w1",
|
||
"type_id": "retaining_wall",
|
||
"start_m": 0.0,
|
||
"end_m": length,
|
||
"options": {"form": "반중력식", "height_m": 2.0, "length_m": length, **options},
|
||
}
|
||
table = build_table([structure], {"retaining_wall": "옹벽"}, rubble_base_thickness_m=rubble)
|
||
return table["structures"][0]
|
||
|
||
|
||
def _amounts(structure: dict) -> dict:
|
||
return {row["name"]: row for row in structure["components"]}
|
||
|
||
|
||
def test_옹벽_터파기는_기초_폭과_양쪽_여유_옆면_1대0점5이고_토공으로만_간다() -> None:
|
||
wall = _wall_table()
|
||
rows = _amounts(wall)
|
||
# 밑폭 1.6 + 0.3×2 = 2.2 · 깊이 0.4 + 0.1 + 0.2 = 0.7 · 옆면 1:0.5 → 윗폭 2.9
|
||
# (2.2 + 2.9) ÷ 2 × 0.7 = 1.785/m
|
||
assert abs(rows["터파기"]["amount"] - 17.85) < 1e-9 # 수직일 때 15.4(2026-09-14 판정으로 고침)
|
||
# 든 것 = 기초·전단키 0.745 + (버림 + 잡석) 1.45 × 0.3 = 1.18 → 되메우기 0.605/m
|
||
assert abs(rows["잔토처리"]["amount"] - 11.8) < 1e-9
|
||
assert abs(rows["되메우기"]["amount"] - 6.05) < 1e-9 # 수직일 때 3.6
|
||
assert {rows[k]["destination"] for k in ("터파기", "되메우기", "잔토처리")} == {"earthwork"}
|
||
assert abs(rows["기초잡석"]["amount"] - 2.9) < 1e-9 # 원문 버림 0.145 × 2
|
||
assert wall["trench_depth_m"] == 0.7
|
||
assert "1:0.5" in rows["터파기"]["basis"] and "지반선" in rows["터파기"]["basis"]
|
||
# 잡석을 얇게 두면 전단키가 그 밑으로 나와 그 몫을 더 팜: (2.2+2.8)/2×0.6 + 0.35×0.1
|
||
thin = _amounts(_wall_table(rubble=0.1))
|
||
assert abs(thin["터파기"]["amount"] - 15.35) < 1e-9
|
||
|
||
|
||
def test_옹벽_물구멍관은_제원_칸이_닿고_돌쌓기와_같은_규격으로_합쳐진다() -> None:
|
||
plain = _amounts(_wall_table())["물구멍관"]
|
||
assert plain["spec"] == "Ø50" and abs(plain["amount"] - 3.2) < 1e-9
|
||
assert plain["basis_kind"] == "observed" and "관측값" in plain["basis"]
|
||
given = _amounts(_wall_table(weep_hole_area_m2=2.5, weep_hole_diameter_mm=75))["물구멍관"]
|
||
assert given["spec"] == "Ø75" and abs(given["amount"] - 1.6 / 2.5 * 0.4 * 10) < 1e-9
|
||
|
||
|
||
def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사유는_표와_같다() -> None:
|
||
shapes = build_figure(_wall_sheet(), rubble_thickness_m=0.3)
|
||
texts = _texts(shapes)
|
||
assert "기초잡석 T=0.3 m — 산출 조건 · 폭은 버림과 같음" in texts
|
||
assert "버림 T=0.10 · 폭 1.80 − 키 0.35 = 1.45 m" in texts
|
||
wall = _paths(shapes)[0]["points"]
|
||
assert [0.95, 0.4] in wall and [1.068, 2.0] in wall and [1.368, 2.0] in wall
|
||
# 표 사유와 같은 말 — 터파기를 어떻게 셌나(비탈분 제외)
|
||
_rows, table_notes = expand_observed(
|
||
"retaining_wall",
|
||
{"form": "반중력식", "height_m": 2.0},
|
||
_wall_sheet() | {"options": {"length_m": 10}},
|
||
)
|
||
trench = [note for note in table_notes if "터파기" in note]
|
||
assert trench and trench[0] in texts and "지반선" in trench[0]
|
||
# 잡석 0.3 → 깊이 0.8 · 한쪽 0.4
|
||
assert (
|
||
"터파기 밑폭 2.20 · 윗폭 3.00 × 깊이 0.80 m (기초 1.6 + 여유 0.3×2 · 옆면 1:0.5)" in texts
|
||
)
|
||
assert not any(text.startswith("기초잡석") for text in _texts(build_figure(_wall_sheet(), 0)))
|
||
|
||
|
||
def test_옹벽_못_그리는_장은_표와_같은_까닭() -> None:
|
||
assert "형식" in figure_reason({**_wall_sheet(), "options": {"height_m": 2.0}})
|
||
other = figure_reason(_wall_sheet(height_m=1.6) | {"height_m": 1.6})
|
||
assert other.startswith("그림 없음 — ") and "자료에 없습니다" in other
|
||
label = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "물구멍" in t]
|
||
assert label == ["물구멍 Ø50 — 3㎡당 1개소 · 관 0.4 m(관측값)"]
|
||
|
||
|
||
def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None:
|
||
sheet = {**_sheet(), "formula_sheet": {"vars": {"L3": 55}}}
|
||
texts = _texts(build_figure(sheet))
|
||
assert "뒷길이 ℓ₃ = 55㎝ (기본)" in texts and "상부 0.85 m" in texts
|