95cae203f4
Phase 7 of the decoupling refactor (behavior-preserving move): - legacy/llm_compiler.py, legacy/llm_engine.py: frozen build_pack path (unused by run_cdsl_only), moved with git mv for history - legacy/exact_rebuild.py: _run_parameterized / _run_exact / _apply_geometric_compensations moved out of rebuild.py, including the part-specific compensation table - rebuild.py keeps run_rebuild / run_cdsl_only / compare_with_gold and imports the moved functions - llm_compiler.py / llm_engine.py become compatibility shims Frozen zone: new capability belongs in executors/ + schema contracts. The project-specific compensation no longer sits in the main pipeline.
254 lines
9.9 KiB
Python
254 lines
9.9 KiB
Python
"""SolidWorks-exact fallback rebuilds and project-specific compensations.
|
|
|
|
``_run_parameterized`` and ``_run_exact`` replay SolidWorks rebuilds through
|
|
generated build123d scripts (``compiler_context``). They are the frozen
|
|
exact/parameterized legs of ``rebuild.run_rebuild``; the production path is
|
|
``run_cdsl_only`` in the session runtime.
|
|
|
|
``_apply_geometric_compensations`` is a project-specific workaround for a
|
|
feature the SolidWorks export misses. It intentionally lives beside the
|
|
legacy paths so a clean checkout of this engine in another project can drop
|
|
it without touching the generic code.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..sketch_solver import resolve_all_sketches
|
|
from ..translator import generate_build123d_code
|
|
|
|
|
|
def _run_parameterized(cdsl: dict[str, Any], out_step: Path, gold_step: Path | None = None) -> dict[str, Any]:
|
|
"""参数化路径: CDSL语义结构 + compiler_context精确数据 → translator生成代码 → 执行
|
|
|
|
采用双层IR架构:
|
|
Learning IR (CDSL) 提供参数化形状、特征结构
|
|
Execution IR (compiler_context) 提供精确坐标
|
|
translator 提供经过充分测试的代码生成
|
|
"""
|
|
|
|
import subprocess
|
|
import tempfile, os
|
|
|
|
t0 = time.time()
|
|
|
|
# 1. 获取 compiler_context (Execution IR: 精确坐标)
|
|
compiler_context = cdsl.get("compiler_context") or {}
|
|
if not compiler_context:
|
|
# 从外部文件加载
|
|
ctx_file = out_step.parent / "{}.compiler_context.json".format(cdsl.get("part_id", ""))
|
|
if ctx_file.exists():
|
|
import json as _json
|
|
with open(ctx_file, "r", encoding="utf-8") as _f:
|
|
compiler_context = _json.load(_f)
|
|
if not compiler_context:
|
|
raise RuntimeError("CDSL缺少 compiler_context,无法重建")
|
|
|
|
part_name = str(cdsl.get("part_id") or out_step.stem)
|
|
context = dict(compiler_context)
|
|
context.setdefault("metadata", {})["part_name"] = part_name
|
|
|
|
# 2. 将 compiler_context 的精确实体注入 CDSL 草图 (供 sketch_solver 使用)
|
|
# 015133: CDSL (Learning IR) 不含坐标,坐标来自 Execution IR
|
|
ctx_sketches_map = {s["id"]: s for s in context.get("sketches", [])}
|
|
cdsl_sketches = cdsl.get("geometry", {}).get("sketches", [])
|
|
for sk in cdsl_sketches:
|
|
ctx_sk = ctx_sketches_map.get(sk["id"])
|
|
if ctx_sk:
|
|
# 注入 entities/contour 供 polygon/complex_arc_shape 生成器使用
|
|
if not sk.get("entities"):
|
|
sk["entities"] = ctx_sk.get("entities", [])
|
|
if not sk.get("contour_edges_mm"):
|
|
sk["contour_edges_mm"] = ctx_sk.get("contour_edges_mm", [])
|
|
|
|
# 3. 解析 CDSL 的参数化草图 (现在有 entities 可用)
|
|
cdsl_resolved = resolve_all_sketches(cdsl)
|
|
|
|
# 4. 将 CDSL 解析后的 profile/profile_from 注入 compiler_context
|
|
# translator 使用 compiler_context 的精确 entities + CDSL 的 profile 分类
|
|
cdsl_resolved_map = {s["id"]: s for s in cdsl_resolved.get("geometry", {}).get("sketches", [])}
|
|
ctx_sketches = list(context.get("sketches", []))
|
|
updated_count = 0
|
|
for i, ctx_sk in enumerate(ctx_sketches):
|
|
sk_id = ctx_sk.get("id", "")
|
|
cdsl_sk = cdsl_resolved_map.get(sk_id)
|
|
if cdsl_sk and cdsl_sk.get("profile"):
|
|
ctx_sketches[i] = {**ctx_sk, "profile": cdsl_sk["profile"]}
|
|
updated_count += 1
|
|
if cdsl_sk and cdsl_sk.get("profile_from"):
|
|
ctx_sketches[i] = {**ctx_sk, "profile_from": cdsl_sk["profile_from"]}
|
|
updated_count += 1
|
|
context["sketches"] = ctx_sketches
|
|
|
|
# 4. 使用 compiler_context 的原始 operations(保持 translator 兼容性)
|
|
|
|
# 5. 读取 gold volume
|
|
gold_volume_mm3 = None
|
|
if gold_step and gold_step.exists():
|
|
try:
|
|
from build123d import import_step
|
|
gold_solid = import_step(str(gold_step))
|
|
gold_volume_mm3 = float(gold_solid.volume)
|
|
except Exception:
|
|
pass
|
|
|
|
# 6. 用 translator 生成并执行
|
|
code = generate_build123d_code(context, gold_volume_mm3=gold_volume_mm3)
|
|
|
|
# 6b. 应用几何补偿 (SW导出缺失的特征)
|
|
part_id = str(cdsl.get("part_id") or "")
|
|
code = _apply_geometric_compensations(code, part_id)
|
|
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as tf:
|
|
tf.write(code)
|
|
script_path = tf.name
|
|
|
|
try:
|
|
r = subprocess.run(
|
|
["python", script_path],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=120,
|
|
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
|
)
|
|
if r.returncode != 0:
|
|
raise RuntimeError(f"Build script failed:\n{r.stderr}")
|
|
finally:
|
|
try:
|
|
os.unlink(script_path)
|
|
except Exception:
|
|
pass
|
|
|
|
# 7. 读取重建结果
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
built_step = Path(part_name + ".step")
|
|
if not built_step.exists():
|
|
built_step = Path.cwd() / (part_name + ".step")
|
|
if built_step.exists():
|
|
import shutil
|
|
shutil.copy2(str(built_step), str(out_step))
|
|
built_step.unlink()
|
|
else:
|
|
raise RuntimeError(f"No STEP output found: {part_name}.step")
|
|
|
|
from build123d import import_step
|
|
rebuilt = import_step(str(out_step))
|
|
bbox = rebuilt.bounding_box()
|
|
bbox_mm = {
|
|
"min": [bbox.min.X, bbox.min.Y, bbox.min.Z],
|
|
"max": [bbox.max.X, bbox.max.Y, bbox.max.Z],
|
|
}
|
|
|
|
elapsed = time.time() - t0
|
|
return {
|
|
"out_step": str(out_step),
|
|
"volume_mm3": float(rebuilt.volume),
|
|
"bbox_mm": bbox_mm,
|
|
"log": [f"param: CDSL-informed translator rebuild, {updated_count} sketches updated from CDSL"],
|
|
"engine": "parameterized",
|
|
"elapsed_s": round(elapsed, 1),
|
|
}
|
|
|
|
|
|
def _run_exact(cdsl: dict[str, Any], out_step: Path, gold_step: Path | None = None) -> dict[str, Any]:
|
|
"""精确路径: generate_build123d_code (后备)"""
|
|
|
|
import subprocess
|
|
|
|
compiler_context = cdsl.get("compiler_context") or {}
|
|
part_name = str(cdsl.get("part_id") or out_step.stem)
|
|
context = dict(compiler_context)
|
|
context.setdefault("metadata", {})["part_name"] = part_name
|
|
|
|
# Read gold volume if available, for chamfer/candidate scoring
|
|
gold_volume_mm3 = None
|
|
if gold_step and gold_step.exists():
|
|
try:
|
|
from build123d import import_step
|
|
gold_solid = import_step(str(gold_step))
|
|
gold_volume_mm3 = float(gold_solid.volume)
|
|
except Exception:
|
|
pass
|
|
|
|
out_step.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Apply geometric compensations FIRST (may return full replacement code)
|
|
part_id = str(cdsl.get("part_id") or "")
|
|
compensation_code = _apply_geometric_compensations("", part_id)
|
|
|
|
if compensation_code and "build123d" in compensation_code and "__main__" in compensation_code:
|
|
# 完整替换代码 (跳过generate_build123d_code)
|
|
code = compensation_code
|
|
else:
|
|
code = generate_build123d_code(context, gold_volume_mm3=gold_volume_mm3)
|
|
code = _apply_geometric_compensations(code, part_id)
|
|
|
|
t0 = time.time()
|
|
script_path = out_step.parent / "_tmp" / f"build_{part_name}_{int(time.time())}.py"
|
|
script_path.parent.mkdir(exist_ok=True)
|
|
script_path.write_text(code, encoding="utf-8")
|
|
|
|
completed = subprocess.run(
|
|
[sys.executable, str(script_path)],
|
|
cwd=out_step.parent,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600,
|
|
)
|
|
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(
|
|
f"Exact compiler FAILED (rc={completed.returncode})\n"
|
|
f"STDOUT:\n{completed.stdout[-2000:]}\n"
|
|
f"STDERR:\n{completed.stderr[-3000:]}"
|
|
)
|
|
# Print any warnings from safe_subtract
|
|
for line in completed.stdout.split('\n'):
|
|
if 'SUBTRACT' in line or 'UNION' in line:
|
|
print(f" {line.strip()}")
|
|
|
|
from build123d import import_step
|
|
# 生成的 build 脚本将 STEP 写到 CWD 下的 "{part_name}.step"
|
|
# 移到 out_step 位置以供后续对比
|
|
actual_step = out_step.parent / f"{part_name}.step"
|
|
if actual_step.exists():
|
|
import shutil
|
|
shutil.copy2(str(actual_step), str(out_step))
|
|
solid = import_step(str(out_step))
|
|
bb = solid.bounding_box()
|
|
elapsed = time.time() - t0
|
|
|
|
return {
|
|
"out_step": str(out_step),
|
|
"volume_mm3": float(solid.volume),
|
|
"bbox_mm": {"min": [bb.min.X, bb.min.Y, bb.min.Z],
|
|
"max": [bb.max.X, bb.max.Y, bb.max.Z]},
|
|
"engine": "exact",
|
|
"elapsed_s": round(elapsed, 1),
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Geometric compensations(项目特例;拷贝到其他项目时可删)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def _apply_geometric_compensations(code: str, part_id: str) -> str:
|
|
"""为SW导出中缺失的特征添加几何补偿切操作"""
|
|
if part_id == "113246":
|
|
if "export_step(result, " not in code:
|
|
return code
|
|
comp = (
|
|
" # === COMPENSATION: 侧槽 (SW缺失特征) ===\n"
|
|
" with BuildSketch(Plane(origin=(-70.0, -13.0, 10.0), "
|
|
"x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0))) as comp_sk:\n"
|
|
" Rectangle(10.0, 3.0, align=(Align.MIN, Align.MIN))\n"
|
|
" comp_cutter = extrude(comp_sk.sketch, amount=10.0)\n"
|
|
" result = safe_subtract(result, comp_cutter)\n"
|
|
)
|
|
code = code.replace("export_step(result, ", comp + " export_step(result, ")
|
|
return code
|