- M01 값 역색인 · 숫자 정규화 한 벌(N1~N10 · 단위 기준 · % ↔ 소수 · t ↔ kg · 인/양 몫) · M01 지문마다 한 번 · 서버 시작 때 뒤에서 지음
- 값 길(상수 지문 → 로직 · 우연 보정 유의 4) · 합친 길(분석 648-D2 차례 · 후보 2~6 엔진 품 대조) · 검산 · 옵션(B1~B6 → 값 길 V → 고르기 맞댐 → 입력 필요)
- 세 단 확정 · 근접(판 차이 · 현장 값 · 감률 · 할증 배수 · 단위 기준 차이) · 사람(+ 값 약한 1 등) · 없음(M01 보강 목록)
- 길 GET hopyo-basis/{키} · hopyo-basis/{키}/{번} · 파생 저장본 · 올림 때 HP 숨은식(STmate G 식) · 끝내기 때 지움 · 판정 길 그대로
- 보정 자료 = 여덟 파일 상수 1,094 · 시험 test_793_3a 11(459 블록 기초 260 · 옵션 전부 52 · 품 같음 141 · 없음 85)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
612 lines
24 KiB
Python
612 lines
24 KiB
Python
"""근거 제안 1 — M01 값 역색인 · 숫자 정규화 한 벌(PLAN 793-3a · 요구 648 · 분석 648-D 1 장) ·
|
||
읽기만.
|
||
|
||
칸(`cells`) = 표 칸 하나 {v, 표, 줄, 칸, 단위, 종류 val(값칸) | cond(조건 수) | ratio(「a 인 / b」
|
||
몫) |
|
||
기준(「10㎡」 당) | eq(기계 요소 값)} · 쓰임(`uses`) = 표 → [(로직, 부름, 칸 | None, 글자 거름,
|
||
입력 묶음)]
|
||
(하위 로직 3 겹까지 펼침 · 인자 이름 바꿈) · 식 속 수(`inline`) · 호표 수량 속 수(`inline_trade` ·
|
||
직종 표지).
|
||
정규화(`lookup`) — N1 표시 자릿수 반올림 · N2 % ↔ 소수 · N3 t ↔ kg · N4 인/양 몫 · N5 직종 묶음 ·
|
||
N6 표 기준 · N7 식 속 수 · N8 단위 성격 · N9 일반 수 · N10 입력 뺌(N9 · N10 =
|
||
`M02_San_Basis_Const`).
|
||
우연 보정(`q_of` · `pb_sig`) = 보정 자료(실무 여덟 파일 상수 · `M02_San_Basis_corpus.json`) 몫 ·
|
||
포아송 이항 꼬리 −log10.
|
||
짓기 = M01 폴더 지문(`stamp`)마다 한 번(`get` · 프로세스 안 캐시 · 지문 바뀌면 다시).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import bisect
|
||
import glob
|
||
import json
|
||
import math
|
||
import re
|
||
import threading
|
||
import unicodedata
|
||
from collections import defaultdict
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from common_util import common_util_derived_store as derived
|
||
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||
|
||
mf, ma = hb.mf, hb.ma
|
||
CORPUS = Path(__file__).with_name("M02_San_Basis_corpus.json")
|
||
INLINE_MAX = 20 # 식 속 수가 이보다 많은 로직(표를 식으로 펼친 것)은 식 속 수 맞음을 안 셈
|
||
PERSON = ("인",)
|
||
|
||
# ── 단위 성격(N8) ─────────────────────────────────────────────────────
|
||
FAM = [
|
||
x.split(":")
|
||
for x in (
|
||
"인:사람 ㎡:넓이 m2:넓이 ㎥:부피 m3:부피 ℓ:부피 km:길이 ㎝:길이 cm:길이 ㎜:길이 mm:길이 "
|
||
"m:길이 ㎏:무게 kg:무게 ton:무게 톤:무게 t:무게 g:무게 %:율 hr:시간 h:시간 시간:시간 "
|
||
"분:시간 min:시간 초:시간 sec:시간 일:시간 회:수 본:수 개:수 매:수 조:수 대:수 ea:수"
|
||
).split()
|
||
]
|
||
|
||
|
||
def fam(unit: Any) -> str | None:
|
||
u = unicodedata.normalize("NFKC", str(unit or "")).strip().lower().split("/")[0].strip()
|
||
if "%" in u:
|
||
return "율"
|
||
for a, f in FAM:
|
||
if u == a or u.startswith(a) and a not in ("m", "t", "h", "g"):
|
||
return f
|
||
return None
|
||
|
||
|
||
def unit_ok(tu: Any, cu: Any) -> bool:
|
||
a, b = fam(tu), fam(cu)
|
||
return a is None or b is None or a == b
|
||
|
||
|
||
_OPS = {
|
||
ast.Add: float.__add__,
|
||
ast.Sub: float.__sub__,
|
||
ast.Mult: float.__mul__,
|
||
ast.Div: float.__truediv__,
|
||
}
|
||
|
||
|
||
def _calc(node: ast.AST) -> float:
|
||
"""수 · 사칙 · 부호만(그 밖 = ValueError) — eval 없음."""
|
||
if isinstance(node, ast.Expression):
|
||
return _calc(node.body)
|
||
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
||
return float(node.value)
|
||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
|
||
x = _calc(node.operand)
|
||
return -x if isinstance(node.op, ast.USub) else x
|
||
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
|
||
return _OPS[type(node.op)](_calc(node.left), _calc(node.right))
|
||
raise ValueError(node)
|
||
|
||
|
||
def _arith(s: str) -> float | None:
|
||
"""「480 - 30」 · 「1/1.30」 → 수 · 못 셈 None."""
|
||
if not re.fullmatch(r"[\d.\s+\-*/()]+", s) or not re.search(r"\d", s):
|
||
return None
|
||
try:
|
||
return _calc(ast.parse(s.strip(), mode="eval"))
|
||
except (SyntaxError, ValueError, ZeroDivisionError):
|
||
return None
|
||
|
||
|
||
def nums_of(v: Any) -> list[float]:
|
||
"""칸 값 → [수] — 수 · [아래, 위] · 식 글자(셈 값 + 낱 수)."""
|
||
if isinstance(v, bool) or v is None:
|
||
return []
|
||
if isinstance(v, (int, float)):
|
||
return [float(v)]
|
||
if isinstance(v, list):
|
||
return [float(x) for x in v if isinstance(x, (int, float)) and not isinstance(x, bool)]
|
||
if isinstance(v, str):
|
||
got = _arith(v)
|
||
if got is None:
|
||
return []
|
||
parts = [float(x) for x in re.findall(r"\d+(?:\.\d+)?", v)]
|
||
return [got] + [p for p in parts if p != got]
|
||
return []
|
||
|
||
|
||
# ── 직종(N5) ─────────────────────────────────────────────────────────
|
||
@lru_cache(maxsize=None)
|
||
def trade(name: str) -> str | None:
|
||
return est.trade(name)
|
||
|
||
|
||
def _flat(t: Any) -> str:
|
||
return re.sub(r"[\s()()_\-·.]", "", re.sub(r"\(.*?\)|_.*$", "", str(t or "")))
|
||
|
||
|
||
def canon(t: str) -> str:
|
||
return trade(t) or trade(_flat(t)) or _flat(t)
|
||
|
||
|
||
def same_trade(a: str, b: str) -> bool:
|
||
x, y = canon(a), canon(b)
|
||
return bool(x and y) and (x == y or len(y) >= 2 and len(x) >= 2 and (x in y or y in x))
|
||
|
||
|
||
# ── M01 읽기 · 로직 훑기 ─────────────────────────────────────────────
|
||
def folder() -> Path:
|
||
return Path(hb.m01.FOLDER)
|
||
|
||
|
||
def stamp() -> str:
|
||
"""M01 지문 — 첫 층 파일 내용(`M02_Hopyo_Judge._stamp` 와 같은 몫) + 보정 자료."""
|
||
files = {
|
||
p.name: derived.file_digest(p)
|
||
for p in sorted(folder().glob("*.json"))
|
||
if not p.name.startswith("_")
|
||
}
|
||
return derived.derived_key("san-basis-index", files, derived.file_digest(CORPUS))
|
||
|
||
|
||
def load(root: Path) -> tuple[dict, dict, dict, dict]:
|
||
tables, logic, eq = {}, {}, {}
|
||
for f in glob.glob(str(root / "소요량_*.json")) + glob.glob(str(root / "계수_*.json")):
|
||
for t in json.loads(Path(f).read_text(encoding="utf-8")).get("표", []):
|
||
tables[t["키"]] = t
|
||
for f in glob.glob(str(root / "로직_*.json")):
|
||
for it in json.loads(Path(f).read_text(encoding="utf-8"))["줄"]:
|
||
logic[it["키"]] = it
|
||
for r in json.loads((root / "기계.json").read_text(encoding="utf-8"))["줄"]:
|
||
eq[r["키"]] = r
|
||
by_no: dict = {}
|
||
for k, t in tables.items():
|
||
by_no.setdefault((k[:2], str(t.get("원문번호"))), []).append(k)
|
||
return tables, logic, eq, by_no
|
||
|
||
|
||
def _resolve(ref: str, tables: dict, by_no: dict) -> str | None:
|
||
if "{" in ref:
|
||
return None
|
||
if ref in tables:
|
||
return ref
|
||
tid, _, no = ref.partition(":")
|
||
hit = by_no.get((tid, no)) or []
|
||
return hit[0] if len(hit) == 1 else None
|
||
|
||
|
||
def scan(item: dict, tables: dict, by_no: dict) -> tuple:
|
||
"""로직 하나 → (찾기 부름, 식 속 수, 하위 로직 부름, 기계 부름, 호표 수량 속 수)."""
|
||
names = {s["이름"] for s in item.get("입력", [])}
|
||
finds, nums, calls, eqs = [], [], [], []
|
||
|
||
def arg_of(node):
|
||
if node[0] == "str":
|
||
return ("lit", node[1])
|
||
if node[0] == "num":
|
||
return ("lit", float(node[1]))
|
||
if node[0] == "name":
|
||
return ("name", node[1]) if node[1] in names else ("expr", node[1])
|
||
return ("expr", str(node)[:80])
|
||
|
||
def visit(node, field=None):
|
||
kind = node[0]
|
||
if kind == "field":
|
||
visit(node[1], node[2])
|
||
elif kind in ("find", "logic"):
|
||
if kind == "find" and (ref := _resolve(node[1], tables, by_no)):
|
||
finds.append((ref, field, {c: arg_of(a) for c, a in node[2]}))
|
||
if kind == "logic" and isinstance(node[1], str) and "{" not in node[1]:
|
||
calls.append((node[1], {c: arg_of(a) for c, a in node[2]}))
|
||
for _, a in node[2]:
|
||
if a[0] not in ("str", "num", "name"):
|
||
visit(a)
|
||
elif kind == "ref":
|
||
if node[1].startswith("EQ") and field:
|
||
eqs.append((node[1], field))
|
||
elif kind == "num":
|
||
nums.append(float(node[1]))
|
||
elif kind == "bin":
|
||
for side in (node[2], node[3]): # 비교 글자(「SCH == 20」)는 셈 수가 아님
|
||
if node[1] in mf._OPS and side[0] == "num":
|
||
continue
|
||
visit(side)
|
||
elif kind == "neg":
|
||
visit(node[1])
|
||
elif kind == "call":
|
||
for a in node[2]:
|
||
visit(a)
|
||
|
||
for text in ma._texts(item):
|
||
if any(text is h["수량"] for h in item.get("호표", [])):
|
||
continue
|
||
try:
|
||
visit(mf.parse(text))
|
||
except mf.FormulaError:
|
||
pass
|
||
trade_nums = [] # (호표 이름, 수량 식 속 수)
|
||
for h in item.get("호표", []):
|
||
before = len(nums)
|
||
try:
|
||
visit(mf.parse(h["수량"]))
|
||
except mf.FormulaError:
|
||
continue
|
||
trade_nums += [(h.get("이름") or "", x) for x in nums[before:]]
|
||
return finds, nums, calls, eqs, trade_nums
|
||
|
||
|
||
def _cells(tables: dict, eq: dict) -> list[dict]:
|
||
cells: list[dict] = []
|
||
for tk, t in tables.items():
|
||
vcols = t.get("값칸", {}) or {}
|
||
base = re.match(r"\s*(\d+(?:\.\d+)?)\s*(\S*)", str(t.get("기준") or ""))
|
||
if base and float(base.group(1)) not in (0, 1): # 「10㎡」 · 「100m」 당 — 표 기준 수(N6)
|
||
cells.append(
|
||
{
|
||
"v": float(base.group(1)),
|
||
"표": tk,
|
||
"줄": 0,
|
||
"칸": "기준",
|
||
"단위": base.group(2),
|
||
"종류": "기준",
|
||
}
|
||
)
|
||
for ri, row in enumerate(t.get("줄", [])):
|
||
got = {}
|
||
for col, unit in vcols.items():
|
||
for x in nums_of(row.get(col)):
|
||
cells.append(
|
||
{
|
||
"v": x,
|
||
"표": tk,
|
||
"줄": ri,
|
||
"칸": col,
|
||
"단위": row.get("단위") or unit,
|
||
"종류": "val",
|
||
}
|
||
)
|
||
got.setdefault(col, x)
|
||
for col, kind in (t.get("조건", {}) or {}).items():
|
||
x = row.get(col)
|
||
if kind == "수" and isinstance(x, (int, float)) and not isinstance(x, bool):
|
||
cells.append(
|
||
{"v": float(x), "표": tk, "줄": ri, "칸": col, "단위": "", "종류": "cond"}
|
||
)
|
||
for a, ua in vcols.items(): # 인/양 몫(N4 · 「5인 / 45㎡」 = 0.111)
|
||
if not str(row.get("단위") or ua).startswith(PERSON) or a not in got:
|
||
continue
|
||
for b, ub in vcols.items():
|
||
ub = str(row.get("단위") or ub)
|
||
if b == a or b not in got or ub.startswith(PERSON) or "%" in ub or not got[b]:
|
||
continue
|
||
cells.append(
|
||
{
|
||
"v": got[a] / got[b],
|
||
"표": tk,
|
||
"줄": ri,
|
||
"칸": f"{a}/{b}",
|
||
"단위": "인",
|
||
"종류": "ratio",
|
||
}
|
||
)
|
||
for ek, r in eq.items(): # 기계 요소 수(가격 · 글 뺌)
|
||
for col, x in (r.get("값") or {}).items():
|
||
if col in ("가격", "주연료종류", "조종원직종") or col.endswith("가격"):
|
||
continue
|
||
unit = (r.get("단위") or {}).get(col, "") if isinstance(r.get("단위"), dict) else ""
|
||
cells += [
|
||
{"v": y, "표": ek, "줄": 0, "칸": col, "단위": unit or "", "종류": "eq"}
|
||
for y in nums_of(x)
|
||
]
|
||
return cells
|
||
|
||
|
||
# ── 역색인 ───────────────────────────────────────────────────────────
|
||
class Index:
|
||
def __init__(self, root: Path | None = None):
|
||
self.tables, self.logic, self.eq, by_no = load(root or folder())
|
||
self.cells = _cells(self.tables, self.eq)
|
||
self.scans = {k: scan(it, self.tables, by_no) for k, it in self.logic.items()}
|
||
order = sorted(range(len(self.cells)), key=lambda i: self.cells[i]["v"])
|
||
self.vals = [self.cells[i]["v"] for i in order]
|
||
self.ids = order
|
||
self.uses = defaultdict(list) # 표 → [(로직, 부름id, 칸 | None, 거름, 묶음)]
|
||
self.eq_uses = defaultdict(list) # 기계 칸 → [(로직, 부름id, (고정 | 입력, 값))]
|
||
self.inline = defaultdict(set)
|
||
self.inline_trade = defaultdict(set)
|
||
self.eq_no = {str(r.get("원문번호")): k for k, r in self.eq.items()} | {
|
||
k: k for k in self.eq
|
||
}
|
||
self.callers = defaultdict(set) # 하위 → 부르는 위 로직(어느 깊이든)
|
||
for k in self.logic:
|
||
self._expand(k, k, {}, {}, 0, k)
|
||
self.inl_index = defaultdict(set)
|
||
for k, xs in self.inline.items():
|
||
for x in xs:
|
||
self.inl_index[round(x, 9)].add(k)
|
||
self.trade_index = defaultdict(set)
|
||
for k, xs in self.inline_trade.items():
|
||
for _, x in xs:
|
||
self.trade_index[round(x, 9)].add(k)
|
||
self.cells_of_table = defaultdict(list)
|
||
for c in self.cells:
|
||
self.cells_of_table[c["표"]].append(c)
|
||
self.item_tables = defaultdict(set)
|
||
for t, us in self.uses.items():
|
||
for u in us:
|
||
self.item_tables[u[0]].add(t)
|
||
self._hits: dict = {}
|
||
self._look: dict = {}
|
||
self._df: dict = {}
|
||
self.prior()
|
||
|
||
def _expand(self, top, key, rename, lit, depth, path):
|
||
finds, nums, calls, eqs, tnums = self.scans[key]
|
||
for name, x in tnums:
|
||
self.inline_trade[top].add((name, x))
|
||
for ref, field, args in finds:
|
||
filt, bind = {}, {}
|
||
for col, (kind, val) in args.items():
|
||
if kind == "lit":
|
||
filt[col] = val
|
||
elif kind == "name" and depth == 0:
|
||
bind[col] = val
|
||
elif kind == "name" and val in rename:
|
||
bind[col] = rename[val]
|
||
elif kind == "name" and val in lit:
|
||
filt[col] = lit[val]
|
||
else: # 중간 · 식 · 안 넘긴 입력 — 같은 인자를 쓰는 부름끼리 같은 줄(가짜 입력 §)
|
||
bind[col] = f"§{path}:{val}"
|
||
if not filt and not bind: # 조건 없는 부름 — 같은 표 부름끼리 같은 줄
|
||
bind["#row"] = f"§{path}:{ref}"
|
||
gid = f"{path}|{ref}|{sorted(filt.items(), key=str)}|{sorted(bind.items())}"
|
||
self.uses[ref].append((top, gid, field, filt, bind))
|
||
self.inline[top].update(nums)
|
||
for ref, field in eqs:
|
||
m = re.match(r"EQ:\{(\w+)\}", ref)
|
||
if m:
|
||
arg = m.group(1)
|
||
inputs = {sp["이름"] for sp in self.logic[key].get("입력", [])}
|
||
if depth == 0 and arg in inputs:
|
||
use = ("입력", arg)
|
||
elif depth > 0 and arg in rename:
|
||
use = ("입력", rename[arg])
|
||
elif depth > 0 and arg in lit and self.eq_no.get(str(lit[arg])):
|
||
use = ("고정", self.eq_no[str(lit[arg])])
|
||
else:
|
||
use = ("입력", f"§{path}:EQ:{arg}")
|
||
self.eq_uses[field].append((top, f"{path}@{ref}", use))
|
||
elif ref.startswith("EQ0"):
|
||
self.eq_uses[field].append((top, f"{path}@{ref}", ("고정", ref)))
|
||
if depth >= 3:
|
||
return
|
||
for j, (sub, args) in enumerate(calls):
|
||
if sub not in self.logic:
|
||
continue
|
||
self.callers[sub].add(top)
|
||
ren, lt = {}, {}
|
||
for name, (kind, val) in args.items():
|
||
if kind == "lit":
|
||
lt[name] = val
|
||
elif kind == "name":
|
||
if depth == 0:
|
||
ren[name] = val
|
||
elif val in rename:
|
||
ren[name] = rename[val]
|
||
elif val in lit:
|
||
lt[name] = lit[val]
|
||
self._expand(top, sub, ren, lt, depth + 1, f"{path}>{sub}.{j}")
|
||
|
||
@staticmethod
|
||
def _pass(row: dict, filt: dict) -> bool:
|
||
"""칸 하나가 줄 거름을 통과?"""
|
||
for col, want in filt.items():
|
||
have = row.get(col)
|
||
if have is None:
|
||
continue
|
||
if isinstance(have, list):
|
||
if isinstance(want, float) and len(have) == 2:
|
||
lo, hi = have
|
||
if (lo is not None and want < lo) or (hi is not None and want > hi):
|
||
return False
|
||
continue
|
||
if isinstance(want, float):
|
||
try:
|
||
if float(have) != want:
|
||
return False
|
||
except (TypeError, ValueError):
|
||
return False
|
||
elif str(have) != str(want):
|
||
return False
|
||
return True
|
||
|
||
@staticmethod
|
||
def _trade_ok(name: str, c: dict) -> bool:
|
||
if c["종류"] == "eq":
|
||
return False
|
||
if c["종류"] != "ratio" and fam(c["단위"]) not in (None, "사람"):
|
||
return False
|
||
return same_trade(name, c["칸"].split("/")[0])
|
||
|
||
def lookup(self, k: dict) -> list[tuple[int, str]]:
|
||
"""상수 → [(칸 id, 꼴 eq | pct | kgt)] — N1 · N2 · N3 · N4 · N5 · N8(캐시)."""
|
||
key = kkey(k)
|
||
if key not in self._look:
|
||
self._look[key] = self._lookup(k)
|
||
return self._look[key]
|
||
|
||
def _lookup(self, k: dict) -> list[tuple[int, str]]:
|
||
v, d, unit = k["v"], k["d"], k["단위"]
|
||
sig = len(re.sub(r"^0+|\.", "", f"{v:.{d}f}").lstrip("0"))
|
||
tol = 0.5 * 10 ** (-d) if d >= 1 and sig >= 2 else 1e-9
|
||
if k.get("몫"):
|
||
tol = abs(v) * 1e-6
|
||
forms = [(v, tol, "eq")]
|
||
tf = fam(unit)
|
||
if tf in (None, "율"):
|
||
forms += [(v * 100, tol * 100, "pct"), (v / 100, tol / 100, "pct")]
|
||
if tf == "무게":
|
||
forms += [(v * 1000, tol * 1000, "kgt"), (v / 1000, tol / 1000, "kgt")]
|
||
out = []
|
||
for x, t, how in forms:
|
||
t = max(t, abs(x) * 1e-9)
|
||
for j in range(
|
||
bisect.bisect_left(self.vals, x - t), bisect.bisect_right(self.vals, x + t)
|
||
):
|
||
cid = self.ids[j]
|
||
c = self.cells[cid]
|
||
if c["종류"] == "cond":
|
||
continue # 조건 칸(관경 · SCH)은 옵션 값 — 상수 설명에 안 씀
|
||
if k.get("직종") and not self._trade_ok(k["직종"], c):
|
||
continue
|
||
if c["종류"] == "ratio" and how != "eq":
|
||
continue
|
||
if how == "pct" and not (tf == "율" or "%" in str(c["단위"])):
|
||
continue
|
||
if how == "kgt" and not (tf == "무게" or fam(c["단위"]) == "무게"):
|
||
continue
|
||
if (
|
||
how == "eq"
|
||
and abs(c["v"] - v) > 1e-9
|
||
and c["종류"] != "ratio"
|
||
and tol < 1e-6
|
||
and not k.get("몫")
|
||
):
|
||
continue
|
||
if not unit_ok(unit, c["단위"]) and how == "eq":
|
||
continue
|
||
out.append((cid, how))
|
||
return out
|
||
|
||
def hits_of(self, cid: int) -> list[tuple]:
|
||
"""칸 → [(로직, 부름id, 줄, 묶음)](캐시)."""
|
||
got = self._hits.get(cid)
|
||
if got is None:
|
||
got = self._hits[cid] = self._hits_of(cid)
|
||
return got
|
||
|
||
def _hits_of(self, cid: int) -> list[tuple]:
|
||
c = self.cells[cid]
|
||
out = []
|
||
if c["종류"] == "기준":
|
||
return [(u[0], f"{u[1]}|기준", 0, {}) for u in self.uses.get(c["표"], [])]
|
||
if c["종류"] == "eq":
|
||
for item, callid, (kind, val) in self.eq_uses.get(c["칸"], []):
|
||
if kind == "고정" and val != c["표"]:
|
||
continue
|
||
out.append(
|
||
(
|
||
item,
|
||
callid,
|
||
c["표"],
|
||
{val: json.dumps(c["표"])} if kind == "입력" and val else {},
|
||
)
|
||
)
|
||
return out
|
||
row = self.tables[c["표"]]["줄"][c["줄"]]
|
||
cols = c["칸"].split("/") if c["종류"] == "ratio" else [c["칸"]]
|
||
for item, callid, field, filt, bind in self.uses.get(c["표"], []):
|
||
if field is not None and field not in cols and c["종류"] != "cond":
|
||
continue
|
||
if not self._pass(row, filt):
|
||
continue
|
||
b = {}
|
||
for col, name in bind.items():
|
||
if col == "#row":
|
||
b[name] = json.dumps(c["줄"])
|
||
elif row.get(col) is not None:
|
||
b[name] = json.dumps(row[col], ensure_ascii=False)
|
||
out.append((item, callid, c["줄"], b))
|
||
return out
|
||
|
||
def explain(self, k: dict) -> list[tuple]:
|
||
"""상수 하나 → [(로직, 부름id | None, 줄, 묶음, 꼴, 닻)] ·
|
||
닻 = 표 칸 · 직종 식 수로 맞음."""
|
||
out = []
|
||
for cid, how in self.lookup(k):
|
||
if self.cells[cid]["종류"] == "eq":
|
||
continue # 기계 요소 값(상각비계수 · 잡재료율 …)은 산근 글에 거의 안 나옴
|
||
out += [(item, call, row, bd, how, True) for item, call, row, bd in self.hits_of(cid)]
|
||
x = round(k["v"], 9)
|
||
if k.get("직종"):
|
||
for item in self.trade_index.get(x, ()):
|
||
if any(
|
||
same_trade(k["직종"], n) for n, y in self.inline_trade[item] if round(y, 9) == x
|
||
):
|
||
out.append((item, None, None, {}, "식", True))
|
||
else:
|
||
forms = (
|
||
(x, round(k["v"] * 100, 9), round(k["v"] / 100, 9))
|
||
if fam(k["단위"]) in (None, "율")
|
||
else (x,)
|
||
)
|
||
for form in forms:
|
||
for item in self.inl_index.get(form, ()):
|
||
if len(self.inline[item]) <= INLINE_MAX:
|
||
out.append((item, None, None, {}, "식", False))
|
||
return out
|
||
|
||
# ── 우연 보정 ────────────────────────────────────────────────────
|
||
def df(self, k: dict) -> int:
|
||
key = kkey(k)
|
||
if key not in self._df:
|
||
self._df[key] = max(1, len({x[0] for x in self.explain(k)}))
|
||
return self._df[key]
|
||
|
||
def prior(self) -> None:
|
||
"""보정 자료 상수마다 설명할 수 있는 로직 → 로직 몫 p · 평균 df."""
|
||
keys = [
|
||
dict(zip(("v", "d", "단위", "직종", "몫"), row))
|
||
for row in json.loads(CORPUS.read_text(encoding="utf-8"))
|
||
]
|
||
cnt: dict = defaultdict(int)
|
||
for k in keys:
|
||
for it in {x[0] for x in self.explain(k)}:
|
||
cnt[it] += 1
|
||
self.prom = {it: (cnt[it] + 1) / (len(keys) + 2) for it in self.logic}
|
||
self.mean_df = sum(self.df(k) for k in keys) / max(len(keys), 1)
|
||
|
||
def q_of(self, item: str, k: dict) -> float:
|
||
"""로직 item 이 상수 k 를 우연히 설명할 확률 ≈ df_k · p_item / 평균 df."""
|
||
p = self.prom.get(item, 1 / max(len(self.prom), 1))
|
||
return min(0.95, max(1e-6, self.df(k) * p / self.mean_df))
|
||
|
||
|
||
def kkey(k: dict) -> tuple:
|
||
return (k["v"], k["d"], k["단위"], k.get("직종"), bool(k.get("몫")))
|
||
|
||
|
||
def pb_sig(qs: list[float], c: int) -> float:
|
||
"""포아송 이항 꼬리 P(X ≥ c) 의 -log10."""
|
||
if c <= 0:
|
||
return 0.0
|
||
dp = [1.0] + [0.0] * len(qs)
|
||
for q in qs:
|
||
for j in range(len(dp) - 1, 0, -1):
|
||
dp[j] = dp[j] * (1 - q) + dp[j - 1] * q
|
||
dp[0] *= 1 - q
|
||
return round(-math.log10(max(sum(dp[c:]), 1e-300)), 2)
|
||
|
||
|
||
_LOCK = threading.Lock()
|
||
_CACHE: dict[str, Index] = {}
|
||
|
||
|
||
def get() -> Index:
|
||
"""M01 지문마다 한 번 지은 역색인(프로세스 안 · 지문 바뀌면 다시)."""
|
||
key = stamp()
|
||
with _LOCK:
|
||
got = _CACHE.get(key)
|
||
if got is None:
|
||
trade.cache_clear()
|
||
_CACHE.clear()
|
||
got = _CACHE[key] = Index()
|
||
return got
|
||
|
||
|
||
def warm() -> None:
|
||
"""서버 시작 때 한 번(뒤에서) — 실패 = 다음 부름에서 다시."""
|
||
try:
|
||
get()
|
||
except Exception: # noqa: BLE001
|
||
pass
|