feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
@@ -10,6 +10,7 @@ from __future__ import annotations
import argparse
import json
import math
import os
import re
import shutil
@@ -34,6 +35,11 @@ DESIGNIR_SCHEMA = (
/ "contracts"
/ "designir-3.0.schema.json"
)
DESIGNIR_SCRIPTS = WORKSPACE_ROOT / "designir-pipeline" / "scripts"
if str(DESIGNIR_SCRIPTS) not in sys.path:
sys.path.insert(0, str(DESIGNIR_SCRIPTS))
from designir_codec import read_designir, write_designir
EXECUTABLE_EDIT_STATES = {
"executable_enlarge_cut_binding",
@@ -80,10 +86,14 @@ def _safe_stem(source: Path) -> str:
def _write_json(path: Path, payload: dict[str, Any]) -> None:
if path.name.endswith(".designir.json"):
write_designir(path, payload)
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
errors="replace",
)
@@ -117,6 +127,66 @@ def _run_json(
) from exc
def _run_simplecad_brep_compare(
runtime: Path,
source: Path,
rebuilt: Path,
report_path: Path,
*,
timeout_seconds: int,
) -> dict[str, Any]:
"""Run the installed SimpleCADAPI comparator in the acceptance zone.
A topology mismatch is a valid diagnostic result, not a worker failure.
The teacher path is provided only to this acceptance subprocess.
"""
command = [
str(runtime),
"-m",
"simplecadapi.inverse_engineer.brep.cli",
"compare",
str(source),
str(rebuilt),
"--output",
str(report_path),
]
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
return {
"status": "unavailable",
"reason": "timeout",
"error": str(exc),
}
if report_path.is_file():
try:
payload = json.loads(report_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return {
"status": "unavailable",
"reason": "invalid_report",
"error": str(exc),
}
payload["status"] = (
"passed" if payload.get("hard_gate_passed") else "mismatch"
)
payload["engine"] = "simplecadapi.inverse_engineer.brep"
return payload
detail = completed.stderr.strip() or completed.stdout.strip()
return {
"status": "unavailable",
"reason": "worker_failed",
"return_code": completed.returncode,
"error": detail[-2000:],
}
def _validate_designir_contract(payload: dict[str, Any]) -> dict[str, Any]:
required = {
"schema_version",
@@ -261,7 +331,7 @@ def _persist_edit_results(
designir_path: Path,
attempts: list[dict[str, Any]],
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
payload = json.loads(designir_path.read_text(encoding="utf-8"))
payload = read_designir(designir_path)
parameters = payload.get("semantic_layer", {}).get("parameters", {})
accepted_parameters: list[dict[str, Any]] = []
attempted_names = set()
@@ -312,6 +382,7 @@ def reconstruct_uploaded_step(
request: str,
maximum_parameters: int = 8,
timeout_seconds: int = 180,
semantic_backend: str = "build123d",
) -> dict[str, Any]:
source = source.expanduser().resolve()
task_dir = task_dir.expanduser().resolve()
@@ -338,6 +409,7 @@ def reconstruct_uploaded_step(
rebuilt_path = task_dir / f"{stem}.reconstructed.step"
validation_dir = task_dir / "validation"
geometry_report_path = validation_dir / "geometry.json"
brep_report_path = validation_dir / "simplecad-brep.json"
edit_report_path = validation_dir / "edits.json"
parameters_path = task_dir / "parameters.json"
task_manifest_path = task_dir / "cad-task.json"
@@ -360,7 +432,7 @@ def reconstruct_uploaded_step(
runtime = _runtime_python()
common = [str(runtime), str(SURFACEIR_PIPELINE)]
available_experience_ids = _surfaceir_experience_ids()
boundary_strategy = (
preferred_boundary_strategy = (
"exact_3d_pcurve"
if (
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
@@ -380,25 +452,9 @@ def reconstruct_uploaded_step(
timeout_seconds=timeout_seconds,
stage="DesignIR 3.0 extraction",
)
payload = json.loads(designir_path.read_text(encoding="utf-8"))
payload = read_designir(designir_path)
contract_checks = _validate_designir_contract(payload)
# The rebuild worker receives no teacher path. It can regenerate the model
# from the DesignIR artifact alone.
_run_json(
common
+ [
"rebuild",
str(designir_path),
"--output",
str(rebuilt_path),
"--boundary-strategy",
boundary_strategy,
],
timeout_seconds=timeout_seconds,
stage="source-independent SurfaceIR rebuild",
)
manifest_path = task_dir / "manifest.json"
_write_json(
manifest_path,
@@ -415,35 +471,162 @@ def reconstruct_uploaded_step(
},
)
validation_dir.mkdir(parents=True, exist_ok=True)
geometry_work_dir = validation_dir / ".geometry-work"
geometry_result = _run_json(
common
+ [
"validate-folder",
str(source.parent),
"--designir-dir",
str(task_dir),
"--rebuilt-dir",
str(geometry_work_dir),
"--limit",
"1",
"--boolean",
"--boundary-strategy",
boundary_strategy,
],
timeout_seconds=timeout_seconds,
stage="Acceptance-Agent geometry validation",
strategy_root = validation_dir / ".strategy-work"
strategies = [preferred_boundary_strategy]
if (
"failure_repair.semantic_edit.boundary_strategy_fallback"
in available_experience_ids
):
strategies.extend(
strategy
for strategy in (
"exact_3d",
"first_pcurve",
"analytic_uv_rect",
"uv_rect_all",
)
if strategy not in strategies
)
strategy_attempts: list[dict[str, Any]] = []
selected: tuple[
tuple[float, ...],
str,
dict[str, Any],
dict[str, Any],
Path,
] | None = None
for strategy in strategies:
attempt_dir = strategy_root / strategy
try:
candidate_result = _run_json(
common
+ [
"validate-folder",
str(source.parent),
"--designir-dir",
str(task_dir),
"--rebuilt-dir",
str(attempt_dir),
"--limit",
"1",
"--boolean",
"--boundary-strategy",
strategy,
],
timeout_seconds=timeout_seconds,
stage=f"Acceptance-Agent geometry validation ({strategy})",
)
report = json.loads(
(attempt_dir / "validation-report.json").read_text(
encoding="utf-8"
)
)
row = (report.get("rows") or [{}])[0]
candidate_steps = sorted(attempt_dir.glob("*.step"))
if not candidate_steps:
raise ReconstructionError(
f"Boundary strategy {strategy} produced no rebuilt STEP"
)
def finite_metric(name: str) -> float:
value = row.get(name)
if isinstance(value, (int, float)) and math.isfinite(value):
return float(value)
return math.inf
score = (
0.0
if row.get("geometry_pass")
else 1.0
if row.get("usable_reconstruction")
else 2.0,
0.0 if row.get("topology_match") else 1.0,
finite_metric("relative_volume_error"),
finite_metric("relative_surface_area_error"),
finite_metric("center_of_mass_error"),
finite_metric("bounds_error"),
finite_metric("bidirectional_sample_distance"),
)
strategy_attempts.append(
{
"strategy": strategy,
"status": "evaluated",
"score": list(score),
"geometry_pass": bool(row.get("geometry_pass")),
"usable_reconstruction": bool(
row.get("usable_reconstruction")
),
"topology_match": bool(row.get("topology_match")),
"relative_volume_error": row.get("relative_volume_error"),
"relative_surface_area_error": row.get(
"relative_surface_area_error"
),
"center_of_mass_error": row.get("center_of_mass_error"),
"bounds_error": row.get("bounds_error"),
"bidirectional_sample_distance": row.get(
"bidirectional_sample_distance"
),
}
)
candidate = (
score,
strategy,
report,
candidate_result,
candidate_steps[0],
)
if selected is None or candidate[0] < selected[0]:
selected = candidate
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
strategy_attempts.append(
{
"strategy": strategy,
"status": "worker_failed",
"error": str(exc),
}
)
strategy_report_path = validation_dir / "strategies.json"
if selected is None:
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": None,
"attempts": strategy_attempts,
},
)
raise ReconstructionError("Every SurfaceIR boundary strategy failed")
_, boundary_strategy, geometry_report, geometry_result, selected_step = selected
shutil.copy2(selected_step, rebuilt_path)
_write_json(
strategy_report_path,
{
"schema_version": "1.0",
"selected_strategy": boundary_strategy,
"attempts": strategy_attempts,
},
)
raw_geometry_report = geometry_work_dir / "validation-report.json"
geometry_report = json.loads(raw_geometry_report.read_text(encoding="utf-8"))
_write_json(geometry_report_path, geometry_report)
geometry_pass = geometry_report.get("geometry_pass_count") == 1
if not geometry_pass:
usable_reconstruction = (
geometry_report.get("usable_reconstruction_count") == 1
)
if not geometry_pass and not usable_reconstruction:
raise ReconstructionError(
"Independent reconstruction did not satisfy the geometry contract; "
f"see {geometry_report_path}"
)
shutil.rmtree(geometry_work_dir)
brep_comparison = _run_simplecad_brep_compare(
runtime,
source,
rebuilt_path,
brep_report_path,
timeout_seconds=timeout_seconds,
)
shutil.rmtree(strategy_root)
attempts: list[dict[str, Any]] = []
if payload["document_status"] == "geometry_present":
@@ -502,7 +685,9 @@ def reconstruct_uploaded_step(
)
payload["reconstruction_strategy"] = {
"boundary_strategy": boundary_strategy,
"selection_status": "geometry_validated",
"selection_status": (
"geometry_validated" if geometry_pass else "unresolved"
),
}
_write_json(designir_path, payload)
applied_experience_ids = _applied_experience_ids(
@@ -525,6 +710,41 @@ def reconstruct_uploaded_step(
"policy": "Only parameters that passed an isolated perturbation test are exposed.",
}
_write_json(parameters_path, parameter_catalog)
brep_hard_gate_passed = brep_comparison.get("hard_gate_passed") is True
brep_available = brep_comparison.get("status") in {"passed", "mismatch"}
quality_tier = (
"brep_exact"
if geometry_pass and brep_hard_gate_passed
else "geometry_summary_accepted_brep_mismatch"
if geometry_pass and brep_available
else "geometry_accepted_brep_unavailable"
if geometry_pass
else "review_required"
)
published_artifacts = [
{"path": rebuilt_path.name, "role": "primary_step"},
{"path": designir_path.name, "role": "editable_designir"},
{"path": parameters_path.name, "role": "validated_parameters"},
{
"path": str(geometry_report_path.relative_to(task_dir)),
"role": "geometry_acceptance",
},
{
"path": str(edit_report_path.relative_to(task_dir)),
"role": "parameter_acceptance",
},
{
"path": str(strategy_report_path.relative_to(task_dir)),
"role": "boundary_strategy_acceptance",
},
]
if brep_report_path.is_file():
published_artifacts.append(
{
"path": str(brep_report_path.relative_to(task_dir)),
"role": "strict_brep_acceptance",
}
)
task_manifest = {
"schema_version": "1.0",
@@ -533,8 +753,12 @@ def reconstruct_uploaded_step(
"route": {
"router": "cad-router",
"runtime": "DesignIR 3.0 / SurfaceIR",
"selected_backend": "build123d",
"selected_backend": semantic_backend,
"execution_adapter": "surfaceir_occt",
"validation_adapters": [
"surfaceir_geometry_acceptance",
"simplecadapi_brep_compare",
],
"source_of_truth": designir_path.name,
},
"source": {
@@ -542,19 +766,7 @@ def reconstruct_uploaded_step(
"role": "teacher_and_acceptance_truth_only",
"embedded_in_output": False,
},
"artifacts": [
{"path": rebuilt_path.name, "role": "primary_step"},
{"path": designir_path.name, "role": "editable_designir"},
{"path": parameters_path.name, "role": "validated_parameters"},
{
"path": str(geometry_report_path.relative_to(task_dir)),
"role": "geometry_acceptance",
},
{
"path": str(edit_report_path.relative_to(task_dir)),
"role": "parameter_acceptance",
},
],
"artifacts": published_artifacts,
"experience": {
"library": (
str(EXPERIENCE_LIBRARY)
@@ -566,7 +778,11 @@ def reconstruct_uploaded_step(
"methods_applied": applied_experience_ids,
},
"validation": {
"geometry_accepted": True,
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison_status": brep_comparison.get("status"),
"brep_hard_gate_passed": brep_hard_gate_passed,
"quality_tier": quality_tier,
"source_independent_rebuild": True,
"anti_cheat_pass": True,
"validated_parameter_count": len(accepted_parameters),
@@ -592,7 +808,15 @@ def reconstruct_uploaded_step(
return {
"schema_version": "1.0",
"status": (
"accepted"
(
"accepted"
if geometry_pass
and (
brep_hard_gate_passed
or brep_comparison.get("status") == "unavailable"
)
else "review_required"
)
if payload["document_status"] == "geometry_present"
else "accepted_empty_source"
),
@@ -603,7 +827,13 @@ def reconstruct_uploaded_step(
"task_manifest": str(task_manifest_path),
"geometry_report": str(geometry_report_path),
"edit_report": str(edit_report_path),
"geometry_accepted": True,
"geometry_accepted": geometry_pass,
"geometry_usable": usable_reconstruction,
"brep_comparison": brep_comparison,
"brep_report": (
str(brep_report_path) if brep_report_path.is_file() else None
),
"quality_tier": quality_tier,
"document_status": payload["document_status"],
"validated_parameter_count": len(accepted_parameters),
"validated_parameter_names": [
@@ -620,6 +850,10 @@ def reconstruct_uploaded_step(
"stages": {
"extraction": extraction,
"geometry_validation": geometry_result,
"brep_validation": {
"engine": "SimpleCADAPI 2.0.2",
"status": brep_comparison.get("status"),
},
},
}
@@ -631,6 +865,12 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--request", default="重建上传的 STEP")
parser.add_argument("--maximum-parameters", type=int, default=8)
parser.add_argument("--timeout-seconds", type=int, default=180)
parser.add_argument(
"--semantic-backend",
choices=("build123d", "simplecadapi"),
default="build123d",
help="Preferred editable semantic backend after SurfaceIR reconstruction.",
)
return parser
@@ -643,6 +883,7 @@ def main(argv: list[str] | None = None) -> int:
request=args.request,
maximum_parameters=args.maximum_parameters,
timeout_seconds=args.timeout_seconds,
semantic_backend=args.semantic_backend,
)
except (ReconstructionError, subprocess.TimeoutExpired) as exc:
print(