"""Session-based CDSL execution entry points. The runtime was split into focused modules (behavior-preserving move): - ``registry``: atomic executor registry, ``atomic_executor`` decorator, and the ``execute_node`` dispatcher. - ``executors/``: one module per executor family; importing the package performs the registration and verifies registry completeness. - ``session``: ``ExecutionSession`` and the ``GeometryAdapter`` protocol. - ``runtime_base``: shared error types and the ``ExtentVector`` value. - ``extents``: end-condition planning. - ``pattern_transform``: translate/mirror/rotate parameter algebra for replay. This module keeps the ``analyze_cdsl`` / ``rebuild_cdsl`` entry points and re-exports the historical ``cdsl_engine.runtime`` names, including the private helpers referenced by the test suite, so every existing import keeps working. """ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass, field from pathlib import Path from typing import Any from . import executors # noqa: F401 (importing performs executor registration) from .capabilities import CapabilityAnalyzer, pattern_transform_blocker, sketch_ids_required_by_contract # Historical private name still imported by the test suite. from .executors.primitives import _execute_box # noqa: F401 from .executors.common import _selector_edges # noqa: F401 from .extents import ( _extent_reference, _extent_vectors, _extent_vectors_from_normal, _normal_from_sketch, _side_extent_vectors, _targeted_extent_vector, ) from .pattern_transform import ( _box_circular_is_exact, _coordinate_axis_direction, _mirrored_node, _mirrored_sketch, _normal_is_coordinate_axis, _owner_plane_frame, _pattern_operation_node, _reflect_point, _rotated_node, _rotated_point, _rotated_sketch, _rotated_vector, _transformed_loft_profiles, _translated_node, _translated_sketch, ) from .registry import ( ALL_ATOMIC_IDS, EXECUTORS, AtomicExecutor, ExecutorFunction, atomic_executor, execute_node, ) from .runtime_base import ExtentVector, FeatureExecutionError, RuntimeExecutionError from .session import ExecutionSession, GeometryAdapter from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches from .specs import ( AxisSpec, BendSpec, GearSpec, HoleSpec, PlaneSpec, RackSpec, ThreadSpec, Vector3, pattern_instance_member_id, transform_copy_member_id, vector_add, vector_cross, vector_dot, vector_scale, vector_subtract, vector_unit, ) from .topology import ( CapabilityResult, FeaturePlanNode, FeatureResult, RuntimeDiagnostic, SelectorResolution, TopologyDelta, TopologyDeltaRelation, TopologyRecord, TopologyRegistry, ) # Historical private imports used by integration tests and local diagnostics. _execute_node = execute_node __all__ = [ "ALL_ATOMIC_IDS", "EXECUTORS", "ExecutionSession", "ExecutorFunction", "ExtentVector", "FeatureExecutionError", "FeaturePlanNode", "FeatureResult", "GeometryAdapter", "RuntimeDiagnostic", "RuntimeExecutionError", "IncrementalCdslExecution", "analyze_cdsl", "prepare_cdsl_execution", "finalize_cdsl_execution", "execute_node", "rebuild_cdsl", ] def analyze_cdsl(cdsl: dict[str, Any]): """Resolve profiles and return the current runtime capability analysis.""" sketch_errors: dict[str, str] = {} resolved = resolve_required_sketches( deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors, ) analyzer = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS) return analyzer.analyze(resolved, sketch_errors=sketch_errors) def _preflight_error(analysis: Any) -> ValueError | None: if analysis.runtime_eligible: return None first = next((result for result in analysis.feature_results if not result.executable), None) if first is None: return ValueError(analysis.document_blockers[0].code) if any(blocker.code == "unknown_atomic" for blocker in first.blockers): return ValueError(f"unsupported atomic_id: {first.atomic_id}") detail = "; ".join(blocker.code for blocker in first.blockers) return ValueError(f"Feature {first.feature_id} is not runtime eligible: {detail}") def _execution_diagnostic( error: Exception, node: FeaturePlanNode, session: ExecutionSession, ) -> RuntimeDiagnostic: failed_resolution = next( (item for item in reversed(session.selector_resolutions) if item["status"] != "resolved"), None, ) if isinstance(error, FeatureExecutionError): return RuntimeDiagnostic(error.code, str(error), feature_id=node.feature_id, detail=error.detail) if failed_resolution and failed_resolution.get("diagnostic"): diagnostic = failed_resolution["diagnostic"] return RuntimeDiagnostic( diagnostic["code"], diagnostic["message"], feature_id=node.feature_id, detail=diagnostic.get("detail") or {}, ) return RuntimeDiagnostic("execution_failed", str(error), feature_id=node.feature_id) @dataclass class IncrementalCdslExecution: """One prepared CDSL replay with feature-at-a-time execution. CADFS selector binding consumes this object before each feature executes. It therefore observes the exact session topology snapshots generated by the one real kernel replay rather than rebuilding a growing prefix for every selector. The generic runtime also uses it through ``rebuild_cdsl``. """ resolved_cdsl: dict[str, Any] analysis: Any session: ExecutionSession diagnostics: list[RuntimeDiagnostic] = field(default_factory=list) next_index: int = 0 def execute_next(self, *, strict: bool = True) -> FeatureResult | None: if self.next_index >= len(self.analysis.plan): raise ValueError("CDSL execution plan is already complete") node = self.analysis.plan[self.next_index] preflight = self.analysis.feature_results[self.next_index] self.next_index += 1 if not preflight.executable: self.diagnostics.extend(preflight.blockers) if strict: detail = "; ".join(blocker.code for blocker in preflight.blockers) raise ValueError(f"Feature {node.feature_id} is not runtime eligible: {detail}") return None try: return execute_node(node, self.session) except Exception as error: diagnostic = _execution_diagnostic(error, node, self.session) self.diagnostics.append(diagnostic) if strict: raise RuntimeExecutionError(diagnostic, list(self.session.selector_resolutions)) from error return None def execute_all(self, *, strict: bool = True) -> None: while self.next_index < len(self.analysis.plan): self.execute_next(strict=strict) def prepare_cdsl_execution(cdsl: dict[str, Any]) -> IncrementalCdslExecution: """Resolve CDSL once and return a reusable sequential execution session.""" sketch_errors: dict[str, str] = {} resolved = resolve_required_sketches( deepcopy(cdsl), sketch_ids_required_by_contract(cdsl), errors=sketch_errors, ) analysis = CapabilityAnalyzer(atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS).analyze( resolved, sketch_errors=sketch_errors, ) session = ExecutionSession( sketches={str(sketch.get("id")): sketch for sketch in (resolved.get("geometry") or {}).get("sketches") or []}, nodes={node.feature_id: node for node in analysis.plan}, ) return IncrementalCdslExecution(resolved, analysis, session) def finalize_cdsl_execution(execution: IncrementalCdslExecution, out_step: Path) -> dict[str, Any]: """Export the current executable checkpoint from an incremental replay.""" session = execution.session output = session.body surface_geometry: dict[str, Any] | None = None if output is None: if not session.surface_members: raise ValueError("CDSL execution produced no body") # 纯曲面文档没有 active solid,但依然是可执行的 CAD 结果。只有在 # 没有实体时才将 surface members 作为 STEP 输出,混合模型继续只导出 # 实体,避免曲面意外改变既有实体比较和下游消费语义。 output = session.adapter.combine_surfaces(*session.surface_members.values()) surface_geometry = session.adapter.surface_geometry(output) out_step.parent.mkdir(parents=True, exist_ok=True) session.adapter.export(output, str(out_step)) geometry = session.adapter.body_geometry(session.body) if session.body is not None else surface_geometry if geometry is None: raise ValueError("CDSL execution produced no exportable geometry") bbox = geometry["bbox_mm"] return { "engine": "cdsl_session_runtime", "out_step": str(out_step), "volume_mm3": float(geometry.get("volume_mm3") or 0.0), "bbox_mm": {"min": bbox[:3], "max": bbox[3:]}, # #7 multi-body:重建结果里的独立实体数(Compound 成员数), # 与 batch 验证的 document_truth.geometry.solid_body_count 对齐。 "solid_count": len(session.adapter.body_solids(session.body)) if session.body is not None else 0, "surface_count": len(session.surface_members), "surface_face_count": int(surface_geometry["face_count"]) if surface_geometry is not None else 0, "surface_area_mm2": float(surface_geometry["area_mm2"]) if surface_geometry is not None else 0.0, "feature_results": [result.as_dict() for result in session.results.values()], "runtime_diagnostics": [diagnostic.as_dict() for diagnostic in execution.diagnostics], "topology_records": [record.public_dict() for record in session.topology.records()], "topology_deltas": list(session.topology.topology_deltas()), "selector_resolution": session.selector_resolutions, } def rebuild_cdsl(cdsl: dict[str, Any], out_step: Path, *, strict: bool = True) -> dict[str, Any]: """Rebuild CDSL through session-scoped atomic executors only.""" execution = prepare_cdsl_execution(cdsl) if strict: error = _preflight_error(execution.analysis) if error is not None: raise error execution.execute_all(strict=strict) return finalize_cdsl_execution(execution, out_step)