feat: unify CAD workflows on DesignIR 3.0

This commit is contained in:
Jerry
2026-07-28 14:41:26 +08:00
parent 6e33bc0e11
commit b5738e9109
36 changed files with 9690 additions and 1305 deletions
@@ -31,6 +31,41 @@ class DesignIRPipelineTests(unittest.TestCase):
self.assertEqual(1, result["facts"]["solid_count"])
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
def test_legacy_model_migrates_to_semantic_designir_3(self) -> None:
migrated = MODULE.migrate_designir_2_to_3(self.payload())
self.assertEqual("3.0", migrated["schema_version"])
self.assertEqual(
"fully_semantic_parametric", migrated["reconstruction_mode"]
)
self.assertIn(
"outer_radius", migrated["edit_interface"]["semantic_parameters"]
)
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary) / "flange.step"
result = MODULE.compile_designir(migrated, output)
self.assertEqual(1, result["facts"]["solid_count"])
def test_generated_step_materializes_hybrid_surface_snapshot(self) -> None:
migrated = MODULE.migrate_designir_2_to_3(self.payload())
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary) / "flange.step"
MODULE.compile_designir(migrated, output)
hybrid = MODULE.materialize_hybrid_designir(migrated, output)
self.assertEqual(
"hybrid_semantic_surface_parametric",
hybrid["reconstruction_mode"],
)
self.assertTrue(hybrid["surface_layer"]["solids"])
self.assertTrue(
hybrid["compiled_surface_provenance"][
"semantic_layer_authoritative"
]
)
self.assertTrue(MODULE.anti_cheat(hybrid)["valid"])
rebuilt = Path(temporary) / "hybrid-rebuilt.step"
MODULE.compile_designir(hybrid, rebuilt)
self.assertTrue(rebuilt.is_file())
def test_teacher_dependency_is_rejected(self) -> None:
payload = self.payload()
payload["source_step"] = "teacher.step"
@@ -48,6 +83,51 @@ class DesignIRPipelineTests(unittest.TestCase):
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
self.assertTrue(Path(result["model_json"]).is_file())
def test_arbitrary_axis_geometric_center_preserves_pose(self) -> None:
payload = self.payload()
payload["family"] = "axis_aligned_cylinder"
payload["parameters"] = {
"outer_radius": {"value": 10, "unit": "mm", "editable": True},
"overall_length": {"value": 400, "unit": "mm", "editable": True},
}
payload["expressions"] = {}
payload["features"] = [
{
"id": "main_body",
"operation": "extrude_circle",
"radius": {"parameter": "outer_radius"},
"height": {"parameter": "overall_length"},
"axis": [0, -1, 0],
"center": [7, 11, 13],
"anchor": "geometric_center",
}
]
payload["edit_interface"] = {
"editable_parameters": ["outer_radius", "overall_length"],
"preserved_interfaces": ["part_center", "primary_axis"],
}
payload["validation_contract"]["perturbations"] = [
{
"parameter": "outer_radius",
"scale": 1.1,
"expected_change": "radius",
"preserve": ["overall_length", "part_center"],
},
{
"parameter": "overall_length",
"scale": 1.1,
"expected_change": "length",
"preserve": ["outer_radius", "part_center"],
},
]
shape = MODULE.build_shape(payload)
bounds = shape.bounding_box()
self.assertAlmostEqual(-189, bounds.min.Y, places=6)
self.assertAlmostEqual(211, bounds.max.Y, places=6)
self.assertAlmostEqual(7, shape.center(MODULE.CenterOf.MASS).X, places=6)
self.assertAlmostEqual(11, shape.center(MODULE.CenterOf.MASS).Y, places=6)
self.assertAlmostEqual(13, shape.center(MODULE.CenterOf.MASS).Z, places=6)
def test_embedded_mesh_is_rejected(self) -> None:
payload = self.payload()
payload["features"][0]["mesh"] = {"vertices": [[0, 0, 0]]}
@@ -97,6 +177,10 @@ class DesignIRPipelineTests(unittest.TestCase):
json.dumps(
{
"model_id": f"case-{index}",
"teacher_sha256": f"teacher-{index}",
"applied_experience_ids": [
"reconstruction.flange.primary"
],
"next_state": "replay_validated",
}
),
@@ -107,6 +191,10 @@ class DesignIRPipelineTests(unittest.TestCase):
reports, ROOT / "config" / "promotion-policy.json"
)
self.assertTrue(result["eligible"])
self.assertEqual(
["reconstruction.flange.primary"],
result["validated_experience_ids"],
)
self.assertEqual("promoted", result["next_state"])
@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
@@ -106,6 +107,22 @@ def make_case(index: int) -> dict:
}
def positive_validation(*experience_ids: str) -> dict:
return {
"validation_kind": "heldout_experience_ab",
"method_results": [
{
"experience_id": experience_id,
"independent_case_count": 3,
"edit_contract_pass_rate": 1.0,
"baseline_score_delta": 0.5,
"passed": True,
}
for experience_id in experience_ids
],
}
class CadExperienceTest(unittest.TestCase):
def test_default_data_paths_use_shallow_local_layout(self):
pipeline_root = Path(__file__).parents[1].resolve()
@@ -179,6 +196,47 @@ class CadExperienceTest(unittest.TestCase):
"canonical_reconstruction_plan_not_recovered_history",
reconstruction["interpretation"],
)
parameters = STEP_MODULE._semantic_parameter_candidates(
surfaces,
[0.0, 0.0, 0.0],
[40.0, 20.0, 40.0],
features,
summary,
)
names = {item["name"] for item in parameters}
self.assertIn("repeated_feature_count", names)
self.assertIn("repeated_feature_diameter", names)
self.assertTrue(
all(
item["epistemic_status"] == "inferred_from_final_brep"
for item in parameters
)
)
def test_non_solid_teacher_is_rejected_from_induction(self):
payload = make_case(1)
payload["geometry_evidence"] = {
"valid": False,
"reconstruction_eligible": False,
"solid_count": 0,
"volume": 0.0,
"rejection_reasons": ["no_closed_solid", "zero_volume"],
}
normalized = MODULE.normalize_case(payload)
self.assertFalse(normalized["reconstruction_eligible"])
self.assertIn("no_closed_solid", normalized["rejection_reasons"])
def test_sha256_identity_deduplicates_renamed_sources(self):
digest = "0" * 64
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for index in range(2):
payload = make_case(index)
payload["provenance"]["source_sha256"] = digest
(root / f"renamed-{index}.json").write_text(json.dumps(payload))
cases, stats = MODULE.load_cases(root)
self.assertEqual(1, len(cases))
self.assertEqual(1, stats["duplicate_case_count"])
def test_conditional_feature_context_does_not_dilute_valid_method(self):
proposal = self.reviewed_proposal()
@@ -202,6 +260,7 @@ class CadExperienceTest(unittest.TestCase):
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
positive_validation(proposal["id"]),
)
self.assertEqual(1, len(library["experiences"]))
@@ -249,6 +308,29 @@ class CadExperienceTest(unittest.TestCase):
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
self.assertIn("distribution.sleeve_od_to_base_od", ids)
def test_audit_allows_aggregate_validation_counts_only(self):
library = {
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"experiences": [
{
"id": "edit_strategy.test",
"support": 20,
"validation": {
"heldout_pair_count": 20,
"treatment_pass_count": 20,
"regressed_pair_count": 0,
"pass_rate_delta": 0.25,
},
}
],
"candidate_experiences": [],
}
self.assertEqual([], MODULE.audit_library(library))
library["experiences"][0]["validation"]["radius"] = 3.0
errors = MODULE.audit_library(library)
self.assertTrue(any("forbidden instance key" in error for error in errors))
def test_family_support_is_not_diluted_by_unrelated_families(self):
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
unrelated = []
@@ -299,7 +381,7 @@ class CadExperienceTest(unittest.TestCase):
)
self.assertEqual([], candidate_library["experiences"])
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
self.assertTrue(candidate_library["policy"]["router_consumable"])
self.assertFalse(candidate_library["policy"]["router_consumable"])
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
promoted_library = MODULE.build_reviewed_library(
@@ -308,10 +390,71 @@ class CadExperienceTest(unittest.TestCase):
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
positive_validation(proposal["id"]),
)
self.assertEqual(1, len(promoted_library["experiences"]))
self.assertEqual([], promoted_library["candidate_experiences"])
def test_parameter_naming_requires_positive_heldout_validation(self):
cases = []
for index in range(20):
payload = make_case(index)
payload["design_ir"]["semantic_parameter_candidates"] = [
{
"name": "bolt_circle_diameter",
"unit": "mm",
"confidence": 0.9,
"semantic_aliases": ["hole_spacing"],
"affects_feature_roles": ["mounting_pattern"],
"epistemic_status": "inferred_from_final_brep",
"edit_contract": {
"expected_change": "pattern_radius_changes"
},
}
]
cases.append(MODULE.normalize_case(payload))
raw = {
"id": "parameter.flange.bolt_circle_diameter",
"kind": "parameter_naming",
"scope": ["flanged_hub_adapter"],
"evidence_query": {
"required_features": ["base_flange"],
"required_relations": [],
"required_parameters": ["bolt_circle_diameter"],
},
"guidance": "Expose the circular mounting pattern diameter as an editable semantic parameter.",
"semantic_rationale": "The named role preserves edit intent across dimensional variants.",
"canonical_parameter": "bolt_circle_diameter",
"semantic_aliases": ["hole_spacing"],
}
proposal = MODULE.normalize_proposals(
{
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [raw],
}
)[0]
unvalidated = MODULE.build_reviewed_library(
cases,
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual([], unvalidated["experiences"])
self.assertEqual(
"awaiting_positive_heldout_ab",
unvalidated["candidate_experiences"][0]["validation_state"],
)
validated = MODULE.build_reviewed_library(
cases,
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
positive_validation(raw["id"]),
)
self.assertEqual("parameter_naming", validated["experiences"][0]["kind"])
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
proposal = self.reviewed_proposal()
proposal["guidance"] = "Always use a diameter of 91 millimeters."
@@ -375,6 +518,7 @@ class CadExperienceTest(unittest.TestCase):
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
positive_validation(proposal["id"]),
)
self.assertEqual([], MODULE.audit_library(library))
self.assertEqual("reconstruction_grammar", library["experiences"][0]["kind"])
@@ -0,0 +1,248 @@
from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from build123d import Plane, Rectangle
ROOT = Path(__file__).resolve().parents[1]
def load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(module)
return module
SURFACEIR = load_module(
"surfaceir_pipeline", ROOT / "scripts" / "surfaceir_pipeline.py"
)
DESIGNIR = load_module(
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
)
class SurfaceIRPipelineTests(unittest.TestCase):
def test_independent_json_roundtrip_preserves_exact_geometry(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
teacher = directory / "teacher.step"
portable_json = directory / "model.designir.json"
rebuilt = directory / "rebuilt.step"
expected = DESIGNIR.Box(10, 20, 30)
DESIGNIR.export_step(expected, teacher)
surfaceir = SURFACEIR.extract_surfaceir(teacher)
SURFACEIR.write_json(portable_json, surfaceir)
teacher.unlink()
serialized = portable_json.read_text(encoding="utf-8").lower()
self.assertNotIn("teacher.step", serialized)
self.assertNotIn("source_path", serialized)
self.assertNotIn("import_step", serialized)
self.assertNotIn('"mesh"', serialized)
rebuilt_shape = SURFACEIR.build_surfaceir(surfaceir)
SURFACEIR.export_step_shape(rebuilt_shape, rebuilt)
self.assertTrue(rebuilt.is_file())
actual = DESIGNIR.import_step(rebuilt)
self.assertEqual(len(expected.solids()), len(actual.solids()))
self.assertEqual(len(expected.faces()), len(actual.faces()))
self.assertEqual(len(expected.edges()), len(actual.edges()))
self.assertLess(
abs(expected.volume - actual.volume) / expected.volume, 1e-10
)
self.assertLess(
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
)
def test_empty_step_document_is_represented_honestly(self) -> None:
payload = {
"schema_version": "3.0",
"designir_kind": "independent_parametric_cad",
"document_status": "empty_geometry",
"surface_layer": {
"vertices": [],
"solids": [],
"free_shells": [],
},
}
rebuilt = SURFACEIR.build_surfaceir(payload)
self.assertEqual(SURFACEIR._facts_from_shape(rebuilt)["face_count"], 0)
def test_open_shell_roundtrip_preserves_surface_geometry(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
teacher = directory / "surface.step"
rebuilt_path = directory / "surface-rebuilt.step"
face = (Plane.XY * Rectangle(10, 20)).face()
builder = SURFACEIR.BRep_Builder()
shell = SURFACEIR.TopoDS_Shell()
builder.MakeShell(shell)
builder.Add(shell, face.wrapped)
SURFACEIR.export_step_shape(shell, teacher)
payload = SURFACEIR.extract_surfaceir(teacher)
self.assertEqual(payload["document_status"], "geometry_present")
self.assertEqual(len(payload["surface_layer"]["solids"]), 0)
self.assertEqual(len(payload["surface_layer"]["free_shells"]), 1)
rebuilt = SURFACEIR.build_surfaceir(
payload, "exact_3d_pcurve"
)
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
_, teacher_facts = SURFACEIR._shape_facts(teacher)
_, rebuilt_facts = SURFACEIR._shape_facts(rebuilt_path)
self.assertEqual(teacher_facts["face_count"], 1)
self.assertEqual(
teacher_facts["face_count"], rebuilt_facts["face_count"]
)
self.assertAlmostEqual(
teacher_facts["surface_area"],
rebuilt_facts["surface_area"],
places=8,
)
def test_overall_scale_is_executable_and_independently_validated(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
teacher = directory / "teacher.step"
designir_path = directory / "box.designir.json"
expected = DESIGNIR.Box(10, 20, 30)
DESIGNIR.export_step(expected, teacher)
payload = SURFACEIR.extract_surfaceir(teacher)
self.assertIn(
"overall_scale",
payload["edit_interface"]["semantic_parameters"],
)
edited = SURFACEIR.apply_semantic_parameter(
payload, "overall_scale", 1.1
)
self.assertEqual(
edited["edit_operations"],
[
{
"operation": "uniform_scale",
"factor": 1.1,
"pivot": [0.0, 0.0, 0.0],
}
],
)
self.assertIsNotNone(SURFACEIR.build_surfaceir(edited))
SURFACEIR.write_json(designir_path, payload)
acceptance = SURFACEIR.validate_semantic_edit(
teacher,
designir_path,
"overall_scale",
1.1,
directory / "acceptance",
)
self.assertTrue(acceptance["accepted"])
self.assertTrue(
acceptance["checks"]["target_parameter"]["pass"]
)
def test_primary_axis_dimensions_are_executable_and_validated(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
teacher = directory / "shaft.step"
designir_path = directory / "shaft.designir.json"
DESIGNIR.export_step(DESIGNIR.Cylinder(10, 40), teacher)
payload = SURFACEIR.extract_surfaceir(teacher)
SURFACEIR.write_json(designir_path, payload)
for parameter, value in (
("outer_diameter", 22.0),
("body_length", 44.0),
):
acceptance = SURFACEIR.validate_semantic_edit(
teacher,
designir_path,
parameter,
value,
directory / parameter,
)
self.assertTrue(acceptance["accepted"])
self.assertTrue(
acceptance["checks"]["target_parameter"]["pass"]
)
def test_canonical_semantics_name_bore_and_polar_hole_pattern(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
teacher = Path(temporary) / "flange.step"
expected = DESIGNIR.Cylinder(20, 5) - DESIGNIR.Cylinder(5, 5)
for x, y in ((10, 0), (0, 10), (-10, 0), (0, -10)):
expected -= DESIGNIR.Pos(x, y, 0) * DESIGNIR.Cylinder(1, 5)
DESIGNIR.export_step(expected, teacher)
payload = SURFACEIR.extract_surfaceir(teacher)
semantic = payload["semantic_layer"]
parameters = semantic["parameters"]
self.assertEqual(parameters["hole_count"]["value"], 4)
self.assertAlmostEqual(parameters["hole_diameter"]["value"], 2)
self.assertAlmostEqual(
parameters["bolt_circle_diameter"]["value"], 20
)
self.assertAlmostEqual(parameters["bore_diameter"]["value"], 10)
self.assertAlmostEqual(parameters["outer_diameter"]["value"], 40)
self.assertAlmostEqual(parameters["plate_thickness"]["value"], 5)
self.assertTrue(
any(
feature["operation"] == "polar_hole_pattern"
for feature in semantic["features"]
)
)
self.assertFalse(
semantic["inference_policy"]["claims_original_feature_history"]
)
edited = SURFACEIR.apply_semantic_parameter(
payload, "hole_diameter", 3
)
self.assertEqual(len(edited["edit_operations"]), 4)
self.assertEqual(
edited["active_edit"]["acceptance_status"], "pending"
)
with self.assertRaisesRegex(ValueError, "increases only"):
SURFACEIR.apply_semantic_parameter(
payload, "hole_diameter", 1.5
)
edited_step = Path(temporary) / "flange-edited.step"
edited_shape = SURFACEIR.build_surfaceir(edited)
SURFACEIR.export_step_shape(edited_shape, edited_step)
edited_payload = SURFACEIR.extract_surfaceir(edited_step)
edited_parameters = edited_payload["semantic_layer"]["parameters"]
self.assertAlmostEqual(
edited_parameters["hole_diameter"]["value"], 3
)
self.assertAlmostEqual(
edited_parameters["bore_diameter"]["value"], 10
)
self.assertEqual(edited_parameters["hole_count"]["value"], 4)
actual = DESIGNIR.import_step(edited_step)
self.assertTrue(actual.is_valid)
self.assertLess(actual.volume, expected.volume)
original_ir = Path(temporary) / "flange.designir.json"
SURFACEIR.write_json(original_ir, payload)
acceptance = SURFACEIR.validate_semantic_edit(
teacher,
original_ir,
"hole_diameter",
2.5,
Path(temporary) / "acceptance",
)
self.assertTrue(acceptance["accepted"])
self.assertEqual(acceptance["state"], "accepted")
if __name__ == "__main__":
unittest.main()