Files
Aislo/resources/tester/test_b08_structure_figure.py
T

222 lines
9.7 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.
"""구조물도 상단 그림 — 그림 치수가 **수량표와 같은 한 벌**인가 (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×(21)=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