feat(810-4): 연결 찾기 뜻 근거 · 옵션 값 꼴 · 전수 판정 목록

- 확정 = 줄 · 금액 같음 + 뜻 근거(품셈 절 번호 같음 또는 이름 낱말 겹침) — 값만 같은 남의 기초(구조물되메우기 ↔ 숲가꾸기 · 모르타르 ↔ 기초다짐 등)는 다른 항 「이름 다름」 = 근접
- 찾은 옵션 = 목록 값 꼴 그대로(수 = 수) — SK 입력값 · M01 다시 셈이 같은 값(간벌재직경 「22」 글이면 표 줄 못 찾던 것 고침)
- 전수 도우미 파일마다 열 갈래로 나눠 맡김(큰 파일 한 프로세스가 붙잡지 않게) · 시험 S8104_ROWS 줄 목록 저장
- 전수(M01 수식 문서 1,518 × 원파일 산근 2,220 블록) — 확정 98 · 근접 617 · 없음 1,505 · 확정 다시 셈 다름 0 · 요구 704 파형강관 Φ800 = GF000179 관경 800 근접(단위 환산 × 6 · 기계 규격 · 율 줄) · 목록 docs/raw/요구_1104/_목록_810-4_연결판정.xlsx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 22:54:48 +09:00
co-authored by Claude Opus 5.5
parent 26bf63b726
commit 0b50fcab85
4 changed files with 55 additions and 10 deletions
+33 -4
View File
@@ -472,6 +472,7 @@ def _totals(got: dict) -> dict:
EXPLAINED = (
"이름 다름",
"단가 차이",
"단위 환산",
"기계 규격",
@@ -516,19 +517,21 @@ def _calc(doc: dict, links: Any, given: dict) -> dict | None:
def try_doc(
base: dict, links: Any, imported: dict, got_imp: dict, mine: list[dict], regions: set[str]
) -> dict:
"""M01 문서 하나 — 옵션 풀이 · 가장 나은 조합 판정."""
"""M01 문서 하나 — 옵션 풀이 · 가장 나은 조합 판정(이름 · 절 근거 없으면 「이름 다름」 = 확정 아님)."""
want = {
"수량": {x["수량"] for x in mine if x["수량"] is not None},
"단가": {x["단가"] for x in mine if x["단가"] is not None},
}
dom, ev = domains(base, imported, regions)
dom = narrow(base, links, dom, want, ev)
named = [] if same_name(imported, base) else [_unnamed(imported, base)]
best: dict | None = None
for pick in combos(base, dom) or [{}]:
got = _calc(base, links, _given(base, pick))
if got is None:
continue
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
off += named
res = verdict(mine, facts(base, got), got_imp, got, off)
rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"]))
if best is None or rank > best["순위"]:
@@ -577,17 +580,43 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
}
def _shown(doc: dict, pick: dict) -> dict[str, str]:
"""고른 옵션(기본과 다른 것) — {열쇠: 값 글}."""
def _shown(doc: dict, pick: dict) -> dict[str, Any]:
"""고른 옵션(기본과 다른 것) — {열쇠: 값}(수 = 수 · 선택 목록의 수 값과 같은 꼴 · SK 입력값)."""
out = {}
for v in doc.get("변수") or []:
vid = md.var_id(v)
if vid in pick and mc.plain_text(mc.number(pick[vid])) != mc.plain_text(
mc.number((v.get("연결") or {}).get("값"))
):
out[str(v.get("열쇠") or vid)] = mc.plain_text(mc.number(pick[vid]))
out[str(v.get("열쇠") or vid)] = ml._norm(mc.number(pick[vid]))
return out
_STOP = {"설치", "해체", "인력", "기계", "작업", "운반", "타설", "제작"}
def _grams(texts: list[Any]) -> set[str]:
"""이름 낱말 — 한글 두 글자 묶음 · 영문 · 수 낱말(일반 낱말 뺌)."""
out: set[str] = set()
for t in texts:
t = re.sub(r"\s|및", "", str(t or ""))
for run in re.findall(r"[가-힣]+", t):
out |= {run[i : i + 2] for i in range(len(run) - 1)}
out |= {w.lower() for w in re.findall(r"[A-Za-z]{2,}", t)}
return out - _STOP
def same_name(imported: dict, base: dict) -> bool:
"""뜻 근거 — 품셈 절 번호가 같거나 이름 낱말이 겹침(값만 같은 남의 기초를 확정으로 안 묶음)."""
secs = sections([x.get("글") for x in (imported.get("가져온") or {}).get("추정") or []])
if secs & sections([base.get("출처")]):
return True
return bool(_grams([imported.get("이름")]) & _grams([base.get("이름")]))
def _unnamed(imported: dict, base: dict) -> dict:
return {"항": "이름 다름", "글": f"{imported.get('이름')} ↔ {base.get('이름')}"}
def _plain(d: dict) -> dict:
return {k: (None if v is None else float(v)) for k, v in d.items()}
Binary file not shown.
+12 -3
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import collections
import json
import os
import pickle
import sys
import time
@@ -70,15 +71,22 @@ def file_docs(t, dim, dm, whole, kind: str, tag: str) -> dict[int, dict]:
return made
def sweep(arg: tuple[str, str]) -> list[dict]:
kind, tag = arg
PARTS = 10 # 파일마다 블록을 나눠 맡김(큰 파일 한 프로세스가 끝을 붙잡지 않게)
def sweep(arg: tuple[str, str, int]) -> list[dict]:
kind, tag, part = arg
t, dim, dm, whole, docs, index, links = _ctx()
rows = []
try:
made = file_docs(t, dim, dm, whole, kind, tag)
except Exception as e: # noqa: BLE001 — 원파일 없음(다른 PC) = 빈 줄
if part:
return []
return [{"파일": tag, "오류": repr(e)[:200], "판정": "오류", "없는파일": "없음" in repr(e)}]
for no, doc in made.items():
if no % PARTS != part:
continue
start = time.time()
try:
res = dm.match(doc, dim.Links(whole, "우리"), index, links)
@@ -101,7 +109,8 @@ def main() -> None:
import test_810_1_stc as c
tags = [("EST", k) for k in e.FILES] + [("STC", k) for k in c.FILES]
with Pool(len(tags)) as pool:
tags = [(kind, tag, part) for kind, tag in tags for part in range(PARTS)]
with Pool(min(len(tags), max(2, (os.cpu_count() or 4) - 2))) as pool:
rows = [r for part in pool.map(sweep, tags) for r in part]
if "--rows" in sys.argv:
pickle.dump(rows, open(sys.argv[sys.argv.index("--rows") + 1], "wb"))
+10 -3
View File
@@ -9,7 +9,7 @@
(d) 바꾸기 — 임시 층 개인: 확정 SK = 같은 키에 M01 사본(바탕 · 입력값) · 금액 = M01 문서 셈 · 원래 몸 보관 →
되돌리기 = 원래 금액 · 근접 SK 바꾸기 = 409 · M01 문서 파일 그대로
(e) 전수(1,518 문서 × 원파일 산근 2,220 블록 · 파일마다 프로세스) — 확정은 모두 다시 셈해도 금액 같음 ·
판정 셋 · 근접 다른 항 갈래 표(S8104_ALL=1 일 때만 · 10 분 남짓)
판정 셋 · 근접 다른 항 갈래 표(S8104_ALL=1 일 때만 · 수십 분 · S8104_ROWS=<pkl> = 줄 목록 저장)
원파일 없으면 그 시험만 skip(다른 PC).
"""
@@ -75,7 +75,7 @@ def test_near_formwork_lists_differences() -> None:
def test_req704_pipe_is_near_with_unit_rate_machine() -> None:
got = judge(19) # 파형강관 매설 Φ800 ↔ GF000179(요구 704)
assert got["판정"] == dm.NEAR and got["키"] == "GF000179" and got["옵션"] == {"관경": "800"}
assert got["판정"] == dm.NEAR and got["키"] == "GF000179" and got["옵션"] == {"관경": 800}
kinds = [d["항"] for d in got["다른"]]
assert kinds.count("단위 환산") == 2 and "기계 규격" in kinds and "율 줄" in kinds
assert any("× 6" in d["글"] for d in got["다른"])
@@ -125,6 +125,12 @@ def test_compare_kinds_and_no_tolerance() -> None:
rate = {**_line("율", "율", "1", "3", "경비"), "수량": None, "단가": None}
res = dm.verdict([*a, rate], a, tot, tot)
assert res["다른"] == [{"항": "율 줄", "글": "가져온 쪽만 — 율"}] and res["판정"] == dm.NEAR
named = dm.verdict(
a, list(a), tot, tot, [dm._unnamed({"이름": "구조물되메우기"}, {"이름": "숲가꾸기"})]
)
assert named["판정"] == dm.NEAR # 값만 같은 남의 기초 = 확정 아님
assert not dm.same_name({"이름": "모르타르"}, {"이름": "기초다짐 및 뒷채움(인력)"})
assert dm.same_name({"이름": "유로폼 설치및해체"}, {"이름": "유로폼 설치 및 해체"})
c = [_line("딴", "노임", "3", "7", code="LB9")]
assert dm.verdict(a, c, tot, tot)["판정"] == dm.NONE
@@ -180,8 +186,9 @@ def test_every_block_table() -> None:
import collections
import subprocess
rows = ["--rows", os.environ["S8104_ROWS"]] if os.environ.get("S8104_ROWS") else []
out = subprocess.run(
[sys.executable, str(HERE / "helper_810_4_sweep.py")], capture_output=True, text=True
[sys.executable, str(HERE / "helper_810_4_sweep.py"), *rows], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr[-2000:]
import json