Files
Aislo/M02_MasterTemplete/M02_Hopyo_Combine.py
T
eomsangdonandClaude Opus 5.5 e9e949112c feat(일위대가실무): 338-1 맞춤 핵심 서버 1 단계 — 엑셀 지문 · 우리 로직 지문 캐시 · 옵션 거꾸로 · 순위 · 판정 · 조합 · 뒤 셈 + 정답 표
- 새 파일 — Words(식 풀기 · 장비 갈래 · 조건 낱말) · Finger(블록 글 · STmate 자원 → 지문 · 기계 줄 · 직접 줄 호표) · Shares(절 몫 벡터) · Ours(마스터 판 열쇠 · 우리 지문 캐시 tmp/m02_hopyo · 엔진 셈 캐시 · 자재 값 찾기 건너뜀) · Option(옵션 공간 · 거꾸로 찾기 · 견줌) · Fit(손잡이 한 곳 · 순위 · 뒤 셈 · 맞춤셈 캐시) · Combine(조합 · 섞인 줄 몫 먼저) · Verdict(블록 셈 · 맞음 / 다른 점 / 후보)
- Match — b_order(이름 · 근거 차례 · 같음 근거 검사 · 옛 판 글 · 조항 오타) · clause(윗마디 0.9 · 나 창 Basis.clause 있으면 그것) · 이름 후보 로직은 접지 않음
- 정답 표 fixtures/m02_hopyo_golden.json(사람 136 · 새 파일 51) · 골든 도우미
- 북부청 92 줄 실측 — 1순위 87 · 다섯 안 91 · 맞음 41 틀림 0 · 다른 점 28 틀림 0 · 조합 맞음 57/66 · 우리 지문 셈 37 초 · 캐시 읽기 0.05 초

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-15 23:36:23 +09:00

