Files
cdsl-cad/backend/tests/test_evidence_v2_annulus_lowering.py
ganjihong e7ade4aa00 fix(evidence_v2): 将 annulus 降级为 analytic_contours 轮廓
同心圆截面改为输出外圆与内圆两条 analytic_contours 轮廓,避免旧 annulus 宏导致 CDSL 文档不符合 schema 且运行时无法执行
2026-08-27 10:55:55 +08:00

171 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""#10 Profile chain: evidence_v2 importer must lower annulus to analytic_contours.
The CDSL-only runtime and ``cdsl_schema.json`` accept only three generic
profiles (``circle``, ``polygon``, ``analytic_contours``). The legacy
``annulus`` macro is still emitted by ``evidence_v2_to_cdsl._analytic_profile``
for concentric circles, which makes the resulting document schema-invalid and
runtime-ineligible. This test suite pins the lowering contract: concentric
circles must produce an ``analytic_contours`` profile (outer + inner ring).
中文说明
--------
这个文件在测试什么(issue #10「10 Profile 链」的回归测试):
1. 背景:CDSL-only 运行时(backend/engine/cdsl_engine)与
cdsl_schema.json 只接受三种通用 profile 类型:
circle / polygon / analytic_contours。
而 evidence_v2 导入器(json_to_cdsl/evidence_v2_to_cdsl.py 的
_analytic_profile)在遇到"同心双圆"(垫圈/圆环 annulus 截面)时,
仍会输出旧宏 {type: "annulus"},导致产出的 CDSL 文档 schema 不合法、
运行时不可执行(报 profile_resolution_failed / unsupported_profile)。
2. 本测试套件把"降级契约"固定下来:同心双圆必须降级为
analytic_contours 轮廓(外圆 role=outer + 内圆 role=inner),
并保证 importer 输出能被 CDSL-only 运行时端到端重建为实体。
3. sys.path 说明:把 json_to_cdsl 与 backend/engine 加入搜索路径,
是为了让测试能直接 import 导入器内部的 _analytic_profile(灰盒测试)
以及 CDSL-only 运行时的 rebuild_cdsl(端到端测试)。
"""
from __future__ import annotations
import math
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
sys.path.insert(0, str(ROOT / "backend" / "engine"))
sys.path.insert(0, str(ROOT / "json_to_cdsl"))
try:
import build123d # noqa: F401
_HAS_BUILD123D = True
except ImportError:
_HAS_BUILD123D = False
from evidence_v2_to_cdsl import _analytic_profile # noqa: E402
def _circle_segment(center: list[float], radius_mm: float, *, construction: bool = False) -> dict:
"""One full SolidWorks sketch circle segment (start == end == center).
构造一条 SolidWorks 草图中的完整圆线段(start == end == center
即闭合圆)。radius_mm 除以 1000 转成米,与导入器内部单位保持一致。
"""
return {
"geometry": {
"segment_type": "swSketchARC",
"construction": construction,
"start": center,
"end": center,
"center": center,
"direction": 1,
"curve": {"type": "circle", "parameters": [*center, 0, 0, 0, 1, radius_mm / 1000.0]},
}
}
def _ring_cdsl(profile: dict) -> dict:
"""Assemble a minimal CDSL document whose single feature extrudes a ring profile.
组装一份最简 CDSL 文档:一张草图(ring,携带被测试的 profile+
一个拉伸特征(extrude_add_blind)。供端到端测试使用,
验证 importer 产出的 profile 能否被运行时重建为实体。
"""
return {
"schema": "cad.cdsl.llm.v1",
"schema_version": "1.1.0",
"kind": "part",
"part_id": "annulus-ring",
"meta": {"unit": "mm"},
"geometry": {"sketches": [{
"id": "ring",
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": profile,
}]},
"features": [{
"id": "ring_add", "atomic_id": "extrude_add_blind", "depends_on": [],
"params": {"distance_mm": 20}, "sketch_id": "ring",
}],
}
class EvidenceV2AnnulusLoweringTests(unittest.TestCase):
"""annulus 降级契约的回归测试套件。
三个测试用例分别覆盖:
1. test_evidence_v2_emits_analytic_contours_for_annulus
主契约:同心双圆(外 r=10mm,内 r=5mm)必须降级为 analytic_contours
且恰好携带 outer(10) + inner(5) 两条轮廓。
2. test_evidence_v2_emits_analytic_contours_for_multiple_circles
回归护栏:两个独立(圆心不同、互不包含)的圆本来就输出
analytic_contours,修复 annulus 分支时不得破坏该既有行为。
3. test_evidence_v2_annulus_output_rebuilds_to_ring_solid
端到端:importer 输出的 analytic_contours 必须能被 CDSL-only 运行时
rebuild_cdsl 重建为实体,且体积与圆环公式
π * (r外² - r内²) * 高度 一致(误差 1%)。
"""
def test_evidence_v2_emits_analytic_contours_for_annulus(self) -> None:
"""Concentric circles must lower to a generic ring (outer + inner)."""
# 主契约测试:同圆心 (0,0) 的两个圆,外径 10mm、内径 5mm
sketch = {"segments": [
_circle_segment([0, 0], 10.0),
_circle_segment([0, 0], 5.0),
]}
profile = _analytic_profile(sketch)
# 1) profile 类型必须是 analytic_contours(而不是旧宏 annulus
self.assertEqual(profile["type"], "analytic_contours")
contours = profile["contours"]
# 2) 必须恰好有两条轮廓
self.assertEqual(len(contours), 2)
# 3) 两条轮廓的角色分别是 outer(外圆)与 inner(内圆)
self.assertEqual({contour["role"] for contour in contours}, {"outer", "inner"})
outer = next(contour for contour in contours if contour["role"] == "outer")
inner = next(contour for contour in contours if contour["role"] == "inner")
# 4) 半径正确:外 10mm / 内 5mm
self.assertEqual(outer["segments"][0]["radius_mm"], 10.0)
self.assertEqual(inner["segments"][0]["radius_mm"], 5.0)
def test_evidence_v2_emits_analytic_contours_for_multiple_circles(self) -> None:
"""Regression guard: independent circles already lower to contours."""
# 回归护栏:两个圆心不同(相距 30mm)、互不包含的独立圆,
# 修复 annulus 分支前后都必须保持输出 analytic_contours(各一条 outer 轮廓)
sketch = {"segments": [
_circle_segment([0, 0], 10.0),
_circle_segment([30, 0], 5.0),
]}
profile = _analytic_profile(sketch)
self.assertEqual(profile["type"], "analytic_contours")
self.assertEqual(len(profile["contours"]), 2)
@unittest.skipUnless(_HAS_BUILD123D, "build123d is not available")
def test_evidence_v2_annulus_output_rebuilds_to_ring_solid(self) -> None:
"""Importer output must be consumed end-to-end by the CDSL-only runtime."""
# 端到端:直接取 _analytic_profile 的输出组装成 CDSL 文档,
# 交给 CDSL-only 运行时 rebuild_cdsl 重建实体,再断言体积符合圆环公式。
# 此测试验证修复后运行时不再报 profile_resolution_failed / unsupported_profile。
from cdsl_engine.runtime import rebuild_cdsl
sketch = {"segments": [
_circle_segment([0, 0], 10.0),
_circle_segment([0, 0], 5.0),
]}
profile = _analytic_profile(sketch)
cdsl = _ring_cdsl(profile)
with tempfile.TemporaryDirectory() as directory:
out_step = Path(directory) / "ring.step"
result = rebuild_cdsl(cdsl, out_step)
# 圆环体积 = π * (r外² - r内²) * 高度 = π * (100 - 25) * 20
expected = math.pi * (10.0 ** 2 - 5.0 ** 2) * 20.0
self.assertAlmostEqual(result["volume_mm3"], expected, delta=expected * 0.01)
if __name__ == "__main__":
unittest.main()