feat(b08): 찰쌓기 양식 한 벌 + 식 칸 셋(when·destination·spec) — 지금 전개와 값 대조
- 풀이기에 명세 13장 보강분: when(안 선 줄, 가리키면 오류) · destination 필수(기본값 없음) ·
spec 의 {제원} 채움 · 반올림 trunc(ROUNDDOWN)·ceil_away(ROUNDUP)
- resources/library_structure/masonry_wet.json — 줄 15 · 뒷길이×돌종류 표 · 제원 vars ·
물구멍 2.5㎡ 실무 관측은 대안 후보로
- 양식 제원 채우기 B08_Quantity_Engine_StructureTemplate.py(계산 없음)
- 대조 시험 121건: 높이·뒷길이·돌종류·기초·사용자 칸·when 갈래에서 전개와 한 줄도 안 갈림.
알려진 차이 하나(실무 관행 계수가 돌종류를 지우는 전개 결함)는 시험에 드러냄
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"library_tier": "program",
|
||||
"item_kind": "form",
|
||||
"type_id": "masonry_wet",
|
||||
"name": "돌쌓기(찰)",
|
||||
"note": "구조물도 양식형 항목 첫 벌(PLAN 3장 ② · 명세 13장 식 칸 계약). 값은 지금 전개(`B08_Quantity_Engine_UnitQuantity.stone_masonry` + 기초잡석)와 같게 둠(판정 Ⓑ). 규격(높이·뒷길이·돌종류)마다 항목을 늘리지 않고 제원 vars 와 표 한 벌로 수량이 다시 남(명세 16장).",
|
||||
"differs_from_engine": [
|
||||
"원문 「-」 칸(야면석 75·깬돌 25·견치돌 25/30 고임돌)에서 전개는 고임돌 줄을 안 세우고 채집석에 0 을 더함. 이 양식은 고임돌 줄이 「표 칸이 비어 있음」 오류로 서고 채집석도 막힘 — 명세 13장 Ⓐ(안 선 줄을 0 으로 치지 않음)을 따른 것",
|
||||
"돌 줄 이름 — 전개는 돌종류 이름(야면석·호박돌…/돌), 양식은 이름 「돌」 고정 + spec 에 종류(명세 13장 Ⓒ)",
|
||||
"「실무 관행」 계수를 고르면 전개는 돌종류까지 지워 돌 줄을 「돌」·계산식 무게로 세움(야면석을 골라도). 관행은 계수 열만 바꾸는 칸이라 전개 쪽 결함으로 보고 양식은 종류를 지킴 — 판정 요청"
|
||||
],
|
||||
"vars": {
|
||||
"H": { "label": "높이(m)", "source": "height_m" },
|
||||
"L": { "label": "연장(m)", "source": "length_m" },
|
||||
"N": {
|
||||
"label": "전면 기울기 1:n",
|
||||
"source": "judged_face_slope",
|
||||
"note": "사용자 값이 있으면 그 값, 없으면 품셈 13-4-4 [주]⑪ 표준경사를 전개가 판정해 넘김"
|
||||
},
|
||||
"L3": { "label": "뒷길이(㎝)", "option": "back_len_cm", "default": 45 },
|
||||
"돌종류": { "label": "돌 종류", "option": "stone_kind", "default": "" },
|
||||
"계수기준": { "label": "야면석 계수", "option": "stone_coeff_basis", "default": "" },
|
||||
"TOP_IN": {
|
||||
"label": "상부 두께(m) — 0 은 안 정함",
|
||||
"option": "thickness_top_m",
|
||||
"default": 0
|
||||
},
|
||||
"BOT_IN": {
|
||||
"label": "하부 두께(m) — 0 은 안 정함",
|
||||
"option": "thickness_bottom_m",
|
||||
"default": 0
|
||||
},
|
||||
"기초": { "label": "기초(기초유·기초버림)", "option": "foundation", "default": "" },
|
||||
"BLIND": { "label": "버림 콘크리트(넣음·안 넣음)", "option": "blinding_concrete", "default": "넣음" },
|
||||
"SUPPLY": { "label": "돌 조달(채집·구입)", "option": "stone_supply", "default": "채집" },
|
||||
"MPA": { "label": "채움 콘크리트 강도(MPa)", "option": "fill_concrete_mpa", "default": "210" },
|
||||
"HOLE_DIA": { "label": "물구멍 지름(㎜)", "option": "weep_hole_diameter_mm", "default": 50 },
|
||||
"HOLE_AREA": {
|
||||
"label": "물구멍 1개소당 벽면적(㎡)",
|
||||
"option": "weep_hole_area_m2",
|
||||
"default": 2,
|
||||
"candidates": [
|
||||
{ "value": 2.5, "source": "소광리 07-구조도 「돌기슭막이(45)찰」 실무 관측 — ea/2.5㎡" }
|
||||
]
|
||||
},
|
||||
"RUBBLE_T": {
|
||||
"label": "기초잡석 두께(m)",
|
||||
"setting": "rubble_base_thickness_m",
|
||||
"default": 0.2
|
||||
}
|
||||
},
|
||||
"tables": {
|
||||
"고임돌표": {
|
||||
"note": "품셈 13-4-3 고임돌 ㎥/㎡ · 「참고자료」 열은 돌종류 미지정·실무 관행일 때(건설품셈 참고자료) · null 은 원문 「-」",
|
||||
"keys": [25, 30, 35, 45, 55, 60, 75],
|
||||
"columns": {
|
||||
"야면석·호박돌": [0.06, 0.07, 0.09, 0.11, 0.14, 0.15, null],
|
||||
"깬잡석": [0.09, 0.11, 0.13, 0.16, 0.19, 0.21, 0.26],
|
||||
"깬돌": [null, 0.1, 0.12, 0.15, 0.18, 0.2, 0.25],
|
||||
"견치돌": [null, null, 0.12, 0.15, 0.18, 0.2, 0.25],
|
||||
"참고자료": [null, 0.1, 0.12, 0.15, 0.18, 0.2, 0.25]
|
||||
}
|
||||
},
|
||||
"채움표": {
|
||||
"note": "품셈 13-4-4 [주]① 채움 콘크리트 ㎥/㎡",
|
||||
"keys": [25, 30, 35, 45, 55, 60, 75],
|
||||
"columns": {
|
||||
"야면석·호박돌": [0.08, 0.1, 0.12, 0.15, 0.18, 0.2, 0.25],
|
||||
"깬잡석": [0.11, 0.14, 0.16, 0.2, 0.25, 0.27, 0.34],
|
||||
"깬돌": [0.11, 0.14, 0.16, 0.2, 0.25, 0.27, 0.34],
|
||||
"견치돌": [0.11, 0.14, 0.16, 0.2, 0.25, 0.27, 0.34],
|
||||
"참고자료": [0.11, 0.14, 0.16, 0.2, 0.25, 0.27, 0.34]
|
||||
}
|
||||
},
|
||||
"돌중량표": {
|
||||
"note": "야면석 돌중량 ton/㎡ — 울진 소광 원단위 라이브러리 실무 관측(35·45·55㎝만). 다른 종류는 계산식",
|
||||
"keys": [35, 45, 55],
|
||||
"columns": { "야면석": [0.575, 0.88, 1.1] }
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"seq": 1,
|
||||
"name": "돌쌓기",
|
||||
"spec": "",
|
||||
"formula": "H*L*SQRT(1+N^2)",
|
||||
"formula_text": "정면적 × √(1+n²) — 비탈면적",
|
||||
"unit": "㎡",
|
||||
"destination": "unit_price",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 2,
|
||||
"name": "입적",
|
||||
"spec": "",
|
||||
"formula": "H*L*(IF(TOP_IN>0,TOP_IN,L3/100+0.3)+IF(BOT_IN>0,BOT_IN,IF(TOP_IN>0,TOP_IN,L3/100+0.3)+0.3*MAX(H-1,0)))/2",
|
||||
"formula_text": "정면적 × 평균두께 · 상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30×(H−1) (실무 구조물도 식, 두께를 넣으면 그 값)",
|
||||
"unit": "㎥",
|
||||
"destination": "reference",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"name": "고임돌",
|
||||
"spec": "",
|
||||
"formula": "A*LOOKUP_EXACT(고임돌표,L3,IF(돌종류='','참고자료',IF(계수기준='실무 관행','참고자료',돌종류)))",
|
||||
"formula_text": "돌쌓기 × 고임돌 원단위(뒷길이·돌종류, 품셈 13-4-3)",
|
||||
"refs": { "A": 1 },
|
||||
"unit": "㎥",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 4,
|
||||
"name": "석적",
|
||||
"spec": "",
|
||||
"formula": "H*L*L3/100*0.77",
|
||||
"formula_text": "정면적 × 뒷길이 × 0.77(채움률) — 소광리 시트에만 있는 줄",
|
||||
"unit": "㎥",
|
||||
"destination": "reference",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 5,
|
||||
"name": "돌",
|
||||
"spec": "{돌종류}",
|
||||
"formula": "A*IF(돌종류='야면석·호박돌',LOOKUP_EXACT(돌중량표,L3,'야면석'),L3/100*0.77*2.65)",
|
||||
"formula_text": "돌쌓기 × 돌중량 — 뒷길이 × 0.77 × 2.65(확정 5차, 잠정) · 야면석은 실무 관측표",
|
||||
"refs": { "A": 1 },
|
||||
"unit": "ton",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 6,
|
||||
"name": "막자갈",
|
||||
"spec": "",
|
||||
"formula": "(0.3+0.45)/2*H*L",
|
||||
"formula_text": "(뒷채움 상 0.30 + 하 0.45) ÷ 2 × H × 연장 — 정본 여섯 탭 공통값",
|
||||
"unit": "㎥",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 7,
|
||||
"name": "채움콘크리트",
|
||||
"spec": "{MPA}",
|
||||
"formula": "A*LOOKUP_EXACT(채움표,L3,IF(돌종류='','참고자료',IF(계수기준='실무 관행','참고자료',돌종류)))",
|
||||
"formula_text": "돌쌓기 × 채움 콘크리트 원단위(뒷길이·돌종류, 품셈 13-4-4 [주]①)",
|
||||
"refs": { "A": 1 },
|
||||
"unit": "㎥",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 8,
|
||||
"name": "모르터",
|
||||
"spec": "",
|
||||
"formula": "A*0.009",
|
||||
"formula_text": "돌쌓기 × 0.009 ㎥/㎡ (줄눈)",
|
||||
"refs": { "A": 1 },
|
||||
"unit": "㎥",
|
||||
"destination": "unit_price",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 9,
|
||||
"name": "물구멍관",
|
||||
"spec": "Ø{HOLE_DIA}",
|
||||
"formula": "A/HOLE_AREA*0.5",
|
||||
"formula_text": "돌쌓기 ÷ 개소당 벽면적 × 0.5 m/개소 (실무 관측 2㎡ · 법은 2~3㎡당 1개소 이상)",
|
||||
"refs": { "A": 1 },
|
||||
"unit": "m",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 10,
|
||||
"name": "채집석",
|
||||
"spec": "",
|
||||
"when": "SUPPLY<>'구입'",
|
||||
"formula": "S+W+R",
|
||||
"formula_text": "석적 + 고임돌 + 막자갈 — 현장 채집분 · 여기서 빼지 않음(사토에서 한 번만 뺌)",
|
||||
"refs": { "S": 4, "W": 3, "R": 6 },
|
||||
"unit": "㎥",
|
||||
"destination": "haul_deduction",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 11,
|
||||
"name": "버림콘크리트",
|
||||
"spec": "",
|
||||
"when": "IF(BLIND='안 넣음',0,IF(BLIND='안넣음',0,IF(BLIND='제외',0,1)))",
|
||||
"formula": "IF(BOT_IN>0,BOT_IN,IF(TOP_IN>0,TOP_IN,L3/100+0.3)+0.3*MAX(H-1,0))*L*0.1",
|
||||
"formula_text": "하단 길이(하부 두께) × 연장 × 두께 0.10m — KDS 44 90 00 · 폭은 잡석다짐 폭(KCS 34 50 05)",
|
||||
"unit": "㎥",
|
||||
"destination": "material",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 12,
|
||||
"name": "터파기",
|
||||
"spec": "",
|
||||
"formula": "(IF(기초='기초유',0.5*0.9,IF(기초='기초버림',0.1*0.7,0))+H*((IF(TOP_IN>0,TOP_IN,L3/100+0.3)+IF(BOT_IN>0,BOT_IN,IF(TOP_IN>0,TOP_IN,L3/100+0.3)+0.3*MAX(H-1,0)))/2+0.2))*L",
|
||||
"formula_text": "기초분(기초유 0.5×0.9 · 기초버림 0.1×0.7) + H × (평균두께 + 0.2) × 연장 — 실무 정본 식",
|
||||
"unit": "㎥",
|
||||
"destination": "earthwork",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 13,
|
||||
"name": "되메우기",
|
||||
"spec": "",
|
||||
"formula": "(IF(기초='기초유',0.5,IF(기초='기초버림',0.1,0))+H)*0.2*L",
|
||||
"formula_text": "(기초깊이 + H) × 0.2 × 연장",
|
||||
"unit": "㎥",
|
||||
"destination": "earthwork",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 14,
|
||||
"name": "잔토처리",
|
||||
"spec": "",
|
||||
"formula": "E-B",
|
||||
"formula_text": "터파기 − 되메우기",
|
||||
"refs": { "E": 12, "B": 13 },
|
||||
"unit": "㎥",
|
||||
"destination": "earthwork",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
},
|
||||
{
|
||||
"seq": 15,
|
||||
"name": "기초잡석",
|
||||
"spec": "",
|
||||
"when": "IF(BLIND='안 넣음',0,IF(BLIND='안넣음',0,IF(BLIND='제외',0,RUBBLE_T>0)))",
|
||||
"formula": "B*(RUBBLE_T/0.1)",
|
||||
"formula_text": "버림 × (잡석두께 ÷ 버림두께 0.1) — 폭이 같음(KCS 34 50 05) · 두께는 확정 3차 ②",
|
||||
"refs": { "B": 11 },
|
||||
"unit": "㎥",
|
||||
"destination": "unit_price",
|
||||
"rounding": { "mode": "none", "digits": 0 },
|
||||
"source": "library"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -27,6 +27,8 @@ pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가
|
||||
|
||||
|
||||
def _solve(rows: list[dict], vars: dict | None = None, tables: dict | None = None) -> list[dict]:
|
||||
# 갈 곳은 계약상 필수(기본값 없음) — 값 시험에서는 「보여주기만」으로 채워 넣음.
|
||||
rows = [{"destination": "reference", **row} for row in rows]
|
||||
result = evaluate_sheets([{"rows": rows, "vars": vars or {}, "tables": tables or {}}])
|
||||
assert result is not None, "Node 풀이가 안 돌았다"
|
||||
return result[0]
|
||||
@@ -75,6 +77,18 @@ def test_반올림_갈래() -> None:
|
||||
assert row["amount"] == "0." + "3" * 30
|
||||
|
||||
|
||||
def test_엑셀_대응표_음수에서_갈리는_넷() -> None:
|
||||
"""명세 13장 대응표 — 양수에서는 같아 시험을 빠져나가던 자리(브레인 검산)."""
|
||||
갈래 = {"floor": "-3", "trunc": "-2", "ceil_away": "-3", "ceil": "-2", "round": "-3"}
|
||||
for mode, expected in 갈래.items():
|
||||
assert _amount("-2.5", {"mode": mode, "digits": 0}) == expected, mode
|
||||
assert _amount("-2.5", {"mode": "round_half_even", "digits": 0}) == "-2"
|
||||
# 양수에서는 둘씩 같음 — 그래서 음수를 따로 봄.
|
||||
assert _amount("2.5", {"mode": "trunc", "digits": 0}) == "2"
|
||||
assert _amount("2.5", {"mode": "ceil_away", "digits": 0}) == "3"
|
||||
assert "모르는 반올림" in _one("1", {"mode": "banker", "digits": 0})["error"]
|
||||
|
||||
|
||||
def test_함수() -> None:
|
||||
assert _amount("2*1*SQRT(1+0.3^2)", {"mode": "round", "digits": 3}) == "2.088"
|
||||
assert _amount("SQRT(2.25)") == "1.5"
|
||||
@@ -162,6 +176,57 @@ def test_참조는_반올림_뒤_값을_받는다() -> None:
|
||||
assert result[1]["amount"] == "45"
|
||||
|
||||
|
||||
def test_when_거짓이면_줄이_안_서고_가리키면_막힌다() -> None:
|
||||
"""명세 13장 Ⓐ — 「안 섬」은 0 이 아님. 가리키는 뒷줄은 오류, 안 쓰면 영향 없음."""
|
||||
rows = [
|
||||
{"seq": 1, "name": "버림", "formula": "2", "when": "BLIND<>'안 넣음'"},
|
||||
{"seq": 2, "name": "잡석", "formula": "B*2", "refs": {"B": 1}},
|
||||
{
|
||||
"seq": 3,
|
||||
"name": "잡석_조건",
|
||||
"formula": "B*2",
|
||||
"refs": {"B": 1},
|
||||
"when": "BLIND<>'안 넣음'",
|
||||
},
|
||||
{"seq": 4, "name": "무관", "formula": "7", "refs": {"B": 1}},
|
||||
]
|
||||
넣음 = _solve(rows, {"BLIND": "넣음"})
|
||||
assert [row["amount"] for row in 넣음] == ["2", "4", "4", "7"]
|
||||
안넣음 = _solve(rows, {"BLIND": "안 넣음"})
|
||||
assert 안넣음[0]["skipped"] and 안넣음[0]["amount"] is None and 안넣음[0]["error"] is None
|
||||
assert "조건이 거짓" in 안넣음[0]["reason"]
|
||||
assert "앞 줄 1(버림) 안 섬" in 안넣음[1]["error"]
|
||||
assert 안넣음[2]["skipped"] and 안넣음[2]["error"] is None # 조건이 먼저라 막히지 않음
|
||||
assert 안넣음[3]["amount"] == "7" # 이름을 안 쓰면 영향 없음
|
||||
|
||||
|
||||
def test_갈_곳이_빠지면_오류_기본값_없음() -> None:
|
||||
"""명세 13장 Ⓑ ⛔⛔ — 기본값이 빠진 줄을 조용히 사라지게 한 결함이 실재함."""
|
||||
rows = [
|
||||
{"seq": 1, "name": "빠짐", "formula": "1"},
|
||||
{"seq": 2, "name": "모름", "formula": "1", "destination": "somewhere"},
|
||||
{"seq": 3, "name": "맞음", "formula": "1", "destination": "material"},
|
||||
]
|
||||
result = evaluate_sheets([{"rows": rows}])[0]
|
||||
assert "갈 곳(destination)이 없음" in result[0]["error"]
|
||||
assert "모르는 갈 곳" in result[1]["error"]
|
||||
assert result[2]["amount"] == "1"
|
||||
|
||||
|
||||
def test_이름은_고정_규격이_제원을_따른다() -> None:
|
||||
"""명세 13장 Ⓒ — 조인 키(이름+규격)가 흔들리지 않게 이름은 고정."""
|
||||
rows = [
|
||||
{"seq": 1, "name": "돌", "spec": "{돌종류}", "formula": "1"},
|
||||
{"seq": 2, "name": "물구멍관", "spec": "Ø{DIA}", "formula": "1"},
|
||||
{"seq": 3, "name": "모름", "spec": "{없는칸}", "formula": "1"},
|
||||
]
|
||||
result = _solve(rows, {"돌종류": "깬돌", "DIA": 50})
|
||||
assert (result[0]["name"], result[0]["spec"]) == ("돌", "깬돌")
|
||||
assert result[1]["spec"] == "Ø50"
|
||||
assert "규격의 모르는 제원" in result[2]["error"]
|
||||
assert _solve(rows[:1], {"돌종류": ""})[0]["spec"] == "" # 안 고르면 규격 빔 — 매칭 성공 아님
|
||||
|
||||
|
||||
def test_고정형과_줄_제원() -> None:
|
||||
rows = [
|
||||
{"seq": 1, "name": "박힌값", "amount": "2.088"},
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""찰쌓기 양식 = 지금 전개 — 값이 한 줄도 안 갈리는지 (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],
|
||||
)
|
||||
# ⚠ 알려진 차이 하나 — 「실무 관행」 계수를 고르면 전개가 돌종류까지 지워 돌 줄이 이름 「돌」·
|
||||
# 계산식 무게로 섬(야면석을 골라도). 관행은 **계수 열만** 바꾸는 칸이라 전개 쪽 결함으로 보고
|
||||
# 양식은 종류를 지킴 — 양식 `differs_from_engine` · PLAN 3장에 판정 요청으로 적음.
|
||||
practice_drops_kind = case.get("stone_coeff_basis") == "실무 관행" and case.get("stone_kind")
|
||||
for row, component in zip(rows, components):
|
||||
if row["name"] == "돌" and practice_drops_kind:
|
||||
assert (component["name"], row["spec"]) == ("돌", case["stone_kind"])
|
||||
continue
|
||||
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)
|
||||
Reference in New Issue
Block a user