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:
@@ -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.
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user