Files
Aislo/resources/tester/test_793_3a_san_basis.py
eomsangdonandClaude Opus 5.5 2101601f39 feat(794-2c): 근거 제안 엔진 후속 — 같은 말 묶음 · 옛 번호 대조표 · 기계 작업량 길 넓힘 · 섞기 오인 넷 고침
- 새 M02_San_Basis_Map — 옛 번호 대조표(건설 옛 11-3 → 8-2-3 · 3-7 → 3-9-1 · 8-2-35 → 8-2-32 · 근거 = 원문 표 값 · 실무 글) · 사방(옛) = 판 대응표 2018 통합 판 · 기계 상수 줄 글 읽기(여러 대입 한 줄 · 괄호 설명 · 끝값 없는 나눗셈) · 작업량당 기계경비 로직 낱말 표
- 엔진 — 같은 말(휠라 = 채움재 · 평떼 = 평잔디 · 육샹 = 육상) · 건설(옛) 같은 번호는 같은 절 + 제목 뜻 겹침만 · Q 식 절 기호 채움(고르는 차례는 채우기 전) · 값 · 제목 길이 품 검산으로 안 받쳐지면 그 기계 작업량당 기계경비(차례 「기계」 · 번호 길은 그대로)
- 검산 probe — 고르기가 범위뿐이면 용량 · 규격 값이 드는 범위 하나로(버킷용량 → 규격구분)
- 459 블록: 없음 85 → 58 · 확정 125 → 164 · 근접 135 → 145 · 사람 114 → 92 · 확정 · 근접 → 없음 0 · 섞기(GF000132) 오인 넷 → GC001148 · GC001189
- 시험 test_793_3a 넓힘 — 없음 ≤ 58 · 확정 ≥ 164 · 손 확인 견본 열 · 1/3 적용 지어 넣지 않음 · 상수 읽기 · 대조표
- 근거후보 꼴 그대로 · M01 · 지식DB 고침 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:45:21 +09:00

