feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user