# -*- coding: utf-8 -*- """ 补齐参数组合空间: 为"规则内合法但库中缺失"的组合生成零件号, 并指向系列级占位数模。 背景: 当前型号表由真实 STEP 文件扫描而来 —— 没有数模就没有型号, 用户选完也拿不到代号。 本脚本按规则反向生成缺失组合, 使其可选、可出号; 数模用每系列一个占位件, 后续 AI 补真实数模。 用法: PYTHONUTF8=1 python gen-missing-combos.py --report # 只统计, 不写文件 PYTHONUTF8=1 python gen-missing-combos.py --write # 写入 CSV + meta 模板 + 占位清单 """ import json, io, os, csv, itertools, sys, collections CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog' REPORT = '--report' in sys.argv WRITE = '--write' in sys.argv # 只有"模板能 100% 对齐"的系列才补 (由 infer-sku-template.py 验证)。 # 其余系列 (旧码混用/编码结构特殊) 保持现状, 不生成可能不存在的零件号。 SAFE = { 'KC', 'KCB', 'KCS', 'KS', 'KSB', 'KSU', 'M', 'MS', 'MSC', 'MAL', 'MALC', 'ULA', 'ULL', 'ULM', 'ULO', 'ULP', 'ULT', 'ULX', 'ULY', 'ULZ', 'USL', 'USSL', } # 各系列的编码模板 (由 infer-sku-template.py 反推, 100% 对齐) # {参数:补零位数}; 空值渲染为空串 TEMPLATES = { 'KC': ('KC{type}{bore}-{stroke}{magnet}{mount}', {}), 'KCB': ('KCB{bore}-{stroke}{magnet}{mount}', {}), 'KCS': ('KCS{type}{bore}-{stroke}{magnet}{mount}', {}), 'KS': ('KS{type}{bore}-{stroke}{magnet}{mount}', {}), 'KSB': ('KSB{bore}-{stroke1}x{stroke2}{magnet}{mount}', {}), 'KSU': ('KSU{type}{bore}-{stroke}{magnet}{mount}', {}), 'M': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}), 'MS': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}), 'MSC': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}), 'MAL': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}), 'MALC': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}), 'ULA': ('ULA{size}{stroke:pad3}{rod}{mount}{sensor}0', {}), 'ULL': ('ULL{size}{stroke:pad3}{rod}{orient}{sensor}0', {}), 'ULM': ('ULM{size}{stroke:pad3}{rod}{orient}{sensor}0', {}), 'ULO': ('ULO{size}{stroke:pad3}{rod}{sensor}0', {}), 'ULP': ('ULP{size}{stroke:pad3}{rod}{orient}{sensor}0', {}), 'ULT': ('ULT{size}{stroke:pad3}{rod}{sensor}0', {}), 'ULX': ('ULX{size}{stroke:pad3}{rod}{sensor}0', {}), 'ULY': ('ULY{size}{stroke:pad3}{rod}{orient}{sensor}0', {}), 'ULZ': ('ULZ{size}{stroke:pad3}{rod}{orient}{sensor}0', {}), 'USL': ('USL{size}{pcmodel}{pin_dia}{locating}{prog}{sensor}0', {}), 'USSL': ('USSL{size}{pcmodel}{hook_pos}{pin_dia}{locating}{prog}{sensor}0', {}), } PLACEHOLDER_STEP = 'PLACEHOLDER.step' # 每系列共用 def S(v): return '' if v is None else str(v) def eq(a, b): return S(a).lower() == S(b).lower() def cond_match(c, prm): if not c: return True val = prm.get(c['Param']) if val is None: return False op, cv = c['Op'], c['Value'] if op == 'eq': return eq(val, cv) if op == 'ne': return not eq(val, cv) if op == 'in': return isinstance(cv, list) and any(eq(val, x) for x in cv) try: a, b = float(S(val)), float(S(cv)) except ValueError: return False return {'gt': a > b, 'ge': a >= b, 'lt': a < b, 'le': a <= b}.get(op, False) def violated(rules, prm): for r in rules or []: if cond_match(r.get('If'), prm) and not cond_match(r.get('Then'), prm): return True return False def render(tpl, prm): """模板渲染: {field} / {field:padN}; 空值渲染为空串。""" out, i = '', 0 while i < len(tpl): if tpl[i] == '{': j = tpl.index('}', i) tok = tpl[i + 1:j] name, _, pad = tok.partition(':') name = name.strip() v = S(prm.get(name, '')) if pad.startswith('pad') and v != '': try: v = v.rjust(int(pad[3:]), '0') except ValueError: return None out += v i = j + 1 else: out += tpl[i] i += 1 return out def load_meta(code): p = os.path.join(CAT, 'series', code + '.meta.json') return json.load(io.open(p, encoding='utf-8')) def link_or_copy(src, dst): """建硬链接; 不支持则回退复制。返回 True 表示已创建。 注: 必须先建链接再考虑失败回退, 绝不能先删目标再建 (删了又建失败 = 文件丢失)。""" if os.path.exists(dst): return False try: os.link(src, dst) return True except OSError: pass try: import shutil shutil.copy2(src, dst) return True except OSError: return False def main(): summary = [] plan = {} for code in sorted(SAFE): meta = load_meta(code) s = meta['series'] params = s['parameters'] csvp = os.path.join(CAT, 'csv', code + '.csv') if not os.path.exists(csvp): print(f'[{code}] 无 CSV, 跳过') continue rows = list(csv.DictReader(io.open(csvp, encoding='utf-8-sig'))) existing = {r['model_code'] for r in rows} # 值域: 数值参数取 CSV 实际出现过的值 (不用 meta.values, 避免含非法/未出现值) doms = collections.OrderedDict() for p in params: pc = p['code'] if p['type'] == 'range': doms[pc] = None # 连续域不枚举 continue vals = list(dict.fromkeys(r.get(pc, '') for r in rows)) vals = [v for v in vals if v != '' or True] doms[pc] = vals if any(v is None for v in doms.values()): print(f'[{code}] 含 range 参数, 跳过 (连续域无法枚举)') continue tpl, _ = TEMPLATES[code] codes = list(doms.keys()) combos = list(itertools.product(*[doms[k] for k in codes])) added = [] for combo in combos: prm = dict(zip(codes, combo)) if violated(s.get('rules'), prm): continue mc = render(tpl, prm) if not mc or mc in existing: continue existing.add(mc) added.append((mc, prm)) summary.append((code, len(rows), len(combos), len(added))) if added: plan[code] = (tpl, params, added, rows) # 输出 print(f'{"系列":7s} {"现有":>7s} {"笛卡尔":>9s} {"待补":>7s}') print('-' * 36) tot_new = 0 for code, n, cart, add in sorted(summary, key=lambda x: -x[3]): tot_new += add print(f'{code:7s} {n:7d} {cart:9,d} {add:7,d}') print(f'\n合计待补 {tot_new:,} 个型号') if not WRITE: print('\n(--report 模式, 未写文件)') return # ---------- 写入 ---------- step_dir = os.path.join(CAT, 'step') placeholder = os.path.join(step_dir, PLACEHOLDER_STEP) manifest = [] # 待 AI 补真实数模的清单 n_csv = n_ph = 0 for code, (tpl, params, added, rows) in plan.items(): csvp = os.path.join(CAT, 'csv', code + '.csv') header = list(rows[0].keys()) dims = [h for h in header if h not in ('model_code', 'step_file')] # 新增行: step_file 统一指向 PLACEHOLDER.step —— # 构建器按 v.step 的路径复制 (同路径只存一份), 3157 个型号共用一个占位件, # 既不占磁盘也不撑大数据包。AI 补真实数模后把该行改成 "<型号>.step" 即可。 with io.open(csvp, 'a', encoding='utf-8-sig', newline='') as fp: w = csv.writer(fp) for mc, prm in added: w.writerow([mc] + [S(prm.get(d, '')) for d in dims] + [PLACEHOLDER_STEP]) manifest.append(f'{code},{mc},{",".join(d + "=" + S(prm.get(d, "")) for d in dims)}') n_csv += 1 print(f'[{code}] CSV +{len(added)}') # 待补清单 (给 AI 补数模用) mf = os.path.join(CAT, '_pending_models.csv') with io.open(mf, 'w', encoding='utf-8-sig', newline='') as fp: fp.write('series,model_code,params\n') fp.write('\n'.join(manifest) + '\n') print(f'\n写入完成: CSV 新增 {n_csv} 行, 占位 STEP {n_ph} 个') print(f'待补数模清单: {mf} ({len(manifest)} 个型号)') if __name__ == '__main__': main()