260 lines
11 KiB
Python
Raw Permalink 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.
"""793-3a 지킴 — 근거 제안 엔진(`M02_San_Basis*`) · 실무 여덟 파일 산근 459 블록.
판정 수 ≥ 분석 648-D2 합친 길(기초 짚음 260 · 옵션 전부 52 · 품 검산 맞음 141 · 없음 94) ·
손 확인 견본(합판거푸집 넷 · 철근 Type-Ⅱ 가공 + 조립 · 레미콘 소량감률 50 · 터파기 #51 ·
야면석소운반 GC001140 석재류 · 불량 · 큰돌쌓기 ÷10 근접) · 지어 넣은 값 0 · 보정 자료 = 여덟 파일 ·
저장본 길(임시 폴더). 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip) ·
STmate 숨은 G 식 = 올림 길과 같은 `M02_San_Basis_Store.hidden`.
794-2c 넓힘 — 없음 85 → 58 · 확정 단 125 → 164 · 손 확인 견본 열(기계 작업량 길 · 섞기 오인 넷 ·
옛 번호 대조 · 같은 말 묶음) · 기계 상수 읽기 · 옛 번호 대조표.
"""
from __future__ import annotations
import json
import time
from collections import Counter
from functools import lru_cache
import helper_m02_hopyo_golden as golden
import pytest
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_San_Basis as sb
from M02_MasterTemplete import M02_San_Basis_Const as bc
from M02_MasterTemplete import M02_San_Basis_Index as bi
from M02_MasterTemplete import M02_San_Basis_Map as bm
from M02_MasterTemplete import M02_San_Basis_Store as store
from M02_MasterTemplete import M02_San_Book as san_book
WAYS = {
"B1",
"B2",
"B3",
"B4",
"B5",
"B6",
"V",
"검산",
"검산(같은 품 여럿)",
"근접",
"근접(같은 품 여럿)",
}
@lru_cache(maxsize=1)
def books() -> list[tuple]:
"""[(파일, HP 꼴 책 + 숨은식)] — 없는 파일이면 skip."""
out = []
for tag, path in golden.FILES.items():
book = golden.read(tag) if path.exists() else None
if book is None:
pytest.skip(f"실무 엑셀 없음 {tag}")
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
out.append((tag, {**book, "숨은식": store.hidden(book, sheets)}))
return out
@lru_cache(maxsize=1)
def results() -> tuple[dict, float]:
sb.ctx()
t0 = time.time()
got = {}
for tag, book in books():
for blk in book["단산"]:
got[f"{tag}#{blk['번']}"] = store.one(book, int(blk["번"]))
return got, time.time() - t0
def test_counts_at_least_d2():
got, sec = results()
kind = Counter(r["꼴"] for r in got.values())
tier = Counter(r["단"] for r in got.values())
same = sum(
1
for r in got.values()
if r["꼴"].startswith("확정")
and "다름" not in (st := [sb.checked(s) for s in sb.chosen_units(r)])
and "같음" in st
)
print(f"\n블록 {len(got)} · 꼴 {dict(kind)} · 단 {dict(tier)} · 품 같음 {same} · {sec:.1f} 초")
assert len(got) == 459
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 260
assert kind["확정·전부"] >= 52
assert same >= 141
assert tier["없음"] <= 94 and kind["없음"] <= 94
# 794-2c — 같은 말 묶음 · 옛 번호 대조표 · 기계 작업량 길(전 85 · 125 · 260)
assert tier["없음"] <= 58 and tier["확정"] >= 164
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 309
def _first(bid: str) -> dict:
return results()[0][bid]["근거후보"]
def test_plywood_four_same():
for bid in ("북부청임도#24", "북부청사방#66", "거창#41", "동부청영월#1841"):
(c,) = _first(bid)
assert (c["기초"], c["단"], c["품검산"]) == ("GC000211", "확정", "같음"), bid
assert {c["옵션"][k] for k in ("사용횟수", "유형")} == {"4회", "보통"}, bid
assert {"높이감률", "구멍땜"} <= set(c["입력필요"]), bid # 글에 없는 감률 = 입력 필요
def test_rebar_type2_cut_and_join():
got = {c["기초"]: c for c in _first("북부청임도#72")}
assert set(got) == {"GC000206", "GC000207"}
assert got["GC000206"]["옵션"]["Type"] == got["GC000207"]["옵션"]["Type"] == "Type-Ⅱ"
assert all(c["단"] == "확정" for c in got.values())
def test_remicon_small_50():
(c,) = _first("북부청임도#21")
assert c["기초"] == "GC000198" and c["단"] == "확정"
assert c["옵션"]["소량감률"] == 50 and c["옵션길"]["소량감률"] == "B5"
def test_excavation_51():
(c,) = _first("북부청임도#51")
assert (c["기초"], c["단"], c["옵션"]["Type"], c["옵션"]["깊이"]) == (
"GC000089",
"확정",
"TypeⅢ",
"5m 이하",
)
def test_rubble_carry_gc001140():
r = results()[0]["동부청영월#1466"]
c = r["근거후보"][0]
assert r["단"] == "사람" and c["기초"] == "GC001140"
assert c["옵션"] == {"재료갈래": "석재류", "운반로": "불량"}
assert set(c["옵션길"].values()) == {"V"}
def test_big_stone_near_unit_basis():
r = results()[0]["북부청임도#32"]
(c,) = r["근거후보"]
assert (r["단"], c["기초"], c["단"], c["품검산"]) == ("근접", "GF000226", "근접", "다름")
assert {t for t, _ in c["까닭"]} == {"단위 기준 차이"} and "÷10" in c["까닭"][0][1]
def test_no_made_up_values():
"""옵션마다 출처 꼬리표 · 입력 필요 · 0 간주 칸은 옵션에 없음 ·
없음 단은 근거후보 없이 보강 까닭만."""
for bid, r in results()[0].items():
for c in r["근거후보"]:
assert set(c["옵션"]) <= set(c.get("옵션길") or {}), bid
assert set((c.get("옵션길") or {}).values()) <= WAYS, bid
assert not set(c["옵션"]) & set(c["입력필요"]), bid
assert all(v is not None for v in c["옵션"].values()), bid
assert c["단"] in ("확정", "근접", "사람")
if r["단"] == "없음":
assert r["근거후보"] == [] and r["보강"]["까닭"], bid
def test_corpus_is_eight_files():
"""보정 자료 = 여덟 파일 상수(일반 뺌) — 뽑기 규칙을 바꾸면 다시 지어 넣음."""
preps = [
bc.prep(blk, book["꼴"], (book["숨은식"] or {}).get(str(blk["번"])))
for _, book in books()
for blk in book["단산"]
]
shipped = json.loads(bi.CORPUS.read_text(encoding="utf-8"))
assert sorted(map(json.dumps, bc.corpus(preps))) == sorted(map(json.dumps, shipped))
def test_store_round_trip(tmp_path):
"""임시 폴더 HP → 뒤 셈 저장본 · 목록 · 블록 하나 · 끝내기."""
_, book = books()[0]
small = {**book, "단산": [b for b in book["단산"] if b["번"] in (21, 51)]}
row = hopyo_store.save(tmp_path, "시험.xls", small, {"id": 0, "이름": "시험"})
key = row["키"]
assert store.solved(tmp_path, key) is None
got = store.solve_all(tmp_path, key)
assert set(got) == {"21", "51"} and store.solved(tmp_path, key) == json.loads(json.dumps(got))
assert store.listing(tmp_path, key)["셈중"] is False
assert store.block(tmp_path, key, 51)["근거후보"][0]["기초"] == "GC000089"
with pytest.raises(Exception):
store.block(tmp_path, key, 999)
store.forget(tmp_path, key)
assert store.solved(tmp_path, key) is None
judge.forget(tmp_path, key)
def test_hidden_g_lines():
book = {"꼴": "STmate", "단산": [{"번": "3", "행": [2, 4]}]}
rows = [[None] * 7, [None] * 6 + ["_x0007_L=20"], [None] * 6 + ["_x0007_"], [None] * 6 + ["x"]]
assert store.hidden(book, {store.SHEET: rows}) == {"3": ["L=20"]}
assert store.hidden({"꼴": "엑셀꼴", "단산": []}, {}) == {}
# ── 794-2c 손 확인 견본 ─────────────────────────────────────────────
HAND = [
("북부청임도#56", "GC001148", "확정"), # 잔토정리 Q 식 · 제목에 품셈 이름 없음 → 기계 작업량 길
("울진대흥#2264", "GC001148", "확정"), # 섞기 오인(GF000132) → 옛 11-3 · 굴착기
("울진대흥#2269", "GC001148", "확정"), # 섞기 오인 → 되메우기 굴착기 Q 식
("울진대흥#2290", "GC001148", "근접"), # 섞기 오인 → 적상 굴착기(운반 · 정지 절 미확정)
("동부청영월#111", "GC001189", "근접"), # 섞기 오인 → 옛 8-2-35 = 현행 8-2-32 임목파쇄기
("북부청임도#41", "GC000227", "확정"), # 휠라 = 채움재 · 옛 6-3-10 같은 절
("울진소광#1329", "GC000143", "확정"), # 평떼 = 잔디 · 옛 4-1-1 같은 절
("울진소광#794", "GF000298", "근접"), # 사방(옛) 4-16-2 → 판 대응표 산림 5-16-1
("동부청영월#33", "GC000129", "근접"), # 옛 3-7 → 현행 3-9-1 벌목(표 값 같음)
("울진대흥#2277", "GC001148", "확정"), # 옛 11-3 → 8-2-3 · 번호 + 검산
]
@pytest.mark.parametrize("bid,key,tier", HAND)
def test_794_2c_hand(bid, key, tier):
r = results()[0][bid]
keys = [c["기초"] for c in r["근거후보"]]
assert key in keys and r["단"] == tier, (bid, r["단"], keys)
assert "GF000132" not in keys, bid # 섞기(굴착기 0.7) 값 우연 짝 없음
def test_794_2c_no_invented_divisor():
"""「/ Q / 3」(1/3 적용) 블록은 검산 못 함 = 사람 · 1/3 을 지어 넣지 않음."""
r = results()[0]["봉화#24"]
assert r["단"] == "사람" and {c["기초"] for c in r["근거후보"]} == {"GC001147", "GC001148"}
assert all(c["옵션"] == {} for c in r["근거후보"])
def test_machine_consts_reader():
got = bm.consts(
[
"q = 0.7 , k = 0.7 , f =1/1.175= 0.85",
"E = 0.5 , Cm = 18 sec(90)",
"Q = 3600*q*k*E*f/Cm = 41.65 m3/hr",
]
)
assert got == [
{"기호": "q", "값": 0.7},
{"기호": "K", "값": 0.7},
{"기호": "f", "값": 0.85},
{"기호": "E", "값": 0.5},
{"기호": "Cm", "값": 18.0, "덧": "90˚"},
]
got = bm.consts(
[
"q (버킷용량(m3)) = 0.7 , f (체적환산계수) = 1/1.25 , K (버킷계수) = 0.9",
"Cm (1회 사이클 시간(초)) = 20 sec (135˚) , E = ( 0.55 + 0.45 ) / 2. - 0.05 = 0.45",
]
)
assert [(x["기호"], x["값"]) for x in got] == [
("q", 0.7),
("f", 0.8),
("K", 0.9),
("Cm", 20.0),
("E", 0.45),
]
assert bm.has_q(["3,600. × q × K × f × E", "Cm 20."]) and not bm.has_q(["Q = 6 ㎥/hr"])
def test_old_number_map():
assert {o["키"] for o in bm.old_rows("건설품셈(옛)", "11-3")} >= {"GC001147", "GC001148"}
assert "GC000129" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "3-7")}
assert "GC001189" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "8-2-35")}
assert bm.sabang_map()["4-16-2"] == "5-16-1"
assert "3-7" not in bm.sabang_map() # 임도(2017) 줄은 사방에 안 씀
assert bm.old_rows("건설품셈(옛)", "8-2") == [] # 대조 없는 번호 = 없음(윗마디 안 받음)