Files
cdsl-cad/backend/engine/cdsl_engine/validate_output3.py
T
2026-08-19 19:34:30 +08:00

134 lines
5.2 KiB
Python

"""用 SolidWorks evidence 的 document_truth 验收 output3 CDSL 重建结果。"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from typing import Any
from build123d import CenterOf, import_step
FORBIDDEN_CDSL_KEYS = {
"compiler_context",
"entities",
"contour_edges_mm",
"contour_regions_mm",
"_raw_entities",
"vertices",
}
def _find_forbidden(value: Any, path: str = "$") -> list[str]:
found: list[str] = []
if isinstance(value, dict):
for key, item in value.items():
child = f"{path}.{key}"
if key in FORBIDDEN_CDSL_KEYS:
found.append(child)
found.extend(_find_forbidden(item, child))
elif isinstance(value, list):
for index, item in enumerate(value):
found.extend(_find_forbidden(item, f"{path}[{index}]"))
return found
def validate(cdsl_path: Path, evidence_dir: Path) -> dict[str, Any]:
cdsl = json.loads(cdsl_path.read_text(encoding="utf-8"))
part_id = str(cdsl["part_id"])
source_name = str(cdsl["meta"]["source"])
evidence = json.loads((evidence_dir / source_name).read_text(encoding="utf-8"))
truth = evidence["document_truth"]
mass = truth["mass_properties"]
step_path = cdsl_path.with_name(f"{part_id}_rebuilt.step")
report_path = cdsl_path.with_name(f"{part_id}.rebuild_report.json")
report = json.loads(report_path.read_text(encoding="utf-8"))
solid = import_step(str(step_path))
truth_volume = float(mass["volume"]) * 1e9
truth_area = float(mass["surface_area"]) * 1e6
truth_com = [float(value) * 1000.0 for value in mass["center_of_mass"]]
rebuilt_com_vector = solid.center(CenterOf.MASS)
rebuilt_com = [rebuilt_com_vector.X, rebuilt_com_vector.Y, rebuilt_com_vector.Z]
bbox = solid.bounding_box()
rebuilt_bbox = [bbox.min.X, bbox.min.Y, bbox.min.Z, bbox.max.X, bbox.max.Y, bbox.max.Z]
truth_bbox = [float(value) * 1000.0 for value in truth["geometry"]["bounding_box"]]
volume_error_pct = abs(float(solid.volume) - truth_volume) / truth_volume * 100.0
area_error_pct = abs(float(solid.area) - truth_area) / truth_area * 100.0
com_delta_mm = math.dist(rebuilt_com, truth_com)
bbox_max_delta_mm = max(abs(a - b) for a, b in zip(rebuilt_bbox, truth_bbox))
forbidden = _find_forbidden(cdsl)
engine = report.get("engine_result", {}).get("engine")
checks = {
"engine_cdsl_only": engine == "cdsl_only",
"no_forbidden_geometry_payload": not forbidden,
"volume_error_le_1pct": volume_error_pct <= 1.0,
"surface_area_error_le_1pct": area_error_pct <= 1.0,
"center_of_mass_delta_le_0_1mm": com_delta_mm <= 0.1,
"bbox_delta_le_0_01mm": bbox_max_delta_mm <= 0.01,
}
return {
"part_id": part_id,
"cdsl_path": str(cdsl_path),
"rebuilt_step": str(step_path),
"source_evidence": str(evidence_dir / source_name),
"cdsl_lines": len(cdsl_path.read_text(encoding="utf-8").splitlines()),
"feature_count": len(cdsl.get("features") or []),
"sketch_count": len((cdsl.get("geometry") or {}).get("sketches") or []),
"engine": engine,
"forbidden_paths": forbidden,
"metrics": {
"truth_volume_mm3": truth_volume,
"rebuilt_volume_mm3": float(solid.volume),
"volume_error_pct": volume_error_pct,
"truth_surface_area_mm2": truth_area,
"rebuilt_surface_area_mm2": float(solid.area),
"surface_area_error_pct": area_error_pct,
"center_of_mass_delta_mm": com_delta_mm,
"bbox_max_delta_mm": bbox_max_delta_mm,
},
"checks": checks,
"passed": all(checks.values()),
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--evidence", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
results = [
validate(path, args.evidence)
for path in sorted(args.output.glob("cylinder_*/*.cdsl.json"))
]
summary = {
"schema": "cad.cdsl.output3.validation.v1",
"count": len(results),
"passed_count": sum(item["passed"] for item in results),
"failed_count": sum(not item["passed"] for item in results),
"max_volume_error_pct": max(item["metrics"]["volume_error_pct"] for item in results),
"max_surface_area_error_pct": max(item["metrics"]["surface_area_error_pct"] for item in results),
"max_center_of_mass_delta_mm": max(item["metrics"]["center_of_mass_delta_mm"] for item in results),
"max_bbox_delta_mm": max(item["metrics"]["bbox_max_delta_mm"] for item in results),
"results": results,
}
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(
f"validated={summary['count']} passed={summary['passed_count']} "
f"failed={summary['failed_count']} report={args.report}"
)
if summary["failed_count"]:
raise SystemExit(1)
if __name__ == "__main__":
main()