301 lines
13 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.
"""M02 일위대가 맞춤 — 조합 풀이(PLAN 338-1 · 설계 2-7): 블록 몫(절마다 노임 · 장비 벡터)을 우리
로직 단위당 벡터 조합으로 풂 · 섞인 줄은 인력 몫 로직이 1순위(몫 먼저).
맞음 = 차원(직종 · 장비 갈래)마다 작은/큰 가중 평균(보통인부 0.5 · 장비 있음 0.5 + 시간 0.5 ·
크기 다르면 × 0.8 · 갈래 일부만 같으면 × 0.8) · 몫 x(0.1 … 0.9) × 0.95 · 제목 몫 수와 같은 x 면
+0.1.
덧 로직 = 1순위가 못 덮는(< COVER) 절은 절 1위(≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 =
「기계 시간당 기계경비」(체크 안 함 · 시간 = 엑셀 값).
순위 덤 = 앞 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 앞 차례 15 + 드문 직종 · 장비
색인
(40 개 이하) + 절 제목 이름 후보 8. 우리 벡터 = `M02_Hopyo_Ours` 지문 변형 L · X(따로 안 셈).
규칙 · 문턱은 336 실험(C5 `c03_vec` · `c05_solve` · C7 `solve`) 그대로 · 문턱은 `M02_Hopyo_Fit`
머리.
"""
from __future__ import annotations
import re
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Ours as ours
COMMON = "보통인부"
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
MACHINE_ANY = "GC000268"
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
_SHARE = re.compile(r"(\d{1,3})\s*%")
_RATIO = re.compile(r"(\d)\s*:\s*(\d)")
_STATE: dict[str, Any] = {"판": None}
# ── 우리 벡터 · 색인 ───────────────────────────────────────────────────
def _vec(v: dict[str, Any]) -> dict[str, Any]:
eq = [(frozenset(a), frozenset(b), q) for a, b, q in v.get("X") or [] if a and q]
return {"노임": {t: q for t, q in (v.get("L") or {}).items() if q}, "장비": eq}
def _load() -> dict[str, Any]:
"""{변형: {로직: [(입력, 벡터)]}, 색인: {차원: {로직}}} — 우리 지문 판마다 한 번."""
table = ours.table() or []
ver = ours.version()
if _STATE["판"] != (ver, id(table)):
var: dict[str, list] = {}
idx: dict[str, set] = {}
for o in table:
vs = [(x.get("in") or {}, _vec(x)) for x in o.get("변형") or []]
var[o["로직"]] = [(g, v) for g, v in vs if v["노임"] or v["장비"]]
if var[o["로직"]]:
for d in dims(var[o["로직"]][0][1]):
idx.setdefault(d, set()).add(o["로직"])
_STATE.update(판=(ver, id(table)), 변형=var, 색인=idx)
return _STATE
def variants(code: str) -> list[tuple[dict, dict]]:
return _load()["변형"].get(code) or []
def _kinds(key: str) -> tuple[frozenset, str]:
kind, _, size = key.partition("|")
return frozenset(kind.split("+")), size
def dims(E: dict[str, Any]) -> set[str]:
out = {t for t in E.get("노임") or {} if t != COMMON}
eq = E.get("장비") or {}
if isinstance(eq, dict):
return out | {"+".join(sorted(_kinds(k)[0])) for k in eq}
return out | {"+".join(sorted(a)) for a, _, _ in eq}
def sim(E: dict[str, Any], ov: dict[str, Any], x: float = 1.0) -> float:
"""E = 엑셀 몫 {노임, 장비: {「굴착기|0.6」: 시간}} · ov = 우리 벡터 — 0..1."""
num = den = 0.0
et, ot = E.get("노임") or {}, ov.get("노임") or {}
for t in set(et) | set(ot):
w = 0.5 if t == COMMON else 1.0
a, b = et.get(t, 0.0), ot.get(t, 0.0) * x
den += w
num += w * min(a, b) / max(a, b) if a > 0 and b > 0 else 0
oe = ov.get("장비") or []
used = set()
for k, a in (E.get("장비") or {}).items():
kinds, size = _kinds(k)
best, pick = 0.0, None
for at, (kinds2, sizes2, b) in enumerate(oe):
if not kinds & kinds2:
continue
f = 1.0 if (not size or not sizes2 or float(size) in sizes2) else 0.8
f *= 1.0 if kinds == kinds2 else 0.8 # 굴착기 ↔ 굴착기+집게
b *= x
s = f * (0.5 + 0.5 * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0))
if s > best:
best, pick = s, at
den += 1.0
num += best
if pick is not None:
used.add(pick)
den += sum(1.0 for at in range(len(oe)) if at not in used)
return num / den if den else 0.0
def fit_one(E: dict[str, Any], code: str, share: bool) -> tuple[float, dict, float]:
"""(맞음, 입력, 몫 x) — 변형마다 x = 1 과 몫 x 중 가장 좋은 것."""
top = (0.0, {}, 1.0)
for given, ov in variants(code):
s = sim(E, ov)
if s > top[0]:
top = (s, given, 1.0)
if share:
for x in fit.SHARES:
s2 = 0.95 * sim(E, ov, x)
if s2 > top[0]:
top = (s2, given, x)
return top
def cover(E: dict[str, Any], code: str) -> float:
"""E 의 차원을 로직 벡터가 얼마나 덮나(로직의 남는 차원은 안 셈)."""
top = 0.0
for _, ov in variants(code):
num = den = 0.0
for t, a in (E.get("노임") or {}).items():
w = 0.5 if t == COMMON else 1.0
b = (ov.get("노임") or {}).get(t, 0.0)
den += w
num += w * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0)
for k in E.get("장비") or {}:
den += 1
num += any(_kinds(k)[0] & a for a, _, _ in ov.get("장비") or [])
top = max(top, num / den if den else 0)
return top
# ── 후보 풀 · 앞선 믿음 ────────────────────────────────────────────────
def title_cands(title: str, unit: str, got: dict, n: int = 8) -> list[str]:
text = re.sub(r"\s+", "", re.sub(r"[\d.·:()\[\]%]+", " ", str(title or "")))
if len(text) < 2:
return []
found = hopyo_match.candidates({"이름": text, "단위": unit}, "로직", got, n)
return [c["로직"] for c in found if c.get("로직")]
class Prior:
"""앞선 믿음 — 앞 차례 · 줄 근거 조항(윗마디 포함) · 같은 책."""
def __init__(self, line: dict[str, Any], ranked: list[str]):
self.rank = {c: k for k, c in enumerate(ranked)}
self.n = max(len(ranked), 1)
self.theirs = [list(x) for x in ((line.get("근거") or {}).get("출처") or [])]
self.books = {b for b, _ in self.theirs}
self.table = hopyo_basis.ours()
def __call__(self, code: str) -> float:
p = 0.3 * (1 - self.rank[code] / self.n) if code in self.rank else 0.0
our = self.table.get(code)
if our and self.theirs:
p += 0.3 if hopyo_match.clause(self.theirs, our)[0] >= 0.9 else 0
p += 0.1 if our["책"] in self.books else 0
return p
def _sum(parts: list[dict]) -> dict[str, Any]:
out: dict[str, dict] = {"노임": {}, "장비": {}}
for s in parts:
for k in ("노임", "장비"):
for d, q in (s.get(k) or {}).items():
out[k][d] = out[k].get(d, 0) + q
return out
def pool_for(E: dict[str, Any], base: list[str], extra: list[str]) -> list[str]:
idx = _load()["색인"]
rare: set[str] = set()
for d in dims(E):
if len(idx.get(d, ())) <= 40:
rare |= idx.get(d, set())
return list(dict.fromkeys([*base, *extra, *sorted(rare)]))
def _hint(title: Any) -> float | None:
m = _SHARE.search(str(title or ""))
return int(m.group(1)) / 100 if m and 0 < int(m.group(1)) < 100 else None
def ranked(E: dict, pool: list[str], prior: Prior, share: bool, hint: float | None) -> list[tuple]:
"""[(순위 점수, 맞음, 몫 x, 로직, 입력)] — 순위 차례 · 제목 몫과 같은 x 면 +0.1."""
out = []
for c in pool:
f, given, x = fit_one(E, c, share)
if f <= 0:
continue
score = f + prior(c)
if hint is not None:
score += 0.1 if abs(x - hint) < 1e-9 else -0.1
out.append((score, f, x, c, given))
out.sort(key=lambda z: -z[0])
return out
def is_mixed(parts: list[dict], line: dict[str, Any]) -> bool:
"""섞인 줄 — 제목 · 이름 · 규격에 몫 표시 + 노임만 든 몫 + 장비만 든 몫."""
text = " ".join(
[str(p.get("제목")) for p in parts] + [str(line.get(k)) for k in ("이름", "규격")]
)
marked = bool(_SHARE.search(text) or _RATIO.search(text))
man = any(p.get("노임") and not p.get("장비") for p in parts)
mach = any(p.get("장비") and not p.get("노임") for p in parts)
return marked and man and mach
def machine_of(E: dict[str, Any]) -> str:
kinds = {frozenset(_kinds(k)[0]) for k in E.get("장비") or {}}
return MACHINE["굴착기"] if kinds == {frozenset({"굴착기"})} else MACHINE_ANY
# ── 풀이 ───────────────────────────────────────────────────────────────
def combine(
primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str]
) -> list[dict[str, Any]]:
"""[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직."""
one = lambda c, w, f, x, step, g=None: { # noqa: E731
"로직": c,
"어디": w,
"맞음": f,
"몫": x,
"단계": step,
"입력": g or {},
}
chosen = [one(primary, "1순위", None, 1.0, "1순위")] if primary else []
if not parts:
return chosen
prior, unit = Prior(line, base), str(line.get("단위") or "")
mixed = is_mixed(parts, line)
groups: dict[str, list] = {}
for p in parts:
groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p)
have = {primary} if primary else set()
def machine(E: dict, where: str) -> None:
if mixed or len(groups) > 1:
m = machine_of(E)
chosen.append(one(m, where, None, 1.0, "장비 몫"))
have.add(m)
for title, ps in groups.items():
E = _sum(ps)
if primary and cover(E, primary) >= fit.COVER:
continue
if E["장비"] and not E["노임"]: # 장비만 든 절 = 기계 시간 로직(닫힌 세계 규칙)
machine(E, title)
continue
share = any(_hint(p.get("제목")) for p in ps) or _hint(title) is not None
pool = pool_for(E, base, title_cands(title, unit, got))
cand = ranked(E, pool, prior, share, _hint(title))
if cand and cand[0][1] >= fit.COMBO[1] and cand[0][3] not in have:
chosen.append(
one(cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절", cand[0][4])
)
have.add(cand[0][3])
continue
for p in ps:
if primary and cover(p, primary) >= fit.COVER:
continue
if p.get("장비") and not p.get("노임"):
machine(p, p.get("제목"))
continue
hint = _hint(p.get("제목"))
pool = pool_for(p, base, title_cands(p.get("제목"), unit, got))
cand = ranked(p, pool, prior, hint is not None or share, hint)
if cand and cand[0][1] >= fit.COMBO[2] and cand[0][3] not in have:
f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4]
chosen.append(one(c, p.get("제목"), f, x, "몫", g))
have.add(c)
return chosen
def solve(
top: list[str], parts: list[dict], line: dict, got: dict, base: list[str]
) -> tuple[list[str], list[dict[str, Any]]]:
"""(새 차례, 조합 대상) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를 인력 몫
로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것."""
chosen = combine((top or [None])[0], parts, line, got, base)
codes = list(dict.fromkeys(c["로직"] for c in chosen if c["로직"]))
if top and len(codes) > 1 and parts and is_mixed(parts, line):
if cover(_sum(parts), top[0]) < fit.MIXED_CUT:
labor = [c for c in chosen if c["로직"] != top[0] and c["단계"] in ("몫", "절")]
if labor:
chosen = [c for c in chosen if c["로직"] != top[0]]
top = [labor[0]["로직"]] + [c for c in top if c != labor[0]["로직"]][:4]
seen: set[str] = set()
chosen = [
c for c in chosen if not (c["로직"] in seen or seen.add(c["로직"]))
] # 같은 로직은 처음 것
if len(chosen) < 2:
return top, []
return top, [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen]