refactor: add DesignIR distillation pipeline
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "designir-pipeline",
|
||||
"version": "0.1.0+codex.20260723115752",
|
||||
"description": "Reconstruct teacher STEP files as executable DesignIR 2.0, validate them independently, and promote only replay-validated generalized experience.",
|
||||
"author": {
|
||||
"name": "Local developer"
|
||||
},
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "DesignIR Pipeline",
|
||||
"shortDescription": "Rebuild STEP as independent executable DesignIR.",
|
||||
"longDescription": "DesignIR Pipeline separates teacher ingestion, isolated reconstruction, independent geometry and editability acceptance, failure clustering, and deterministic experience promotion.",
|
||||
"developerName": "Local developer",
|
||||
"category": "Productivity",
|
||||
"capabilities": [
|
||||
"Protected STEP teacher ingestion",
|
||||
"Executable DesignIR 2.0",
|
||||
"Teacher-free isolated reconstruction",
|
||||
"Independent geometry acceptance",
|
||||
"Parameter perturbation acceptance",
|
||||
"Replay-validated experience promotion",
|
||||
"Instance-data leakage audits"
|
||||
],
|
||||
"defaultPrompt": "Extract private teacher evidence, author executable DesignIR without teacher STEP access, rebuild in isolation, validate independently, and promote only repeated replay-validated experience."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# DesignIR Pipeline
|
||||
|
||||
This directory owns the STEP reconstruction and distillation boundary:
|
||||
|
||||
```text
|
||||
teacher STEP
|
||||
-> deterministic private evidence extraction
|
||||
-> reconstruction agent authors DesignIR 2.0
|
||||
-> isolated compiler rebuilds STEP without teacher access
|
||||
-> acceptance agent compares teacher and rebuilt STEP
|
||||
-> deterministic publisher promotes repeated validated experience
|
||||
```
|
||||
|
||||
## Security boundary
|
||||
|
||||
| Component | Teacher STEP | Private evidence | DesignIR | Rebuilt STEP |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Ingestion service | read | write | no | no |
|
||||
| Reconstruction agent | denied | read | write | request compilation |
|
||||
| Isolated compiler | denied | no | read | write |
|
||||
| Acceptance agent | read | read | read | read |
|
||||
| Publisher | denied | aggregate only | schema only | reports only |
|
||||
|
||||
The reconstruction environment must remain functional after the teacher STEP is
|
||||
removed. `scripts/designir_pipeline.py anti-cheat` rejects source STEP
|
||||
references, imported B-Reps, embedded meshes, topology references, and generator
|
||||
source that calls STEP import APIs.
|
||||
|
||||
## Workspace
|
||||
|
||||
Runtime data is role-separated under `workspace/`:
|
||||
|
||||
For the exact input, process, acceptance, and output locations, see
|
||||
[`workspace/README.md`](workspace/README.md).
|
||||
|
||||
```text
|
||||
workspace/
|
||||
teacher/inbox/ uploaded STEP/STP files
|
||||
teacher/evidence/ private deterministic evidence
|
||||
reconstruction/inbox/ evidence packets for Agent A
|
||||
reconstruction/designir/ authored DesignIR 2.0
|
||||
reconstruction/output/ independently rebuilt STEP
|
||||
acceptance/inbox/ manifests visible to Agent B
|
||||
acceptance/reports/ geometry and editability reports
|
||||
promotion/candidates/ untrusted generalized methods
|
||||
promotion/replay_validated/
|
||||
promotion/promoted/ deterministic published experience
|
||||
quarantine/ unsupported vocabulary and failed cases
|
||||
```
|
||||
|
||||
Only `.gitkeep` files are versioned in runtime directories. Teacher geometry,
|
||||
private evidence, generated parts, and reports stay local.
|
||||
|
||||
## DesignIR 2.0
|
||||
|
||||
The executable contract is documented by
|
||||
`contracts/designir-2.0.schema.json`. It describes coordinate systems, datums,
|
||||
parameters, expressions, sketches, constraints, features, patterns,
|
||||
attachments, construction stages, edit interfaces, and validation contracts.
|
||||
It never contains teacher geometry.
|
||||
|
||||
The initial compiler deliberately supports a small, honest vocabulary through
|
||||
both Build123d and SimpleCADAPI adapters:
|
||||
|
||||
- `extrude_circle`
|
||||
- `extrude_rectangle`
|
||||
- `add_cylinder`
|
||||
- `through_hole`
|
||||
- `polar_hole_pattern`
|
||||
|
||||
Unsupported geometry is quarantined with explicit missing capabilities instead
|
||||
of embedding the source B-Rep.
|
||||
|
||||
The SimpleCADAPI adapter also emits its replayable
|
||||
`*.simplecad.model.json` operation graph beside the STEP. DesignIR remains the
|
||||
backend-neutral source of truth.
|
||||
|
||||
## Agents
|
||||
|
||||
- `agents/reconstruction-agent.md`: Agent A authors DesignIR from private
|
||||
evidence and promoted methods, then requests an isolated rebuild.
|
||||
- `agents/acceptance-agent.md`: Agent B independently checks geometry and
|
||||
parameter perturbations against the teacher.
|
||||
|
||||
Neither agent publishes experience. `config/promotion-policy.json` is evaluated
|
||||
by deterministic code after several independent cases pass.
|
||||
|
||||
## Commands
|
||||
|
||||
Use the CAD Python environment:
|
||||
|
||||
```bash
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py validate part.designir.json
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py compile \
|
||||
part.designir.json --output rebuilt.step
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py isolated-rebuild \
|
||||
part.designir.json --output rebuilt.step --backend simplecadapi
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py accept \
|
||||
--teacher teacher.step --designir part.designir.json \
|
||||
--rebuilt rebuilt.step --report acceptance.json
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# Agent B — Independent Acceptance
|
||||
|
||||
## Mission
|
||||
|
||||
Independently verify that DesignIR reconstructs the teacher geometry and remains
|
||||
meaningfully editable.
|
||||
|
||||
## Allowed inputs
|
||||
|
||||
- Teacher STEP from the protected teacher vault.
|
||||
- Candidate DesignIR.
|
||||
- Reconstructed STEP.
|
||||
- Deterministic compiler version.
|
||||
- Modification test contract.
|
||||
|
||||
Do not read Agent A chain-of-thought or informal rationale.
|
||||
|
||||
## Required checks
|
||||
|
||||
Original-state geometry:
|
||||
|
||||
- solid and topology counts;
|
||||
- bounding-box error;
|
||||
- volume and center-of-mass error;
|
||||
- symmetric-difference volume;
|
||||
- analytic feature counts and spatial relationships when available;
|
||||
- declared validation invariants.
|
||||
|
||||
Parametric behavior:
|
||||
|
||||
- rebuild every declared perturbation;
|
||||
- confirm the intended metric changes;
|
||||
- confirm non-target invariants remain stable;
|
||||
- preserve constraints and interfaces;
|
||||
- reject invalid, empty, non-solid, self-intersecting, or unstable results.
|
||||
|
||||
## Required output
|
||||
|
||||
Write one machine-readable acceptance report with separate geometry,
|
||||
feature-semantic, parameter-editability, and stability sections. Never collapse
|
||||
the result into an unexplained similarity score.
|
||||
|
||||
The agent may recommend pass, fail, or quarantine. Only the deterministic
|
||||
publisher changes promotion state.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: designir-acceptance-agent
|
||||
role: accept
|
||||
teacher_step_access: read
|
||||
private_evidence_access: read
|
||||
designir_access: read
|
||||
reconstruction_output_access: read
|
||||
acceptance_report_access: write
|
||||
publish_access: denied
|
||||
instructions: acceptance-agent.md
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Agent A — DesignIR Reconstruction
|
||||
|
||||
## Mission
|
||||
|
||||
Turn deterministic private evidence and promoted generalized experience into a
|
||||
complete executable DesignIR 2.0 model. Produce design logic, not copied
|
||||
geometry.
|
||||
|
||||
## Allowed inputs
|
||||
|
||||
- One private evidence packet produced by the ingestion service.
|
||||
- The user modification request.
|
||||
- Promoted generalized experience.
|
||||
- The DesignIR schema and supported operation vocabulary.
|
||||
|
||||
## Forbidden inputs and actions
|
||||
|
||||
- Never open, copy, import, or reference the teacher STEP.
|
||||
- Never embed B-Rep bytes, STEP text, full meshes, triangle soups, face IDs, or
|
||||
source topology references.
|
||||
- Never call `import_step`, `read_step`, `STEPControl_Reader`, or equivalent.
|
||||
- Never represent the teacher as a base feature.
|
||||
- Never infer unsupported geometry silently.
|
||||
|
||||
## Required output
|
||||
|
||||
Write one `*.designir.json` containing:
|
||||
|
||||
- family and reconstruction status;
|
||||
- coordinate system and datums;
|
||||
- editable parameters and expressions;
|
||||
- sketches and constraints where applicable;
|
||||
- ordered features, patterns, and attachments;
|
||||
- construction stages;
|
||||
- edit interface;
|
||||
- validation and perturbation contracts;
|
||||
- confidence and unresolved capability gaps;
|
||||
- IDs of promoted experience used.
|
||||
|
||||
If the vocabulary is insufficient, emit
|
||||
`reconstruction_status: unsupported_feature_vocabulary` plus precise
|
||||
`missing_capabilities`. Do not approximate secretly.
|
||||
|
||||
## Completion gate
|
||||
|
||||
Request `isolated-rebuild`. Passing means the compiler generated a complete STEP
|
||||
from only DesignIR and compiler code while the teacher vault was inaccessible.
|
||||
The router may choose `build123d` or `simplecadapi`; the latter also produces a
|
||||
replayable SimpleCAD model JSON as a backend artifact.
|
||||
@@ -0,0 +1,10 @@
|
||||
name: designir-reconstruction-agent
|
||||
role: reconstruct
|
||||
teacher_step_access: denied
|
||||
private_evidence_access: read
|
||||
promoted_experience_access: read
|
||||
designir_access: write
|
||||
reconstruction_output_access: request_only
|
||||
acceptance_report_access: denied
|
||||
publish_access: denied
|
||||
instructions: reconstruction-agent.md
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Evaluate blind baseline/experience STEP reconstructions against held-out STEP."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import importlib.util
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from build123d import import_step
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
EXTRACTOR = (
|
||||
HERE.parents[1]
|
||||
/ "skills/cad-experience-builder/scripts/step_to_case.py"
|
||||
)
|
||||
|
||||
|
||||
def load_extractor():
|
||||
spec = importlib.util.spec_from_file_location("ab50_step_to_case", EXTRACTOR)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
EXTRACT = load_extractor()
|
||||
|
||||
|
||||
def rel_error(actual: float, expected: float) -> float:
|
||||
return abs(actual - expected) / max(abs(expected), 1e-9)
|
||||
|
||||
|
||||
def feature_names(payload: dict[str, Any]) -> set[str]:
|
||||
result: set[str] = set()
|
||||
for item in payload.get("design_ir", {}).get("features", []):
|
||||
if isinstance(item, str):
|
||||
result.add(item)
|
||||
elif isinstance(item, dict):
|
||||
name = item.get("kind") or item.get("type") or item.get("name")
|
||||
if name:
|
||||
result.add(str(name))
|
||||
return result
|
||||
|
||||
|
||||
def f1_score(left: set[str], right: set[str]) -> float:
|
||||
if not left and not right:
|
||||
return 1.0
|
||||
if not left or not right:
|
||||
return 0.0
|
||||
overlap = len(left & right)
|
||||
precision = overlap / len(right)
|
||||
recall = overlap / len(left)
|
||||
return 2 * precision * recall / max(precision + recall, 1e-9)
|
||||
|
||||
|
||||
def mesh(shape, tolerance: float = 0.35) -> tuple[np.ndarray, np.ndarray]:
|
||||
vertices, triangles = shape.tessellate(tolerance)
|
||||
points = np.asarray([[v.X, v.Y, v.Z] for v in vertices], dtype=float)
|
||||
faces = np.asarray(triangles, dtype=int)
|
||||
return points, faces
|
||||
|
||||
|
||||
def sample_surface(
|
||||
vertices: np.ndarray, faces: np.ndarray, count: int, seed: int
|
||||
) -> np.ndarray:
|
||||
tri = vertices[faces]
|
||||
cross = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0])
|
||||
areas = np.linalg.norm(cross, axis=1) / 2.0
|
||||
total = float(areas.sum())
|
||||
if total <= 1e-12:
|
||||
return vertices.copy()
|
||||
rng = np.random.default_rng(seed)
|
||||
chosen = rng.choice(len(faces), size=count, p=areas / total)
|
||||
selected = tri[chosen]
|
||||
u = rng.random(count)
|
||||
v = rng.random(count)
|
||||
reflected = u + v > 1.0
|
||||
u[reflected] = 1.0 - u[reflected]
|
||||
v[reflected] = 1.0 - v[reflected]
|
||||
return (
|
||||
selected[:, 0]
|
||||
+ u[:, None] * (selected[:, 1] - selected[:, 0])
|
||||
+ v[:, None] * (selected[:, 2] - selected[:, 0])
|
||||
)
|
||||
|
||||
|
||||
def cube_transforms() -> list[np.ndarray]:
|
||||
transforms = []
|
||||
for permutation in itertools.permutations(range(3)):
|
||||
base = np.eye(3)[:, permutation]
|
||||
for signs in itertools.product((-1.0, 1.0), repeat=3):
|
||||
transforms.append(base @ np.diag(signs))
|
||||
return transforms
|
||||
|
||||
|
||||
TRANSFORMS = cube_transforms()
|
||||
|
||||
|
||||
def surface_distance(
|
||||
reference, generated, seed: int, samples: int
|
||||
) -> tuple[float, float]:
|
||||
ref_v, ref_f = mesh(reference)
|
||||
gen_v, gen_f = mesh(generated)
|
||||
ref = sample_surface(ref_v, ref_f, samples, seed)
|
||||
gen = sample_surface(gen_v, gen_f, samples, seed + 1)
|
||||
ref -= (ref.min(axis=0) + ref.max(axis=0)) / 2.0
|
||||
gen -= (gen.min(axis=0) + gen.max(axis=0)) / 2.0
|
||||
ref_tree = cKDTree(ref)
|
||||
best_chamfer = math.inf
|
||||
best_hausdorff = math.inf
|
||||
for transform in TRANSFORMS:
|
||||
candidate = gen @ transform
|
||||
gen_tree = cKDTree(candidate)
|
||||
d_gr = ref_tree.query(candidate, workers=-1)[0]
|
||||
d_rg = gen_tree.query(ref, workers=-1)[0]
|
||||
chamfer = float((d_gr.mean() + d_rg.mean()) / 2.0)
|
||||
hausdorff = float(max(np.quantile(d_gr, 0.95), np.quantile(d_rg, 0.95)))
|
||||
if chamfer < best_chamfer:
|
||||
best_chamfer = chamfer
|
||||
best_hausdorff = hausdorff
|
||||
diagonal = max(float(np.linalg.norm(np.ptp(ref, axis=0))), 1e-9)
|
||||
return best_chamfer / diagonal, best_hausdorff / diagonal
|
||||
|
||||
|
||||
def task_experience(step_path: Path) -> tuple[int, int]:
|
||||
task = step_path.parent / "cad-task.json"
|
||||
if not task.is_file():
|
||||
return 0, 0
|
||||
payload = json.loads(task.read_text())
|
||||
exp = payload.get("experience", {})
|
||||
returned = exp.get("methods_returned")
|
||||
if returned is None:
|
||||
returned = len(exp.get("methods", exp.get("methods_applied", [])))
|
||||
if isinstance(returned, list):
|
||||
returned = len(returned)
|
||||
return int(returned or 0), len(exp.get("methods_applied", []))
|
||||
|
||||
|
||||
def evaluate_one(
|
||||
case: dict[str, Any], variant: str, samples: int
|
||||
) -> dict[str, Any]:
|
||||
generated_path = Path(case[f"{variant}_step"])
|
||||
row: dict[str, Any] = {
|
||||
"id": case["id"],
|
||||
"family": case["family"],
|
||||
"variant": variant,
|
||||
"generated": generated_path.is_file(),
|
||||
}
|
||||
if not generated_path.is_file():
|
||||
row["error"] = "missing STEP"
|
||||
return row
|
||||
try:
|
||||
reference = import_step(case["reference_step"])
|
||||
generated = import_step(str(generated_path))
|
||||
ref_case = json.loads(Path(case["case_json"]).read_text())
|
||||
gen_case = EXTRACT.extract_step_case(generated_path)
|
||||
ref_bbox = sorted(float(v) for v in reference.bounding_box().size)
|
||||
gen_bbox = sorted(float(v) for v in generated.bounding_box().size)
|
||||
bbox_error = float(
|
||||
np.mean([rel_error(a, b) for a, b in zip(gen_bbox, ref_bbox)])
|
||||
)
|
||||
volume_error = rel_error(float(generated.volume), float(reference.volume))
|
||||
area_error = rel_error(float(generated.area), float(reference.area))
|
||||
face_error = rel_error(len(generated.faces()), len(reference.faces()))
|
||||
edge_error = rel_error(len(generated.edges()), len(reference.edges()))
|
||||
chamfer, hausdorff95 = surface_distance(
|
||||
reference, generated, int(case["id"]), samples
|
||||
)
|
||||
feature_f1 = f1_score(feature_names(ref_case), feature_names(gen_case))
|
||||
family_match = float(
|
||||
ref_case["design_ir"]["part_family"]
|
||||
== gen_case["design_ir"]["part_family"]
|
||||
)
|
||||
returned, applied = task_experience(generated_path)
|
||||
|
||||
# Transparent 0–100 similarity index. It is a benchmark summary, not a
|
||||
# claim of manufacturing interchangeability or exact B-rep identity.
|
||||
score = 100.0 * (
|
||||
0.18 * math.exp(-volume_error)
|
||||
+ 0.12 * math.exp(-area_error)
|
||||
+ 0.16 * math.exp(-bbox_error)
|
||||
+ 0.24 * math.exp(-8.0 * chamfer)
|
||||
+ 0.10 * math.exp(-(face_error + edge_error) / 2.0)
|
||||
+ 0.15 * feature_f1
|
||||
+ 0.05 * family_match
|
||||
)
|
||||
row.update(
|
||||
{
|
||||
"valid": bool(generated.is_valid),
|
||||
"solid_count": len(generated.solids()),
|
||||
"reference_volume": float(reference.volume),
|
||||
"generated_volume": float(generated.volume),
|
||||
"volume_rel_error": volume_error,
|
||||
"surface_area_rel_error": area_error,
|
||||
"bbox_mape": bbox_error,
|
||||
"face_rel_error": face_error,
|
||||
"edge_rel_error": edge_error,
|
||||
"surface_chamfer_norm": chamfer,
|
||||
"surface_hausdorff95_norm": hausdorff95,
|
||||
"feature_f1": feature_f1,
|
||||
"family_match": family_match,
|
||||
"experience_methods_returned": returned,
|
||||
"experience_methods_applied": applied,
|
||||
"similarity_score": score,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
row["error"] = f"{type(exc).__name__}: {exc}"
|
||||
return row
|
||||
|
||||
|
||||
def mean(rows: list[dict[str, Any]], key: str) -> float:
|
||||
values = [float(row[key]) for row in rows if key in row]
|
||||
return sum(values) / len(values) if values else math.nan
|
||||
|
||||
|
||||
def write_report(rows: list[dict[str, Any]]) -> None:
|
||||
complete = [row for row in rows if "similarity_score" in row]
|
||||
baseline = [row for row in complete if row["variant"] == "baseline"]
|
||||
experience = [row for row in complete if row["variant"] == "experience"]
|
||||
by_id = {}
|
||||
for row in complete:
|
||||
by_id.setdefault(row["id"], {})[row["variant"]] = row
|
||||
pairs = [
|
||||
item for item in by_id.values() if {"baseline", "experience"} <= item.keys()
|
||||
]
|
||||
paired_baseline = [item["baseline"] for item in pairs]
|
||||
paired_experience = [item["experience"] for item in pairs]
|
||||
deltas = [
|
||||
item["experience"]["similarity_score"]
|
||||
- item["baseline"]["similarity_score"]
|
||||
for item in pairs
|
||||
]
|
||||
wins = sum(delta > 0.25 for delta in deltas)
|
||||
ties = sum(abs(delta) <= 0.25 for delta in deltas)
|
||||
losses = sum(delta < -0.25 for delta in deltas)
|
||||
methods_returned = sum(
|
||||
row.get("experience_methods_returned", 0) for row in experience
|
||||
)
|
||||
methods_applied = sum(
|
||||
row.get("experience_methods_applied", 0) for row in experience
|
||||
)
|
||||
report = f"""# CAD 经验库 50 件 A/B 盲重建报告
|
||||
|
||||
## 口径
|
||||
|
||||
- 测试集:固定分层抽取 50 件;这 50 件不进入经验库。
|
||||
- 训练经验:其余 950 件生成的通用经验库。
|
||||
- 两组输入完全相同:仅多视图图片和文字描述;生成过程禁止读取参考 STEP 和解析 JSON。
|
||||
- Baseline:空经验库;Experience:950 件经验库。
|
||||
- 参考 STEP 只在生成完成后用于评分。
|
||||
- “相似度”是体积、面积、包围盒、表面距离、拓扑和语义特征的综合诊断分,不代表制造级 1:1 或可互换性。
|
||||
|
||||
## 当前完成度
|
||||
|
||||
- 有效 Baseline:{len(baseline)}/50
|
||||
- 有效 Experience:{len(experience)}/50
|
||||
- 完整配对:{len(pairs)}/50
|
||||
- 经验方法返回/实际采用:{methods_returned}/{methods_applied}
|
||||
|
||||
## 汇总
|
||||
|
||||
| 指标 | 无经验 | 有经验 | 差值 |
|
||||
|---|---:|---:|---:|
|
||||
| 综合相似度(0–100,越高越好) | {mean(paired_baseline, "similarity_score"):.2f} | {mean(paired_experience, "similarity_score"):.2f} | {mean(paired_experience, "similarity_score") - mean(paired_baseline, "similarity_score"):+.2f} |
|
||||
| 体积相对误差(越低越好) | {mean(paired_baseline, "volume_rel_error"):.3f} | {mean(paired_experience, "volume_rel_error"):.3f} | {mean(paired_experience, "volume_rel_error") - mean(paired_baseline, "volume_rel_error"):+.3f} |
|
||||
| 表面积相对误差 | {mean(paired_baseline, "surface_area_rel_error"):.3f} | {mean(paired_experience, "surface_area_rel_error"):.3f} | {mean(paired_experience, "surface_area_rel_error") - mean(paired_baseline, "surface_area_rel_error"):+.3f} |
|
||||
| 包围盒尺寸 MAPE | {mean(paired_baseline, "bbox_mape"):.3f} | {mean(paired_experience, "bbox_mape"):.3f} | {mean(paired_experience, "bbox_mape") - mean(paired_baseline, "bbox_mape"):+.3f} |
|
||||
| 归一化表面 Chamfer | {mean(paired_baseline, "surface_chamfer_norm"):.4f} | {mean(paired_experience, "surface_chamfer_norm"):.4f} | {mean(paired_experience, "surface_chamfer_norm") - mean(paired_baseline, "surface_chamfer_norm"):+.4f} |
|
||||
| 特征 F1 | {mean(paired_baseline, "feature_f1"):.3f} | {mean(paired_experience, "feature_f1"):.3f} | {mean(paired_experience, "feature_f1") - mean(paired_baseline, "feature_f1"):+.3f} |
|
||||
|
||||
经验组胜/平/负(±0.25 分视为平):**{wins}/{ties}/{losses}**。
|
||||
"""
|
||||
(HERE / "report.md").write_text(report)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--samples", type=int, default=1600)
|
||||
args = parser.parse_args()
|
||||
manifest = json.loads((HERE / "manifest.json").read_text())
|
||||
rows = [
|
||||
evaluate_one(case, variant, args.samples)
|
||||
for case in manifest["cases"]
|
||||
for variant in ("baseline", "experience")
|
||||
]
|
||||
(HERE / "metrics.json").write_text(
|
||||
json.dumps(rows, indent=2, ensure_ascii=False) + "\n"
|
||||
)
|
||||
keys = sorted({key for row in rows for key in row})
|
||||
with (HERE / "metrics.csv").open("w", newline="") as stream:
|
||||
writer = csv.DictWriter(stream, fieldnames=keys)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
write_report(rows)
|
||||
completed = sum("similarity_score" in row for row in rows)
|
||||
print(json.dumps({"evaluated": completed, "expected": 100}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
bbox_mape,edge_rel_error,error,experience_methods_applied,experience_methods_returned,face_rel_error,family,family_match,feature_f1,generated,generated_volume,id,reference_volume,similarity_score,solid_count,surface_area_rel_error,surface_chamfer_norm,surface_hausdorff95_norm,valid,variant,volume_rel_error
|
||||
0.0,0.5,,0,0,0.25,general_mechanical_part,1.0,0.8571428571428571,True,125663.70614359173,000166,125663.70614359173,93.81711632834559,1,6.425513440379324e-14,0.004847580881730266,0.00977766538694562,True,baseline,0.0
|
||||
0.0,0.5,,0,0,0.25,general_mechanical_part,1.0,0.8571428571428571,True,125663.70614359173,000166,125663.70614359173,93.81711632834559,1,6.425513440379324e-14,0.004847580881730266,0.00977766538694562,True,experience,0.0
|
||||
0.45066264681866697,0.125,,0,0,0.1111111111111111,patterned_plate,0.0,0.8571428571428571,True,106129.48703937844,004764,36749.40939347248,54.33835930595081,1,1.0309687869495758,0.05550481239248205,0.1775660613682025,True,baseline,1.8879236099568286
|
||||
0.3413730293323282,0.125,,3,18,0.1111111111111111,patterned_plate,0.0,0.8571428571428571,True,88774.08881569223,004764,36749.40939347248,59.488074232257325,1,0.7876989178243013,0.0465090322155671,0.15562347480527094,True,experience,1.4156602862700836
|
||||
0.5696649029982362,0.0,,0,0,0.0,plate_or_bracket,1.0,1.0,True,19016.0,011680,898496.0,60.445468751589296,1,0.9780420050604243,0.10796723414266624,0.36481967349123434,True,baseline,0.9788357432865589
|
||||
0.583774250440917,0.0,,4,10,0.0,plate_or_bracket,1.0,1.0,True,13547.999999999998,011680,898496.0,59.90070139134242,1,0.9842171382062035,0.11235674854244815,0.374964117147598,True,experience,0.9849214687655816
|
||||
0.18571427761876255,0.2631578947368421,,0,0,0.26666666666666666,patterned_plate,1.0,1.0,True,23882.59512641282,016033,44618.37587532573,81.50778192907364,1,0.34116223818430896,0.018452946285241585,0.08713798240651745,True,baseline,0.4647363410728704
|
||||
0.3238095238095235,0.02631578947368421,,3,10,0.06666666666666667,patterned_plate,1.0,0.9411764705882353,True,13866.369040708962,016033,44618.37587532573,75.91174289423121,1,0.5000404499582691,0.026851094204940862,0.12169100582216402,True,experience,0.6892229094251465
|
||||
0.5959595959595959,0.2857142857142857,,0,0,0.28125,flanged_rotational_part,1.0,1.0,True,118756.98485692723,020543,24145.808587596228,51.56160371886595,1,1.5105685440071281,0.08451218347517844,0.22747541674825858,True,baseline,3.9183271053483395
|
||||
0.23232323232323235,0.05194805194805195,,0,0,0.1875,flanged_rotational_part,1.0,0.888888888888889,True,55638.90609255368,020543,24145.808587596228,69.33575293745653,1,0.4640684949891876,0.042969081471212464,0.11104259063963073,True,experience,1.3042883774509648
|
||||
0.06666666666666685,0.2,,0,0,0.19480519480519481,patterned_prismatic_part,1.0,1.0,True,569438.4483554161,024675,580617.6091315141,91.92371160771847,1,0.2739018704584683,0.011073315847120254,0.07352156562273186,True,baseline,0.019253912730652068
|
||||
0.5777777777777777,0.8933333333333333,,0,0,1.4025974025974026,patterned_prismatic_part,1.0,0.9333333333333333,True,38876.03946418978,024675,580617.6091315141,56.84188202691976,1,0.8746474563374864,0.07094038930770122,0.33837892456299423,True,experience,0.9330436437807312
|
||||
0.0980952380952385,0.16666666666666666,,0,0,0.14285714285714285,patterned_plate,1.0,0.9333333333333333,True,73807.58425433026,029190,131254.75510206894,83.74861954252185,1,0.1560183040026604,0.024101504873521428,0.04232654923555514,True,baseline,0.4376768735202429
|
||||
0.14047619047618823,1.5277777777777777,,0,0,1.6428571428571428,patterned_plate,1.0,0.7999999999999999,True,71846.37021205797,029190,131254.75510206894,73.91053356207678,1,0.20263501345527155,0.02460078307658505,0.04143094591291666,True,experience,0.45261891535901033
|
||||
0.31241830065359194,0.9285714285714286,,0,0,0.5625,patterned_plate,1.0,0.923076923076923,True,34352.594678913054,030352,60881.15044407846,73.94372815488485,1,0.48742381144539443,0.02510282770499014,0.10902462973941958,True,baseline,0.4357433388111292
|
||||
0.4306604575163398,2.642857142857143,,3,10,2.0625,patterned_plate,1.0,0.7692307692307692,True,11173.345159683424,030352,60881.15044407846,59.45331217188532,1,0.6732862799472543,0.03957952772551367,0.16850566658178062,True,experience,0.8164728314398964
|
||||
0.21212121212121218,0.5,,0,0,0.43243243243243246,flanged_rotational_part,1.0,0.9600000000000001,True,73578.89519968569,046014,162271.3445381513,75.92217396437314,1,0.333964798541362,0.033942930350360764,0.05126001418573268,True,baseline,0.5465687709120651
|
||||
0.13636363636363644,0.5,,4,10,0.43243243243243246,flanged_rotational_part,1.0,0.9600000000000001,True,111532.3028776836,046014,162271.3445381513,81.61765078194381,1,0.24200901291799348,0.02662187229047825,0.043202965297600246,True,experience,0.31268023202050027
|
||||
0.6851851851851847,0.3709677419354839,,0,0,0.34782608695652173,patterned_prismatic_part,1.0,0.823529411764706,True,167251.53322574112,047048,33461.296317388085,44.79000279500974,1,1.6260155362793598,0.11322328439048983,0.2789388077989153,True,baseline,3.9983578531842254
|
||||
0.7175925925925921,0.1693548387096774,,3,18,0.2391304347826087,patterned_prismatic_part,1.0,0.823529411764706,True,169277.67589913897,047048,33461.296317388085,44.38628038601891,1,1.587008333438104,0.13259494837120114,0.321530970786483,True,experience,4.058909681606514
|
||||
1.4575235605335388e-16,0.5,,0,0,0.3125,hollow_or_stepped_shaft,1.0,0.9166666666666666,True,8278002.394429736,048346,8341713.8934443,93.45850460439323,1,0.01545893719801078,0.008802226092164953,0.016975575422477428,True,baseline,0.007637698898380382
|
||||
1.4575235605335388e-16,0.5,,0,0,0.3125,hollow_or_stepped_shaft,1.0,0.9166666666666666,True,8292610.800268921,048346,8341713.8934443,93.51807327218812,1,0.012238325281746575,0.008857323306102671,0.017730676062993916,True,experience,0.005886451369899948
|
||||
0.02857142857142833,0.2222222222222222,,0,0,0.21212121212121213,patterned_plate,0.0,0.8571428571428571,True,315117.2806010815,053664,349435.5647059466,85.57701767939263,1,0.07576465816363272,0.012696566244914555,0.02434484580541925,True,baseline,0.0982106218459597
|
||||
0.07936507936507906,0.0,,0,0,0.030303030303030304,patterned_plate,0.0,0.8571428571428571,True,282776.14120936824,053664,349435.5647059466,83.57268406993335,1,0.167398697885095,0.016320456106002173,0.05162627100789126,True,experience,0.19076313412080106
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,063487,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,063487,,,,,,,,experience,
|
||||
0.05572210294349599,0.420863309352518,,0,0,0.4396551724137931,flanged_rotational_part,1.0,0.923076923076923,True,68680.44493144708,065487,81354.16556588435,87.21779338748289,1,0.060356551334099576,0.022572421105818447,0.053770414665160235,True,baseline,0.15578453231351141
|
||||
,,missing STEP,,,,flanged_rotational_part,,,False,,065487,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,067186,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,067186,,,,,,,,experience,
|
||||
,,missing STEP,,,,rotational_part,,,False,,073706,,,,,,,,baseline,
|
||||
,,missing STEP,,,,rotational_part,,,False,,073706,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,077955,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,077955,,,,,,,,experience,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,082187,,,,,,,,baseline,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,082187,,,,,,,,experience,
|
||||
,,missing STEP,,,,rotational_part,,,False,,089970,,,,,,,,baseline,
|
||||
,,missing STEP,,,,rotational_part,,,False,,089970,,,,,,,,experience,
|
||||
,,missing STEP,,,,shaft,,,False,,092092,,,,,,,,baseline,
|
||||
,,missing STEP,,,,shaft,,,False,,092092,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,095520,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,095520,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,102128,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,102128,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,104465,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,104465,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,110143,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,110143,,,,,,,,experience,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,112069,,,,,,,,baseline,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,112069,,,,,,,,experience,
|
||||
0.22361111004444456,0.6538461538461539,,0,0,0.6153846153846154,rotational_part,1.0,0.761904761904762,True,67519.74039233122,116166,147536.10631008807,71.3680012563105,1,0.38925591734814796,0.03424668647245051,0.07518718288900171,True,baseline,0.5423510754010293
|
||||
0.18888888915554566,0.7307692307692307,,3,16,0.6923076923076923,rotational_part,1.0,0.8571428571428572,True,69565.58402815007,116166,147536.10631008807,73.4520592182128,1,0.365981954180488,0.032489227662340435,0.07289222461786297,True,experience,0.528484343473599
|
||||
0.6746686351889055,0.45217391304347826,,0,0,0.45,multi_axis_manifold,0.0,0.7692307692307692,True,34234.94531047202,134828,349556.2389887021,48.526262227273214,1,0.9239731944207157,0.10449739414590258,0.3144594900968871,True,baseline,0.9020616956815967
|
||||
0.7062506154470962,0.5043478260869565,,5,14,0.5,multi_axis_manifold,0.0,0.7692307692307692,True,38190.323227729335,134828,349556.2389887021,48.60371993717729,1,0.9152269986233839,0.09836507300701838,0.29918959165673675,True,experience,0.8907462692177447
|
||||
7.3809523809523805,0.5,,0,0,0.3333333333333333,rotational_part,1.0,0.9411764705882353,True,25446.900494077883,136930,38.877209088173686,25.720060532485707,1,72.6363636363658,1.666072751413829,2.6857505921882785,True,baseline,653.545454545469
|
||||
6.936507936507937,0.5,,0,0,0.3333333333333333,rotational_part,1.0,0.9411764705882353,True,21205.750411731566,136930,38.877209088173686,25.725642575854934,1,67.18181818181998,1.6549008981976585,2.6698915132197145,True,experience,544.4545454545574
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,138022,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,138022,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,141680,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,141680,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,145574,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,145574,,,,,,,,experience,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,150196,,,,,,,,baseline,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,150196,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,150587,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,150587,,,,,,,,experience,
|
||||
,,missing STEP,,,,multi_axis_manifold,,,False,,152568,,,,,,,,baseline,
|
||||
,,missing STEP,,,,multi_axis_manifold,,,False,,152568,,,,,,,,experience,
|
||||
,,missing STEP,,,,plate_or_bracket,,,False,,153245,,,,,,,,baseline,
|
||||
,,missing STEP,,,,plate_or_bracket,,,False,,153245,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,158256,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,158256,,,,,,,,experience,
|
||||
,,missing STEP,,,,hollow_or_stepped_shaft,,,False,,162558,,,,,,,,baseline,
|
||||
,,missing STEP,,,,hollow_or_stepped_shaft,,,False,,162558,,,,,,,,experience,
|
||||
,,missing STEP,,,,multi_axis_manifold,,,False,,167962,,,,,,,,baseline,
|
||||
,,missing STEP,,,,multi_axis_manifold,,,False,,167962,,,,,,,,experience,
|
||||
,,missing STEP,,,,rotational_part,,,False,,168239,,,,,,,,baseline,
|
||||
,,missing STEP,,,,rotational_part,,,False,,168239,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,174675,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,174675,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,183151,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,183151,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,189669,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,189669,,,,,,,,experience,
|
||||
,,missing STEP,,,,rotational_part,,,False,,194029,,,,,,,,baseline,
|
||||
,,missing STEP,,,,rotational_part,,,False,,194029,,,,,,,,experience,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,201547,,,,,,,,baseline,
|
||||
,,missing STEP,,,,prismatic_block,,,False,,201547,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,204730,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,204730,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,211326,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,211326,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,214404,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_plate,,,False,,214404,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,229115,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,229115,,,,,,,,experience,
|
||||
,,missing STEP,,,,hybrid_freeform_part,,,False,,234097,,,,,,,,baseline,
|
||||
,,missing STEP,,,,hybrid_freeform_part,,,False,,234097,,,,,,,,experience,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,242307,,,,,,,,baseline,
|
||||
,,missing STEP,,,,patterned_prismatic_part,,,False,,242307,,,,,,,,experience,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
# CAD 经验库 50 件 A/B 盲重建报告
|
||||
|
||||
> 运行状态:2026-07-23 因 Codex 账户用量上限中断;当前为 15/50
|
||||
> 完整配对的阶段结果。系统提示可在 2026-07-29 10:24 后继续。
|
||||
|
||||
## 口径
|
||||
|
||||
- 测试集:固定分层抽取 50 件;这 50 件不进入经验库。
|
||||
- 训练经验:其余 950 件生成的通用经验库。
|
||||
- 两组输入完全相同:仅多视图图片和文字描述;生成过程禁止读取参考 STEP 和解析 JSON。
|
||||
- Baseline:空经验库;Experience:950 件经验库。
|
||||
- 参考 STEP 只在生成完成后用于评分。
|
||||
- “相似度”是体积、面积、包围盒、表面距离、拓扑和语义特征的综合诊断分,不代表制造级 1:1 或可互换性。
|
||||
|
||||
## 当前完成度
|
||||
|
||||
- 有效 Baseline:16/50
|
||||
- 有效 Experience:15/50
|
||||
- 完整配对:15/50
|
||||
- 经验方法返回/实际采用:106/28
|
||||
|
||||
## 汇总
|
||||
|
||||
| 指标 | 无经验 | 有经验 | 差值 |
|
||||
|---|---:|---:|---:|
|
||||
| 综合相似度(0–100,越高越好) | 69.11 | 66.64 | -2.47 |
|
||||
| 体积相对误差(越低越好) | 44.519 | 37.136 | -7.383 |
|
||||
| 表面积相对误差 | 5.385 | 4.997 | -0.388 |
|
||||
| 包围盒尺寸 MAPE | 0.766 | 0.760 | -0.006 |
|
||||
| 归一化表面 Chamfer | 0.1537 | 0.1559 | +0.0023 |
|
||||
| 特征 F1 | 0.907 | 0.878 | -0.029 |
|
||||
|
||||
经验组胜/平/负(±0.25 分视为平):**4/4/7**。
|
||||
|
||||
## 阶段解释
|
||||
|
||||
- 经验组的体积、表面积和包围盒平均误差较低,说明通用构造经验对尺度/主体关系有一定帮助。
|
||||
- 经验组的表面距离、特征 F1 和综合分暂时较差,说明部分经验会诱导多余或错误特征,当前不能宣称整体提升。
|
||||
- 每组由独立的大模型运行生成;即使经验命中为 0,两次结果也可能因模型随机性不同。因此最终应完成 50 对,并对若干样本做重复运行,才能把经验效果与随机波动分开。
|
||||
@@ -0,0 +1,10 @@
|
||||
# STEP reconstruction benchmark
|
||||
|
||||
Each case stores only a protected teacher STEP in Git LFS. DesignIR,
|
||||
reconstructed STEP, acceptance reports, and perturbation artifacts are produced
|
||||
at runtime outside the case directory.
|
||||
|
||||
These two teachers were migrated from the retired Exact Base fixtures. Their old
|
||||
copied-B-Rep specifications and generated outputs were intentionally removed.
|
||||
They are now held-out truth for independent DesignIR reconstruction.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"case_id": "015133",
|
||||
"teacher": "teacher.step",
|
||||
"purpose": "held_out_designir_reconstruction"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"case_id": "015240",
|
||||
"teacher": "teacher.step",
|
||||
"purpose": "held_out_designir_reconstruction"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"minimum_independent_cases": 3,
|
||||
"required_states": [
|
||||
"geometry_validated",
|
||||
"editability_validated",
|
||||
"replay_validated"
|
||||
],
|
||||
"thresholds": {
|
||||
"relative_volume_error_max": 0.01,
|
||||
"center_of_mass_error_mm_max": 0.1,
|
||||
"bounding_box_axis_error_mm_max": 0.1,
|
||||
"symmetric_difference_ratio_max": 0.02,
|
||||
"parameter_perturbation_success_rate_min": 1.0
|
||||
},
|
||||
"publication_guards": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"teacher_geometry_allowed": false,
|
||||
"automatic_agent_publish_allowed": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cadset.local/schemas/designir-2.0.schema.json",
|
||||
"title": "DesignIR 2.0",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"family",
|
||||
"units",
|
||||
"reconstruction_status",
|
||||
"coordinate_system",
|
||||
"datums",
|
||||
"parameters",
|
||||
"expressions",
|
||||
"sketches",
|
||||
"constraints",
|
||||
"features",
|
||||
"patterns",
|
||||
"attachments",
|
||||
"construction_stages",
|
||||
"edit_interface",
|
||||
"validation_contract"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {"const": "2.0"},
|
||||
"designir_kind": {"const": "executable_parametric_design"},
|
||||
"model_id": {"type": "string", "pattern": "^[A-Za-z0-9._-]+$"},
|
||||
"family": {"type": "string", "minLength": 1},
|
||||
"units": {"const": "mm"},
|
||||
"backend_hint": {
|
||||
"enum": ["auto", "build123d", "simplecadapi"]
|
||||
},
|
||||
"reconstruction_status": {
|
||||
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
|
||||
},
|
||||
"missing_capabilities": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"coordinate_system": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["origin", "x_axis", "y_axis", "z_axis"],
|
||||
"properties": {
|
||||
"origin": {"$ref": "#/$defs/vector3"},
|
||||
"x_axis": {"$ref": "#/$defs/vector3"},
|
||||
"y_axis": {"$ref": "#/$defs/vector3"},
|
||||
"z_axis": {"$ref": "#/$defs/vector3"}
|
||||
}
|
||||
},
|
||||
"datums": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "object"}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"$ref": "#/$defs/parameter"}
|
||||
},
|
||||
"expressions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string", "minLength": 1}
|
||||
},
|
||||
"sketches": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"}
|
||||
},
|
||||
"constraints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["kind", "participants"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"kind": {"type": "string"},
|
||||
"participants": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/feature"}
|
||||
},
|
||||
"patterns": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"}
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"}
|
||||
},
|
||||
"construction_stages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "feature_ids"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"feature_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit_interface": {
|
||||
"type": "object",
|
||||
"required": ["editable_parameters"],
|
||||
"properties": {
|
||||
"editable_parameters": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"preserved_interfaces": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation_contract": {
|
||||
"type": "object",
|
||||
"required": ["invariants", "perturbations"],
|
||||
"properties": {
|
||||
"invariants": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"}
|
||||
},
|
||||
"perturbations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["parameter", "scale"],
|
||||
"properties": {
|
||||
"parameter": {"type": "string"},
|
||||
"scale": {"type": "number", "exclusiveMinimum": 0},
|
||||
"expected_change": {"type": "string"},
|
||||
"preserve": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thresholds": {"type": "object"}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"promoted_method_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"confidence": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "number", "minimum": 0, "maximum": 1}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"vector3": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "number"},
|
||||
{"type": "number"},
|
||||
{"type": "number"}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["value", "unit", "editable"],
|
||||
"properties": {
|
||||
"value": {"type": ["number", "integer"]},
|
||||
"unit": {"enum": ["mm", "deg", "count", "ratio"]},
|
||||
"editable": {"type": "boolean"},
|
||||
"role": {"type": "string"},
|
||||
"minimum": {"type": "number"},
|
||||
"maximum": {"type": "number"}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{"type": "number"},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["parameter"],
|
||||
"properties": {"parameter": {"type": "string"}}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["expression"],
|
||||
"properties": {"expression": {"type": "string"}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"feature": {
|
||||
"type": "object",
|
||||
"required": ["id", "operation"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"operation": {
|
||||
"enum": [
|
||||
"extrude_circle",
|
||||
"extrude_rectangle",
|
||||
"add_cylinder",
|
||||
"through_hole",
|
||||
"polar_hole_pattern"
|
||||
]
|
||||
},
|
||||
"radius": {"$ref": "#/$defs/value"},
|
||||
"diameter": {"$ref": "#/$defs/value"},
|
||||
"height": {"$ref": "#/$defs/value"},
|
||||
"width": {"$ref": "#/$defs/value"},
|
||||
"depth": {"$ref": "#/$defs/value"},
|
||||
"count": {"$ref": "#/$defs/value"},
|
||||
"pitch_diameter": {"$ref": "#/$defs/value"},
|
||||
"axis": {"type": "string"},
|
||||
"center": {"$ref": "#/$defs/vector3"},
|
||||
"host": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"designir_kind": "executable_parametric_design",
|
||||
"model_id": "raised_hub_flange",
|
||||
"family": "raised_hub_flange",
|
||||
"units": "mm",
|
||||
"backend_hint": "build123d",
|
||||
"reconstruction_status": "ready",
|
||||
"missing_capabilities": [],
|
||||
"coordinate_system": {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1]
|
||||
},
|
||||
"datums": {
|
||||
"part_center": {"kind": "point", "at": [0, 0, 0]},
|
||||
"primary_axis": {"kind": "axis", "axis": "z"},
|
||||
"mounting_plane": {"kind": "plane", "normal": "primary_axis"}
|
||||
},
|
||||
"parameters": {
|
||||
"outer_radius": {"value": 50, "unit": "mm", "editable": true, "role": "outer_radius"},
|
||||
"plate_thickness": {"value": 4, "unit": "mm", "editable": true, "role": "plate_thickness"},
|
||||
"bore_diameter": {"value": 20, "unit": "mm", "editable": true, "role": "bore_diameter"},
|
||||
"hub_radius": {"value": 16, "unit": "mm", "editable": true, "role": "hub_radius"},
|
||||
"hub_height": {"value": 1, "unit": "mm", "editable": true, "role": "hub_height"},
|
||||
"hole_count": {"value": 8, "unit": "count", "editable": true, "role": "hole_count"},
|
||||
"hole_diameter": {"value": 3.4, "unit": "mm", "editable": true, "role": "clearance_hole_diameter"},
|
||||
"bolt_circle_diameter": {"value": 80, "unit": "mm", "editable": true, "role": "bolt_circle_diameter"}
|
||||
},
|
||||
"expressions": {
|
||||
"total_height": "plate_thickness + hub_height"
|
||||
},
|
||||
"sketches": [],
|
||||
"constraints": [
|
||||
{"id": "shared_axis", "kind": "concentric", "participants": ["base_flange", "raised_hub", "central_bore", "mounting_pattern"]},
|
||||
{"id": "uniform_pattern", "kind": "equal_angular_spacing", "participants": ["mounting_pattern"]}
|
||||
],
|
||||
"features": [
|
||||
{"id": "base_flange", "operation": "extrude_circle", "radius": {"parameter": "outer_radius"}, "height": {"parameter": "plate_thickness"}, "axis": "primary_axis"},
|
||||
{"id": "raised_hub", "operation": "add_cylinder", "radius": {"parameter": "hub_radius"}, "height": {"parameter": "hub_height"}, "center": [0, 0, 4], "axis": "primary_axis"},
|
||||
{"id": "central_bore", "operation": "through_hole", "diameter": {"parameter": "bore_diameter"}, "axis": "primary_axis", "host": "base_flange"},
|
||||
{"id": "mounting_pattern", "operation": "polar_hole_pattern", "count": {"parameter": "hole_count"}, "diameter": {"parameter": "hole_diameter"}, "pitch_diameter": {"parameter": "bolt_circle_diameter"}, "axis": "primary_axis", "host": "base_flange"}
|
||||
],
|
||||
"patterns": [
|
||||
{"feature": "mounting_pattern", "kind": "polar", "axis": "primary_axis"}
|
||||
],
|
||||
"attachments": [
|
||||
{"feature": "raised_hub", "host": "base_flange", "relation": "face_attached"}
|
||||
],
|
||||
"construction_stages": [
|
||||
{"id": "primary_envelope", "feature_ids": ["base_flange", "raised_hub"]},
|
||||
{"id": "passages", "feature_ids": ["central_bore", "mounting_pattern"]}
|
||||
],
|
||||
"edit_interface": {
|
||||
"editable_parameters": ["outer_radius", "plate_thickness", "bore_diameter", "hub_radius", "hub_height", "hole_count", "hole_diameter", "bolt_circle_diameter"],
|
||||
"preserved_interfaces": ["part_center", "primary_axis", "mounting_plane"]
|
||||
},
|
||||
"validation_contract": {
|
||||
"invariants": [
|
||||
{"kind": "single_closed_solid"},
|
||||
{"kind": "concentric", "participants": ["base_flange", "central_bore"]},
|
||||
{"kind": "equal_angular_spacing", "feature": "mounting_pattern"}
|
||||
],
|
||||
"perturbations": [
|
||||
{"parameter": "plate_thickness", "scale": 1.1, "expected_change": "overall_height_increases", "preserve": ["outer_radius", "hole_count"]},
|
||||
{"parameter": "bolt_circle_diameter", "scale": 1.15, "expected_change": "mounting_pattern_radius_increases", "preserve": ["outer_radius", "bore_diameter"]},
|
||||
{"parameter": "hole_count", "scale": 1.125, "expected_change": "mounting_hole_count_changes", "preserve": ["outer_radius", "bore_diameter"]}
|
||||
],
|
||||
"thresholds": {
|
||||
"relative_volume_error_max": 0.01,
|
||||
"symmetric_difference_ratio_max": 0.02
|
||||
}
|
||||
},
|
||||
"experience": {"promoted_method_ids": []},
|
||||
"confidence": {"family": 1.0, "features": 1.0, "parameters": 1.0}
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORKSPACE_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
|
||||
PYTHON_BIN="${CAD_EXPERIENCE_PYTHON:-}"
|
||||
|
||||
if [[ -z "$PYTHON_BIN" && -x "$WORKSPACE_ROOT/text-to-cad/.venv/bin/python" ]]; then
|
||||
PYTHON_BIN="$WORKSPACE_ROOT/text-to-cad/.venv/bin/python"
|
||||
fi
|
||||
if [[ -z "$PYTHON_BIN" ]]; then
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
|
||||
exec "$PYTHON_BIN" "$PLUGIN_ROOT/skills/cad-experience-builder/scripts/cad_experience.py" "$@"
|
||||
@@ -0,0 +1,786 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile and validate DesignIR 2.0 without depending on teacher geometry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from build123d import Align, Box, CenterOf, Cylinder, Part, Pos, export_step, import_step
|
||||
|
||||
|
||||
SCHEMA_VERSION = "2.0"
|
||||
DESIGNIR_KIND = "executable_parametric_design"
|
||||
SUPPORTED_OPERATIONS = {
|
||||
"extrude_circle",
|
||||
"extrude_rectangle",
|
||||
"add_cylinder",
|
||||
"through_hole",
|
||||
"polar_hole_pattern",
|
||||
}
|
||||
REQUIRED_TOP_LEVEL = {
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"family",
|
||||
"units",
|
||||
"reconstruction_status",
|
||||
"coordinate_system",
|
||||
"datums",
|
||||
"parameters",
|
||||
"expressions",
|
||||
"sketches",
|
||||
"constraints",
|
||||
"features",
|
||||
"patterns",
|
||||
"attachments",
|
||||
"construction_stages",
|
||||
"edit_interface",
|
||||
"validation_contract",
|
||||
}
|
||||
FORBIDDEN_KEYS = {
|
||||
"source_step",
|
||||
"teacher_step",
|
||||
"source_path",
|
||||
"brep",
|
||||
"b_rep",
|
||||
"step_text",
|
||||
"mesh",
|
||||
"triangles",
|
||||
"vertices",
|
||||
"face_refs",
|
||||
"surface_ids",
|
||||
"topology_refs",
|
||||
"base_step",
|
||||
}
|
||||
FORBIDDEN_TEXT = (
|
||||
"source.step",
|
||||
"teacher.step",
|
||||
".stp",
|
||||
".step",
|
||||
"import_step",
|
||||
"read_step",
|
||||
"stepcontrol_reader",
|
||||
)
|
||||
SAFE_EXPRESSION_NODES = (
|
||||
ast.Expression,
|
||||
ast.BinOp,
|
||||
ast.UnaryOp,
|
||||
ast.Add,
|
||||
ast.Sub,
|
||||
ast.Mult,
|
||||
ast.Div,
|
||||
ast.Pow,
|
||||
ast.Mod,
|
||||
ast.USub,
|
||||
ast.UAdd,
|
||||
ast.Constant,
|
||||
ast.Name,
|
||||
ast.Load,
|
||||
)
|
||||
|
||||
|
||||
class DesignIRError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise DesignIRError(f"Cannot read JSON {path}: {exc}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise DesignIRError("DesignIR must be a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def write_json(path: Path, value: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def find_forbidden(value: Any, trail: str = "$") -> str | None:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
normalized = str(key).lower()
|
||||
if normalized in FORBIDDEN_KEYS:
|
||||
return f"{trail}.{key}"
|
||||
found = find_forbidden(child, f"{trail}.{key}")
|
||||
if found:
|
||||
return found
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
found = find_forbidden(child, f"{trail}[{index}]")
|
||||
if found:
|
||||
return found
|
||||
elif isinstance(value, str):
|
||||
lowered = value.lower()
|
||||
if any(token in lowered for token in FORBIDDEN_TEXT):
|
||||
return trail
|
||||
return None
|
||||
|
||||
|
||||
def _vector3(value: Any, label: str) -> None:
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 3
|
||||
or any(not isinstance(item, (int, float)) for item in value)
|
||||
):
|
||||
raise DesignIRError(f"{label} must be a three-number vector")
|
||||
|
||||
|
||||
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
missing = sorted(REQUIRED_TOP_LEVEL - set(payload))
|
||||
if missing:
|
||||
raise DesignIRError("Missing DesignIR fields: " + ", ".join(missing))
|
||||
if payload["schema_version"] != SCHEMA_VERSION:
|
||||
raise DesignIRError("DesignIR schema_version must be 2.0")
|
||||
if payload["designir_kind"] != DESIGNIR_KIND:
|
||||
raise DesignIRError(f"designir_kind must be {DESIGNIR_KIND}")
|
||||
if payload["units"] != "mm":
|
||||
raise DesignIRError("Only millimetres are supported")
|
||||
if payload["reconstruction_status"] not in {
|
||||
"ready",
|
||||
"partial",
|
||||
"unsupported_feature_vocabulary",
|
||||
}:
|
||||
raise DesignIRError("Invalid reconstruction_status")
|
||||
forbidden = find_forbidden(payload)
|
||||
if forbidden:
|
||||
raise DesignIRError(f"Teacher geometry dependency is forbidden at {forbidden}")
|
||||
|
||||
coordinate_system = payload["coordinate_system"]
|
||||
if not isinstance(coordinate_system, dict):
|
||||
raise DesignIRError("coordinate_system must be an object")
|
||||
for name in ("origin", "x_axis", "y_axis", "z_axis"):
|
||||
_vector3(coordinate_system.get(name), f"coordinate_system.{name}")
|
||||
|
||||
parameters = payload["parameters"]
|
||||
if not isinstance(parameters, dict):
|
||||
raise DesignIRError("parameters must be an object")
|
||||
for name, parameter in parameters.items():
|
||||
if not isinstance(parameter, dict) or not isinstance(
|
||||
parameter.get("value"), (int, float)
|
||||
):
|
||||
raise DesignIRError(f"Parameter {name} requires a numeric value")
|
||||
if parameter.get("unit") not in {"mm", "deg", "count", "ratio"}:
|
||||
raise DesignIRError(f"Parameter {name} has an unsupported unit")
|
||||
if not isinstance(parameter.get("editable"), bool):
|
||||
raise DesignIRError(f"Parameter {name} requires editable=true/false")
|
||||
|
||||
features = payload["features"]
|
||||
if not isinstance(features, list) or not features:
|
||||
raise DesignIRError("features must be a non-empty array")
|
||||
feature_ids: set[str] = set()
|
||||
for feature in features:
|
||||
if not isinstance(feature, dict):
|
||||
raise DesignIRError("Every feature must be an object")
|
||||
feature_id = str(feature.get("id", "")).strip()
|
||||
if not feature_id or feature_id in feature_ids:
|
||||
raise DesignIRError("Every feature requires a unique id")
|
||||
feature_ids.add(feature_id)
|
||||
operation = feature.get("operation")
|
||||
if operation not in SUPPORTED_OPERATIONS:
|
||||
raise DesignIRError(f"Unsupported DesignIR operation: {operation}")
|
||||
|
||||
editable = payload["edit_interface"].get("editable_parameters", [])
|
||||
unknown = sorted(set(editable) - set(parameters))
|
||||
if unknown:
|
||||
raise DesignIRError("Unknown editable parameters: " + ", ".join(unknown))
|
||||
for test in payload["validation_contract"].get("perturbations", []):
|
||||
if test.get("parameter") not in parameters:
|
||||
raise DesignIRError(
|
||||
f"Perturbation references unknown parameter {test.get('parameter')}"
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _safe_expression(expression: str, values: dict[str, float]) -> float:
|
||||
try:
|
||||
tree = ast.parse(expression, mode="eval")
|
||||
except SyntaxError as exc:
|
||||
raise DesignIRError(f"Invalid expression {expression!r}") from exc
|
||||
if not all(isinstance(node, SAFE_EXPRESSION_NODES) for node in ast.walk(tree)):
|
||||
raise DesignIRError(f"Unsafe expression {expression!r}")
|
||||
unknown = {
|
||||
node.id
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Name) and node.id not in values
|
||||
}
|
||||
if unknown:
|
||||
raise DesignIRError("Unknown expression symbols: " + ", ".join(sorted(unknown)))
|
||||
return float(eval(compile(tree, "<designir-expression>", "eval"), {"__builtins__": {}}, values))
|
||||
|
||||
|
||||
def parameter_values(payload: dict[str, Any]) -> dict[str, float]:
|
||||
values = {
|
||||
name: float(parameter["value"])
|
||||
for name, parameter in payload["parameters"].items()
|
||||
}
|
||||
pending = dict(payload.get("expressions", {}))
|
||||
while pending:
|
||||
progressed = False
|
||||
for name, expression in list(pending.items()):
|
||||
try:
|
||||
values[name] = _safe_expression(str(expression), values)
|
||||
except DesignIRError as exc:
|
||||
if "Unknown expression symbols" in str(exc):
|
||||
continue
|
||||
raise
|
||||
del pending[name]
|
||||
progressed = True
|
||||
if not progressed:
|
||||
raise DesignIRError(
|
||||
"Unresolved or cyclic expressions: " + ", ".join(sorted(pending))
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, dict) and set(value) == {"parameter"}:
|
||||
name = str(value["parameter"])
|
||||
if name not in values:
|
||||
raise DesignIRError(f"{label} references unknown parameter {name}")
|
||||
return values[name]
|
||||
if isinstance(value, dict) and set(value) == {"expression"}:
|
||||
name = str(value["expression"])
|
||||
if name not in values:
|
||||
raise DesignIRError(f"{label} references unknown expression {name}")
|
||||
return values[name]
|
||||
raise DesignIRError(f"{label} must be a number, parameter, or expression")
|
||||
|
||||
|
||||
def _axis_is_z(feature: dict[str, Any]) -> None:
|
||||
if feature.get("axis", "primary_axis") not in {"z", "primary_axis"}:
|
||||
raise DesignIRError("The initial compiler supports only the primary Z axis")
|
||||
|
||||
|
||||
def build_shape(payload: dict[str, Any]) -> Part:
|
||||
validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
f"DesignIR is not independently rebuildable; missing capabilities: {missing}"
|
||||
)
|
||||
values = parameter_values(payload)
|
||||
shape: Any = None
|
||||
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
if operation in {"extrude_circle", "add_cylinder"}:
|
||||
radius = resolve_value(feature["radius"], values, f"{feature['id']}.radius")
|
||||
height = resolve_value(feature["height"], values, f"{feature['id']}.height")
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
tool = Pos(*map(float, center)) * Cylinder(
|
||||
radius,
|
||||
height,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
shape = tool if shape is None else shape + tool
|
||||
elif operation == "extrude_rectangle":
|
||||
width = resolve_value(feature["width"], values, f"{feature['id']}.width")
|
||||
depth = resolve_value(feature["depth"], values, f"{feature['id']}.depth")
|
||||
height = resolve_value(feature["height"], values, f"{feature['id']}.height")
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
tool = Pos(*map(float, center)) * Box(
|
||||
width,
|
||||
depth,
|
||||
height,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
shape = tool if shape is None else shape + tool
|
||||
elif operation in {"through_hole", "polar_hole_pattern"}:
|
||||
if shape is None:
|
||||
raise DesignIRError(f"{feature['id']} requires a host solid")
|
||||
diameter = resolve_value(
|
||||
feature["diameter"], values, f"{feature['id']}.diameter"
|
||||
)
|
||||
bounds = shape.bounding_box()
|
||||
cutter_height = float(bounds.size.Z) + 2.0
|
||||
cutter_z = float(bounds.min.Z) - 1.0
|
||||
centers: list[tuple[float, float]]
|
||||
if operation == "through_hole":
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
round(resolve_value(feature["count"], values, f"{feature['id']}.count"))
|
||||
)
|
||||
pitch = resolve_value(
|
||||
feature["pitch_diameter"],
|
||||
values,
|
||||
f"{feature['id']}.pitch_diameter",
|
||||
)
|
||||
if count < 1:
|
||||
raise DesignIRError(f"{feature['id']}.count must be positive")
|
||||
centers = [
|
||||
(
|
||||
math.cos(2.0 * math.pi * index / count) * pitch / 2.0,
|
||||
math.sin(2.0 * math.pi * index / count) * pitch / 2.0,
|
||||
)
|
||||
for index in range(count)
|
||||
]
|
||||
for x, y in centers:
|
||||
cutter = Pos(x, y, cutter_z) * Cylinder(
|
||||
diameter / 2.0,
|
||||
cutter_height,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
shape = shape - cutter
|
||||
|
||||
if shape is None:
|
||||
raise DesignIRError("DesignIR produced no geometry")
|
||||
result = Part(shape.wrapped)
|
||||
if not list(result.solids()) or result.volume <= 0:
|
||||
raise DesignIRError("DesignIR did not produce a valid solid")
|
||||
return result
|
||||
|
||||
|
||||
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
f"DesignIR is not independently rebuildable; missing capabilities: {missing}"
|
||||
)
|
||||
simplecad_src = Path(__file__).resolve().parents[2] / "SimpleCADAPI" / "src"
|
||||
if str(simplecad_src) not in sys.path:
|
||||
sys.path.insert(0, str(simplecad_src))
|
||||
try:
|
||||
import simplecadapi as scad
|
||||
except ImportError as exc:
|
||||
raise DesignIRError("SimpleCADAPI backend is unavailable") from exc
|
||||
|
||||
values = parameter_values(payload)
|
||||
shape: Any = None
|
||||
z_min: float | None = None
|
||||
z_max: float | None = None
|
||||
with scad.GraphSession() as session:
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
if operation in {"extrude_circle", "add_cylinder"}:
|
||||
radius = resolve_value(
|
||||
feature["radius"], values, f"{feature['id']}.radius"
|
||||
)
|
||||
height = resolve_value(
|
||||
feature["height"], values, f"{feature['id']}.height"
|
||||
)
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
point = tuple(map(float, center))
|
||||
tool = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=point,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
shape = (
|
||||
tool
|
||||
if shape is None
|
||||
else scad.union_rsolid([shape, tool], clean=True)
|
||||
)
|
||||
z_min = point[2] if z_min is None else min(z_min, point[2])
|
||||
z_max = (
|
||||
point[2] + height
|
||||
if z_max is None
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation == "extrude_rectangle":
|
||||
width = resolve_value(
|
||||
feature["width"], values, f"{feature['id']}.width"
|
||||
)
|
||||
depth = resolve_value(
|
||||
feature["depth"], values, f"{feature['id']}.depth"
|
||||
)
|
||||
height = resolve_value(
|
||||
feature["height"], values, f"{feature['id']}.height"
|
||||
)
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
point = tuple(map(float, center))
|
||||
tool = scad.make_box_rsolid(
|
||||
width=width,
|
||||
height=depth,
|
||||
depth=height,
|
||||
bottom_face_center=point,
|
||||
)
|
||||
shape = (
|
||||
tool
|
||||
if shape is None
|
||||
else scad.union_rsolid([shape, tool], clean=True)
|
||||
)
|
||||
z_min = point[2] if z_min is None else min(z_min, point[2])
|
||||
z_max = (
|
||||
point[2] + height
|
||||
if z_max is None
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation in {"through_hole", "polar_hole_pattern"}:
|
||||
if shape is None or z_min is None or z_max is None:
|
||||
raise DesignIRError(f"{feature['id']} requires a host solid")
|
||||
diameter = resolve_value(
|
||||
feature["diameter"], values, f"{feature['id']}.diameter"
|
||||
)
|
||||
if operation == "through_hole":
|
||||
center = feature.get("center", [0, 0, 0])
|
||||
_vector3(center, f"{feature['id']}.center")
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
round(
|
||||
resolve_value(
|
||||
feature["count"], values, f"{feature['id']}.count"
|
||||
)
|
||||
)
|
||||
)
|
||||
pitch = resolve_value(
|
||||
feature["pitch_diameter"],
|
||||
values,
|
||||
f"{feature['id']}.pitch_diameter",
|
||||
)
|
||||
if count < 1:
|
||||
raise DesignIRError(
|
||||
f"{feature['id']}.count must be positive"
|
||||
)
|
||||
centers = [
|
||||
(
|
||||
math.cos(2.0 * math.pi * index / count) * pitch / 2.0,
|
||||
math.sin(2.0 * math.pi * index / count) * pitch / 2.0,
|
||||
)
|
||||
for index in range(count)
|
||||
]
|
||||
cutter_height = z_max - z_min + 2.0
|
||||
cutters = [
|
||||
scad.make_cylinder_rsolid(
|
||||
radius=diameter / 2.0,
|
||||
height=cutter_height,
|
||||
bottom_face_center=(x, y, z_min - 1.0),
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
)
|
||||
for x, y in centers
|
||||
]
|
||||
shape = scad.cut_rsolid(
|
||||
shape, cutters, skip_non_intersecting=False
|
||||
)
|
||||
print(
|
||||
f"simplecad grounding {feature['id']}: volume={shape.get_volume():.6f}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
model_json = scad.export_model_json(session=session, indent=2)
|
||||
if shape is None or shape.get_volume() <= 0:
|
||||
raise DesignIRError("DesignIR did not produce a valid SimpleCAD solid")
|
||||
return shape, model_json
|
||||
|
||||
|
||||
def geometry_facts(shape: Any) -> dict[str, Any]:
|
||||
bounds = shape.bounding_box()
|
||||
center = shape.center(CenterOf.MASS)
|
||||
return {
|
||||
"solid_count": len(list(shape.solids())),
|
||||
"face_count": len(list(shape.faces())),
|
||||
"edge_count": len(list(shape.edges())),
|
||||
"vertex_count": len(list(shape.vertices())),
|
||||
"bounds_min_mm": [float(bounds.min.X), float(bounds.min.Y), float(bounds.min.Z)],
|
||||
"bounds_max_mm": [float(bounds.max.X), float(bounds.max.Y), float(bounds.max.Z)],
|
||||
"size_mm": [float(bounds.size.X), float(bounds.size.Y), float(bounds.size.Z)],
|
||||
"volume_mm3": float(shape.volume),
|
||||
"center_of_mass_mm": [float(center.X), float(center.Y), float(center.Z)],
|
||||
}
|
||||
|
||||
|
||||
def compile_designir(
|
||||
payload: dict[str, Any], output: Path, backend: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
selected_backend = backend or str(payload.get("backend_hint", "build123d"))
|
||||
output = output.expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
model_json_path: Path | None = None
|
||||
if selected_backend == "build123d":
|
||||
shape = build_shape(payload)
|
||||
export_step(shape, output)
|
||||
elif selected_backend == "simplecadapi":
|
||||
shape, model_json = build_simplecad_shape(payload)
|
||||
from simplecadapi import export_step as simplecad_export_step
|
||||
|
||||
simplecad_export_step(shapes=shape, filename=str(output))
|
||||
model_json_path = output.with_suffix(".simplecad.model.json")
|
||||
model_json_path.write_text(model_json, encoding="utf-8")
|
||||
else:
|
||||
raise DesignIRError(f"Unsupported DesignIR compiler backend: {selected_backend}")
|
||||
rebuilt = import_step(output)
|
||||
return {
|
||||
"output": str(output),
|
||||
"backend": selected_backend,
|
||||
"model_json": str(model_json_path) if model_json_path else None,
|
||||
"facts": geometry_facts(rebuilt),
|
||||
}
|
||||
|
||||
|
||||
def anti_cheat(payload: dict[str, Any], generator: Path | None = None) -> dict[str, Any]:
|
||||
validate_designir(payload)
|
||||
violations: list[str] = []
|
||||
forbidden = find_forbidden(payload)
|
||||
if forbidden:
|
||||
violations.append(f"DesignIR teacher dependency at {forbidden}")
|
||||
if generator:
|
||||
text = generator.read_text(encoding="utf-8").lower()
|
||||
for token in FORBIDDEN_TEXT + ("breptools", "stl", "trimesh"):
|
||||
if token in text:
|
||||
violations.append(f"Generator contains forbidden token {token}")
|
||||
return {"valid": not violations, "violations": violations}
|
||||
|
||||
|
||||
def isolated_rebuild(
|
||||
designir_path: Path, output: Path, backend: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
payload = read_json(designir_path)
|
||||
guard = anti_cheat(payload)
|
||||
if not guard["valid"]:
|
||||
raise DesignIRError("; ".join(guard["violations"]))
|
||||
with tempfile.TemporaryDirectory(prefix="designir-isolated-") as temporary:
|
||||
isolated_root = Path(temporary)
|
||||
isolated_designir = isolated_root / "model.designir.json"
|
||||
write_json(isolated_designir, payload)
|
||||
isolated_payload = read_json(isolated_designir)
|
||||
result = compile_designir(
|
||||
isolated_payload, isolated_root / "rebuilt.step", backend=backend
|
||||
)
|
||||
final_output = output.expanduser().resolve()
|
||||
final_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(result["output"], final_output)
|
||||
final_model_json: Path | None = None
|
||||
if result.get("model_json"):
|
||||
final_model_json = final_output.with_suffix(".simplecad.model.json")
|
||||
shutil.copy2(str(result["model_json"]), final_model_json)
|
||||
return {
|
||||
"valid": True,
|
||||
"teacher_step_available": False,
|
||||
"designir_only": True,
|
||||
"backend": result["backend"],
|
||||
"output": str(final_output),
|
||||
"model_json": str(final_model_json) if final_model_json else None,
|
||||
"facts": geometry_facts(import_step(final_output)),
|
||||
}
|
||||
|
||||
|
||||
def _max_axis_error(first: list[float], second: list[float]) -> float:
|
||||
return max(abs(a - b) for a, b in zip(first, second))
|
||||
|
||||
|
||||
def _distance(first: list[float], second: list[float]) -> float:
|
||||
return math.sqrt(sum((a - b) ** 2 for a, b in zip(first, second)))
|
||||
|
||||
|
||||
def _symmetric_difference_volume(first: Part, second: Part) -> float:
|
||||
try:
|
||||
return float((first - second).volume + (second - first).volume)
|
||||
except Exception as exc: # OpenCascade failures become an explicit metric state.
|
||||
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
|
||||
|
||||
|
||||
def acceptance_report(
|
||||
teacher_path: Path,
|
||||
rebuilt_path: Path,
|
||||
payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
validate_designir(payload)
|
||||
teacher = import_step(teacher_path.expanduser().resolve())
|
||||
rebuilt = import_step(rebuilt_path.expanduser().resolve())
|
||||
teacher_facts = geometry_facts(teacher)
|
||||
rebuilt_facts = geometry_facts(rebuilt)
|
||||
volume_base = max(teacher_facts["volume_mm3"], 1e-12)
|
||||
symmetric_difference = _symmetric_difference_volume(teacher, rebuilt)
|
||||
geometry = {
|
||||
"teacher": teacher_facts,
|
||||
"rebuilt": rebuilt_facts,
|
||||
"relative_volume_error": abs(
|
||||
rebuilt_facts["volume_mm3"] - teacher_facts["volume_mm3"]
|
||||
)
|
||||
/ volume_base,
|
||||
"center_of_mass_error_mm": _distance(
|
||||
rebuilt_facts["center_of_mass_mm"],
|
||||
teacher_facts["center_of_mass_mm"],
|
||||
),
|
||||
"bounding_box_axis_error_mm": _max_axis_error(
|
||||
rebuilt_facts["size_mm"], teacher_facts["size_mm"]
|
||||
),
|
||||
"symmetric_difference_volume_mm3": symmetric_difference,
|
||||
"symmetric_difference_ratio": symmetric_difference / volume_base,
|
||||
"topology_count_delta": {
|
||||
key: rebuilt_facts[key] - teacher_facts[key]
|
||||
for key in ("solid_count", "face_count", "edge_count", "vertex_count")
|
||||
},
|
||||
}
|
||||
|
||||
perturbations: list[dict[str, Any]] = []
|
||||
with tempfile.TemporaryDirectory(prefix="designir-edits-") as temporary:
|
||||
for index, test in enumerate(
|
||||
payload["validation_contract"].get("perturbations", [])
|
||||
):
|
||||
modified = copy.deepcopy(payload)
|
||||
name = test["parameter"]
|
||||
original = float(modified["parameters"][name]["value"])
|
||||
modified["parameters"][name]["value"] = original * float(test["scale"])
|
||||
try:
|
||||
output = Path(temporary) / f"perturbation-{index}.step"
|
||||
result = compile_designir(modified, output)
|
||||
perturbations.append(
|
||||
{
|
||||
"parameter": name,
|
||||
"original_value": original,
|
||||
"modified_value": modified["parameters"][name]["value"],
|
||||
"expected_change": test.get("expected_change"),
|
||||
"preserve": test.get("preserve", []),
|
||||
"success": result["facts"]["solid_count"] > 0,
|
||||
"facts": result["facts"],
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
perturbations.append(
|
||||
{"parameter": name, "success": False, "error": str(exc)}
|
||||
)
|
||||
success_rate = (
|
||||
sum(1 for item in perturbations if item["success"]) / len(perturbations)
|
||||
if perturbations
|
||||
else 1.0
|
||||
)
|
||||
thresholds = payload["validation_contract"].get("thresholds", {})
|
||||
threshold_checks = {
|
||||
"relative_volume_error": geometry["relative_volume_error"]
|
||||
<= float(thresholds.get("relative_volume_error_max", 0.01)),
|
||||
"center_of_mass_error_mm": geometry["center_of_mass_error_mm"]
|
||||
<= float(thresholds.get("center_of_mass_error_mm_max", 0.1)),
|
||||
"bounding_box_axis_error_mm": geometry["bounding_box_axis_error_mm"]
|
||||
<= float(thresholds.get("bounding_box_axis_error_mm_max", 0.1)),
|
||||
"symmetric_difference_ratio": geometry["symmetric_difference_ratio"]
|
||||
<= float(thresholds.get("symmetric_difference_ratio_max", 0.02)),
|
||||
"parameter_perturbation_success_rate": success_rate
|
||||
>= float(thresholds.get("parameter_perturbation_success_rate_min", 1.0)),
|
||||
}
|
||||
passed = all(threshold_checks.values())
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "independent_designir_acceptance",
|
||||
"model_id": payload["model_id"],
|
||||
"geometry": geometry,
|
||||
"feature_semantics": {
|
||||
"declared_feature_count": len(payload["features"]),
|
||||
"declared_constraint_count": len(payload["constraints"]),
|
||||
"unsupported_capabilities": payload.get("missing_capabilities", []),
|
||||
},
|
||||
"editability": {
|
||||
"editable_parameter_count": len(
|
||||
payload["edit_interface"]["editable_parameters"]
|
||||
),
|
||||
"perturbations": perturbations,
|
||||
"parameter_perturbation_success_rate": success_rate,
|
||||
},
|
||||
"threshold_checks": threshold_checks,
|
||||
"verdict": "pass" if passed else "fail",
|
||||
"next_state": "replay_validated" if passed else "quarantine",
|
||||
}
|
||||
|
||||
|
||||
def promotion_check(reports: list[Path], policy_path: Path) -> dict[str, Any]:
|
||||
policy = read_json(policy_path)
|
||||
loaded = [read_json(path) for path in reports]
|
||||
model_ids = {str(item.get("model_id")) for item in loaded}
|
||||
passing = [item for item in loaded if item.get("next_state") == "replay_validated"]
|
||||
minimum = int(policy["minimum_independent_cases"])
|
||||
eligible = len(passing) >= minimum and len(model_ids) >= minimum
|
||||
return {
|
||||
"eligible": eligible,
|
||||
"independent_case_count": len(model_ids),
|
||||
"passing_case_count": len(passing),
|
||||
"required_case_count": minimum,
|
||||
"next_state": "promoted" if eligible else "candidate",
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
validate = commands.add_parser("validate")
|
||||
validate.add_argument("designir", type=Path)
|
||||
|
||||
guard = commands.add_parser("anti-cheat")
|
||||
guard.add_argument("designir", type=Path)
|
||||
guard.add_argument("--generator", type=Path)
|
||||
|
||||
compile_command = commands.add_parser("compile")
|
||||
compile_command.add_argument("designir", type=Path)
|
||||
compile_command.add_argument("--output", type=Path, required=True)
|
||||
compile_command.add_argument(
|
||||
"--backend", choices=("build123d", "simplecadapi")
|
||||
)
|
||||
|
||||
isolated = commands.add_parser("isolated-rebuild")
|
||||
isolated.add_argument("designir", type=Path)
|
||||
isolated.add_argument("--output", type=Path, required=True)
|
||||
isolated.add_argument("--backend", choices=("build123d", "simplecadapi"))
|
||||
|
||||
accept = commands.add_parser("accept")
|
||||
accept.add_argument("--teacher", type=Path, required=True)
|
||||
accept.add_argument("--designir", type=Path, required=True)
|
||||
accept.add_argument("--rebuilt", type=Path, required=True)
|
||||
accept.add_argument("--report", type=Path, required=True)
|
||||
|
||||
promote = commands.add_parser("promotion-check")
|
||||
promote.add_argument("reports", type=Path, nargs="+")
|
||||
promote.add_argument("--policy", type=Path, required=True)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
if args.command == "validate":
|
||||
payload = validate_designir(read_json(args.designir))
|
||||
result = {
|
||||
"valid": True,
|
||||
"model_id": payload["model_id"],
|
||||
"supported_operations": sorted(SUPPORTED_OPERATIONS),
|
||||
}
|
||||
elif args.command == "anti-cheat":
|
||||
result = anti_cheat(read_json(args.designir), args.generator)
|
||||
elif args.command == "compile":
|
||||
result = compile_designir(
|
||||
read_json(args.designir), args.output, backend=args.backend
|
||||
)
|
||||
elif args.command == "isolated-rebuild":
|
||||
result = isolated_rebuild(
|
||||
args.designir, args.output, backend=args.backend
|
||||
)
|
||||
elif args.command == "accept":
|
||||
result = acceptance_report(
|
||||
args.teacher, args.rebuilt, read_json(args.designir)
|
||||
)
|
||||
write_json(args.report, result)
|
||||
else:
|
||||
result = promotion_check(args.reports, args.policy)
|
||||
except (DesignIRError, OSError) as exc:
|
||||
print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False, indent=2))
|
||||
return 2
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result.get("valid", result.get("eligible", True)) is not False else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: cad-experience-builder
|
||||
description: Batch-extract STEP files into private JSON, use the active large model to semantically distill transferable CAD methods from a sanitized batch, and deterministically verify and publish a backend-neutral experience library. Use when distilling STEP/STP corpora, reviewing candidate methods, inducing feature motifs and constraints, auditing instance-data leakage, or preparing promoted experience for cad-router and CAD backends.
|
||||
---
|
||||
|
||||
# CAD Experience Builder
|
||||
|
||||
Induce methods, not copied answers. Keep source-part evidence outside the
|
||||
published library and require repeated support before promoting any experience.
|
||||
|
||||
## Required workflow
|
||||
|
||||
1. Put source `.step` or `.stp` files in
|
||||
`workspace/teacher/inbox/`. Only deterministic ingestion and Agent B may
|
||||
access this vault.
|
||||
2. Parse every new, content-unique STEP into visible private JSON under
|
||||
`workspace/teacher/evidence/`:
|
||||
|
||||
```bash
|
||||
../../scripts/cad-experience extract-folder
|
||||
```
|
||||
|
||||
3. Prepare a sanitized semantic batch:
|
||||
|
||||
```bash
|
||||
../../scripts/cad-experience prepare
|
||||
```
|
||||
|
||||
4. Read `workspace/promotion/candidates/semantic-batch.json` completely. Act as the semantic
|
||||
distiller: compare families, parameter roles, datum roles, canonical
|
||||
reconstruction stages, feature cardinality classes, relations, prior
|
||||
vocabulary, failure rules, and validation targets; then author
|
||||
`workspace/promotion/candidates/experience-draft.json` using
|
||||
`references/llm-review-contract.md`.
|
||||
|
||||
Propose methods, invariants, and family-level `reconstruction_grammar`
|
||||
records, not raw co-occurrence pairs. A reconstruction grammar is a
|
||||
canonical backend-neutral strategy inferred from repeated geometry; it is
|
||||
not the source model's recovered feature history. Do not infer
|
||||
manufacturing intent or failure causes that final B-Rep evidence cannot
|
||||
support.
|
||||
|
||||
5. Publish only after the model draft exists:
|
||||
|
||||
```bash
|
||||
../../scripts/cad-experience publish \
|
||||
--draft ../../workspace/promotion/candidates/experience-draft.json
|
||||
```
|
||||
|
||||
The publisher recomputes support from the private corpus, rejects numeric
|
||||
instance answers, keeps weak proposals as candidates, and marks the library
|
||||
router-consumable only after this verification.
|
||||
|
||||
6. Audit the public library. Treat any failure as a hard publishing failure:
|
||||
|
||||
```bash
|
||||
python scripts/cad_experience.py audit \
|
||||
../../workspace/promotion/promoted/library.json
|
||||
```
|
||||
|
||||
7. Query only the published library for creation, modification, or rebuilding:
|
||||
|
||||
```bash
|
||||
python scripts/cad_experience.py query \
|
||||
../../workspace/promotion/promoted/library.json \
|
||||
--family flanged_hub_adapter \
|
||||
--features base_flange,hollow_sleeve,counterbore
|
||||
```
|
||||
|
||||
8. Pass the query result, never the teacher vault, unrelated private evidence,
|
||||
or a case JSON,
|
||||
to `$cad-router` or a backend.
|
||||
|
||||
Direct `distill` without an LLM-authored draft is intentionally blocked. The
|
||||
active Codex model supplies semantic judgment; no separate web API key is
|
||||
required.
|
||||
|
||||
## STEP extraction boundary
|
||||
|
||||
- The V2 extractor reads analytic B-Rep surfaces, bounding boxes, topology
|
||||
counts, axes, radii, and locations into a private evidence record.
|
||||
- It derives scale-independent body classes, part families, rotational and
|
||||
prismatic motifs, passage and pattern relationships, complexity roles, and
|
||||
normalized ratio observations before sanitization.
|
||||
- It also derives private exact cardinality evidence plus sanitized symbolic
|
||||
parameter roles, datum roles, feature-count classes, and canonical
|
||||
reconstruction stages. These are the evidence used to learn how to rebuild a
|
||||
family without publishing any teacher dimensions.
|
||||
- Exact source paths, hashes, dimensions, coordinates, and face references are
|
||||
permitted in private case JSON and forbidden in the published library.
|
||||
- Keep input/output as a human-visible staging area and induction corpus. Never
|
||||
use it directly during creation, modification, or reconstruction.
|
||||
- A semantic candidate is not a trusted design method until repeated,
|
||||
independently sourced cases satisfy the promotion policy.
|
||||
|
||||
## Promotion rules
|
||||
|
||||
- One case is evidence, not experience.
|
||||
- Publish generalized, dimension-free observations below threshold as visible
|
||||
`candidate_experiences`, but never expose them to generation consumers.
|
||||
- Require `min_support` independent cases and conditional `min_confidence`
|
||||
within the proposal's semantic context before promotion.
|
||||
- Promote feature compositions, semantic constraints, dimensionless
|
||||
distributions, validation checks, and failure-repair methods.
|
||||
- Reject absolute dimensions, coordinates, face references, source paths,
|
||||
source hashes, and full parameter dictionaries from published experience.
|
||||
- Keep generated examples out of the trusted corpus until independently
|
||||
validated.
|
||||
- Evaluate generalization on held-out cases that were not used for induction.
|
||||
|
||||
## Backend contract
|
||||
|
||||
The library is backend-neutral. A consumer receives:
|
||||
|
||||
- `feature_motif`: reusable compositions of feature types;
|
||||
- `constraint`: invariant semantic relationships;
|
||||
- `design_rule`: conditional modeling guidance;
|
||||
- `dimensionless_distribution`: multi-case ratio ranges;
|
||||
- `validation_rule`: checks independent of one part's coordinates;
|
||||
- `failure_repair`: reusable detection and repair methods.
|
||||
- `reconstruction_grammar`: symbolic parameters, datums, canonical feature
|
||||
stages, optional/required feature roles, and validation roles for a family.
|
||||
|
||||
build123d, SimpleCADAPI, or another backend translates the same queried
|
||||
experience into its own operations. Experience must never contain executable
|
||||
teacher geometry.
|
||||
|
||||
## References
|
||||
|
||||
- Read `references/case-input-contract.md` before producing batch input.
|
||||
- Read `references/experience-library-schema.md` before extending the output
|
||||
schema or adding a consumer.
|
||||
- Read `references/llm-review-contract.md` before authoring or revising a
|
||||
semantic proposal draft.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Case JSON input contract
|
||||
|
||||
Case JSON is private evidence used only during induction. It may contain exact
|
||||
parameters, but the inducer never copies those fields into published experience.
|
||||
Store source STEP files in `workspace/teacher/inbox` and generated private
|
||||
evidence in `workspace/teacher/evidence`.
|
||||
|
||||
Do not pass either folder or a case JSON to cad-router or a CAD backend.
|
||||
Creation, modification, and reconstruction may consume only the promoted public
|
||||
experience library.
|
||||
|
||||
Supported fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"provenance": {"source_sha256": "..."},
|
||||
"design_ir": {
|
||||
"part_family": "flanged_hub_adapter",
|
||||
"features": [{"type": "base_flange"}, {"type": "hollow_sleeve"}],
|
||||
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
|
||||
"reconstruction_evidence": {
|
||||
"parameter_roles": ["overall_long_span", "coaxial_diameter_roles"],
|
||||
"datum_roles": ["part_center", "primary_axis"],
|
||||
"feature_roles": ["rotational_body", "coaxial_cylindrical_stack"],
|
||||
"relation_roles": ["coaxial_stack"],
|
||||
"canonical_stages": [
|
||||
{
|
||||
"id": "establish_reference_frame",
|
||||
"operation": "define_datums",
|
||||
"feature_roles": [],
|
||||
"reference_roles": ["part_center", "primary_axis"]
|
||||
}
|
||||
],
|
||||
"private_cardinality_evidence": {
|
||||
"feature_role_count": 5,
|
||||
"feature_role_count_class": "repeated"
|
||||
},
|
||||
"validation_roles": ["closed_solid", "coaxial_stack"]
|
||||
},
|
||||
"normalized_observations": [
|
||||
{
|
||||
"name": "sleeve_od_to_base_od",
|
||||
"value": 0.35,
|
||||
"numerator_role": "sleeve_outer_diameter",
|
||||
"denominator_role": "base_outer_diameter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"experience": {
|
||||
"rules": [
|
||||
{
|
||||
"id": "bound_counterbore_to_host",
|
||||
"scope": ["counterbore"],
|
||||
"statement": "Terminate a counterbore at its host boundary.",
|
||||
"repair": "Bind the cutter extent to the host feature."
|
||||
}
|
||||
],
|
||||
"validation_targets": [
|
||||
{
|
||||
"id": "counterbore_host_boundary",
|
||||
"check": "A counterbore does not cross an unrelated adjacent feature."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules and checks containing numeric literals are rejected because they are
|
||||
likely instance answers. Ratios are accepted only through
|
||||
`normalized_observations`, with an explicit numerator and denominator role.
|
||||
|
||||
Legacy distilled JSON can be used when it contains the fields above. Exact
|
||||
`parameters`, `evidence`, centers, axes, and surface references are ignored.
|
||||
|
||||
V3 extractor output should also provide several independent semantic signals
|
||||
per case: body-aspect class, topology-complexity role, analytic feature
|
||||
networks, part family, relationship candidates, and role-named dimensionless
|
||||
observations. It additionally provides symbolic reconstruction roles, canonical
|
||||
reconstruction stages, and private exact cardinality evidence. Sanitization
|
||||
converts exact counts into qualitative classes before LLM review. Do not encode
|
||||
an absolute source dimension as a feature token.
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# Published experience library
|
||||
|
||||
The published library uses schema version `2.0` and contains only promoted,
|
||||
generalized experience:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"induction_mode": "llm_proposals_with_deterministic_evidence_verification",
|
||||
"policy": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"llm_semantic_review_required": true,
|
||||
"draft_evidence_verified": true,
|
||||
"router_consumable": true,
|
||||
"minimum_support": 20
|
||||
},
|
||||
"corpus_summary": {
|
||||
"accepted_case_count": 1000,
|
||||
"duplicate_case_count": 12
|
||||
},
|
||||
"experience_summary": {
|
||||
"llm_proposal_count": 24,
|
||||
"promoted_experience_count": 18,
|
||||
"candidate_experience_count": 6,
|
||||
"by_kind": [{"kind": "feature_motif", "count": 7}],
|
||||
"by_scope": [{"scope": "flanged_hub_adapter", "count": 5}]
|
||||
},
|
||||
"experiences": [
|
||||
{
|
||||
"id": "motif.base_flange+hollow_sleeve",
|
||||
"kind": "feature_motif",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"when": {"features": ["base_flange", "hollow_sleeve"]},
|
||||
"guidance": "Treat these feature roles as a reusable composition.",
|
||||
"support": 243,
|
||||
"confidence": 0.91
|
||||
}
|
||||
],
|
||||
"candidate_experiences": [
|
||||
{
|
||||
"id": "motif.flanged_hub_adapter.base_flange+hollow_sleeve",
|
||||
"kind": "feature_motif",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"when": {"features": ["base_flange", "hollow_sleeve"]},
|
||||
"support": 7,
|
||||
"required_support": 20,
|
||||
"remaining_support": 13,
|
||||
"promotion_state": "candidate",
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Allowed numeric values describe evidence strength or dimensionless
|
||||
distributions: support counts, confidence, frequency, percentiles, and sample
|
||||
counts. Absolute lengths, diameters, heights, radii, angles tied to a single
|
||||
case, positions, and coordinates are forbidden.
|
||||
|
||||
Consumers query this library by requested family and feature vocabulary. They
|
||||
must not receive the induction corpus, private case JSON, or
|
||||
`candidate_experiences`. Only `experiences` is executable guidance.
|
||||
|
||||
## Reconstruction grammar
|
||||
|
||||
`reconstruction_grammar` is the executable bridge between generalized
|
||||
experience and a backend-neutral design plan. It may contain only:
|
||||
|
||||
- symbolic `parameter_roles`;
|
||||
- symbolic `datum_roles`;
|
||||
- required or optional semantic feature and relation roles;
|
||||
- canonical stages with tokenized operation, feature, and reference roles;
|
||||
- qualitative cardinality classes such as `single`, `paired`, `repeated`, or
|
||||
`dense`;
|
||||
- validation roles.
|
||||
|
||||
It must not contain parameter values, exact member counts, absolute
|
||||
coordinates, source identifiers, face references, or executable teacher
|
||||
geometry. The publisher verifies that every promoted role and stage is present
|
||||
in enough independent private cases. A consumer instantiates the grammar using
|
||||
the user's dimensions, inferred dimensionless distributions, and backend
|
||||
operations.
|
||||
@@ -0,0 +1,97 @@
|
||||
# LLM semantic review contract
|
||||
|
||||
Read the sanitized `work/review/semantic-batch.json`, not raw STEP geometry.
|
||||
Maintain stable proposal identifiers when an existing candidate expresses the
|
||||
same method.
|
||||
|
||||
Write `work/review/experience-draft.json` with this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [
|
||||
{
|
||||
"id": "constraint.part_family.shared_axis",
|
||||
"kind": "constraint",
|
||||
"scope": ["part_family"],
|
||||
"evidence_query": {
|
||||
"required_features": ["rotational_body", "central_passage"],
|
||||
"required_relations": ["dominant_axis_alignment"],
|
||||
"context_features": ["rotational_body", "central_passage"]
|
||||
},
|
||||
"guidance": "Preserve a shared axis across the participating feature roles.",
|
||||
"semantic_rationale": "The relationship remains meaningful when individual dimensions change.",
|
||||
"check": "Confirm that participating features retain their intended common axis."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Allowed kinds are `feature_motif`, `constraint`, `design_rule`,
|
||||
`validation_rule`, `failure_repair`, `dimensionless_distribution`, and
|
||||
`reconstruction_grammar`.
|
||||
Distribution proposals also provide `observation_name`; the publisher computes
|
||||
the distribution from private evidence and never accepts values from the draft.
|
||||
|
||||
A `reconstruction_grammar` proposal must additionally contain symbolic,
|
||||
number-free reconstruction content:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "reconstruction.patterned_prismatic_part.primary",
|
||||
"kind": "reconstruction_grammar",
|
||||
"scope": ["patterned_prismatic_part"],
|
||||
"evidence_query": {
|
||||
"required_features": ["planar_dominant_body", "cylindrical_feature_network"],
|
||||
"required_relations": [],
|
||||
"context_features": ["planar_dominant_body"]
|
||||
},
|
||||
"guidance": "Build a datum-driven envelope before adding the passage network and repeated features.",
|
||||
"semantic_rationale": "The staged method transfers across dimensional variants while preserving feature relationships.",
|
||||
"reconstruction_grammar": {
|
||||
"parameter_roles": ["overall_long_span", "overall_middle_span", "overall_short_span"],
|
||||
"datum_roles": ["part_center", "primary_axis", "primary_transverse_plane"],
|
||||
"feature_roles": ["planar_dominant_body", "cylindrical_feature_network"],
|
||||
"relation_roles": [],
|
||||
"canonical_stages": [
|
||||
{
|
||||
"id": "establish_reference_frame",
|
||||
"operation": "define_datums",
|
||||
"feature_roles": [],
|
||||
"reference_roles": ["part_center", "primary_axis"]
|
||||
},
|
||||
{
|
||||
"id": "construct_primary_envelope",
|
||||
"operation": "extrude_profile",
|
||||
"feature_roles": ["planar_dominant_body"],
|
||||
"reference_roles": ["part_center", "primary_axis"]
|
||||
}
|
||||
],
|
||||
"cardinality_classes": ["plane_dense", "cylinder_dense"],
|
||||
"validation_roles": ["closed_solid", "surface_mix_consistency"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Follow these rules:
|
||||
|
||||
- Require an evidence query composed only of semantic feature and relation
|
||||
tokens present in the sanitized batch.
|
||||
- Use scopes to prevent unrelated part families from supporting a proposal.
|
||||
- Use `context_features` to define the conditional population used for
|
||||
confidence, so a valid subtype method is not diluted by unrelated variants
|
||||
in the same broad family.
|
||||
- Explain why a proposal transfers across dimensions and variants.
|
||||
- Avoid dimensions, coordinates, source identifiers, face references, and
|
||||
numeric literals in prose.
|
||||
- Do not infer build order or causal failure mechanisms from final geometry.
|
||||
- Treat `canonical_stages` as a recommended reconstruction order, never as a
|
||||
claim about the source CAD history.
|
||||
- For reconstruction grammars, retain only roles and stages repeated by the
|
||||
scoped evidence. Do not insert family knowledge that the sanitized batch
|
||||
does not support.
|
||||
- Prefer a small set of meaningful methods over every possible feature pair.
|
||||
- Preserve earlier proposal IDs when evidence still supports the same meaning.
|
||||
- Let the publisher calculate support and confidence; never claim them in the
|
||||
draft.
|
||||
+1258
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,767 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a private, backend-neutral case record from an analytic STEP model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
EXTRACTOR_VERSION = "3.0"
|
||||
|
||||
|
||||
def _rounded(value: float) -> float:
|
||||
return round(float(value), 6)
|
||||
|
||||
|
||||
def _component(value: Any, name: str) -> float:
|
||||
component = getattr(value, name)
|
||||
return float(component() if callable(component) else component)
|
||||
|
||||
|
||||
def _point(value: Any) -> list[float]:
|
||||
return [_rounded(_component(value, name)) for name in ("X", "Y", "Z")]
|
||||
|
||||
|
||||
def _direction(value: Any) -> list[float]:
|
||||
return _point(value)
|
||||
|
||||
|
||||
def _canonical_axis(direction: list[float]) -> str:
|
||||
index = max(range(3), key=lambda item: abs(direction[item]))
|
||||
return ("x", "y", "z")[index]
|
||||
|
||||
|
||||
def _radial_offset(location: list[float], center: list[float], axis: str) -> float:
|
||||
axes = {"x": (1, 2), "y": (0, 2), "z": (0, 1)}
|
||||
first, second = axes[axis]
|
||||
return math.hypot(
|
||||
location[first] - center[first],
|
||||
location[second] - center[second],
|
||||
)
|
||||
|
||||
def _ratio(numerator: float, denominator: float) -> float:
|
||||
return _rounded(numerator / denominator) if denominator > 1e-9 else 0.0
|
||||
|
||||
|
||||
def _observation(
|
||||
name: str, value: float, numerator_role: str, denominator_role: str
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"value": _rounded(value),
|
||||
"numerator_role": numerator_role,
|
||||
"denominator_role": denominator_role,
|
||||
}
|
||||
|
||||
|
||||
def _cardinality_class(count: int) -> str:
|
||||
if count <= 0:
|
||||
return "absent"
|
||||
if count == 1:
|
||||
return "single"
|
||||
if count == 2:
|
||||
return "paired"
|
||||
if count <= 6:
|
||||
return "repeated"
|
||||
return "dense"
|
||||
|
||||
|
||||
def _surface_record(face: Any, index: int) -> dict[str, Any]:
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import (
|
||||
GeomAbs_Cone,
|
||||
GeomAbs_Cylinder,
|
||||
GeomAbs_Plane,
|
||||
GeomAbs_Sphere,
|
||||
GeomAbs_Torus,
|
||||
)
|
||||
|
||||
adaptor = BRepAdaptor_Surface(face.wrapped)
|
||||
kind = adaptor.GetType()
|
||||
record: dict[str, Any] = {
|
||||
"surface_id": f"face_{index}",
|
||||
"area": _rounded(face.area),
|
||||
"center": _point(face.center()),
|
||||
}
|
||||
if kind == GeomAbs_Plane:
|
||||
plane = adaptor.Plane()
|
||||
record.update(
|
||||
{
|
||||
"type": "plane",
|
||||
"location": _point(plane.Location()),
|
||||
"axis": _direction(plane.Axis().Direction()),
|
||||
}
|
||||
)
|
||||
elif kind == GeomAbs_Cylinder:
|
||||
cylinder = adaptor.Cylinder()
|
||||
record.update(
|
||||
{
|
||||
"type": "cylinder",
|
||||
"location": _point(cylinder.Location()),
|
||||
"axis": _direction(cylinder.Axis().Direction()),
|
||||
"radius": _rounded(cylinder.Radius()),
|
||||
}
|
||||
)
|
||||
elif kind == GeomAbs_Cone:
|
||||
cone = adaptor.Cone()
|
||||
record.update(
|
||||
{
|
||||
"type": "cone",
|
||||
"location": _point(cone.Location()),
|
||||
"axis": _direction(cone.Axis().Direction()),
|
||||
"reference_radius": _rounded(cone.RefRadius()),
|
||||
"semi_angle_radians": _rounded(cone.SemiAngle()),
|
||||
}
|
||||
)
|
||||
elif kind == GeomAbs_Sphere:
|
||||
sphere = adaptor.Sphere()
|
||||
record.update(
|
||||
{
|
||||
"type": "sphere",
|
||||
"location": _point(sphere.Location()),
|
||||
"radius": _rounded(sphere.Radius()),
|
||||
}
|
||||
)
|
||||
elif kind == GeomAbs_Torus:
|
||||
torus = adaptor.Torus()
|
||||
record.update(
|
||||
{
|
||||
"type": "torus",
|
||||
"location": _point(torus.Location()),
|
||||
"axis": _direction(torus.Axis().Direction()),
|
||||
"major_radius": _rounded(torus.MajorRadius()),
|
||||
"minor_radius": _rounded(torus.MinorRadius()),
|
||||
}
|
||||
)
|
||||
else:
|
||||
record["type"] = "other"
|
||||
return record
|
||||
|
||||
|
||||
def _semantic_summary(
|
||||
surfaces: list[dict[str, Any]], bbox_center: list[float], bbox_size: list[float]
|
||||
) -> tuple[str, list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
|
||||
cylinders = [item for item in surfaces if item["type"] == "cylinder"]
|
||||
cones = [item for item in surfaces if item["type"] == "cone"]
|
||||
axis_votes: Counter[str] = Counter()
|
||||
for surface in cylinders + cones:
|
||||
axis_votes[_canonical_axis(surface["axis"])] += max(surface["area"], 1.0)
|
||||
dominant_axis = axis_votes.most_common(1)[0][0] if axis_votes else "z"
|
||||
scale = max(bbox_size) if bbox_size else 1.0
|
||||
sorted_spans = sorted((max(float(value), 1e-9) for value in bbox_size))
|
||||
short_span, middle_span, long_span = sorted_spans
|
||||
long_to_middle = _ratio(long_span, middle_span)
|
||||
middle_to_short = _ratio(middle_span, short_span)
|
||||
short_to_long = _ratio(short_span, long_span)
|
||||
coaxial_tolerance = max(scale * 0.005, 1e-4)
|
||||
|
||||
parallel_cylinders: list[dict[str, Any]] = []
|
||||
coaxial_cylinders: list[dict[str, Any]] = []
|
||||
off_axis_cylinders: list[dict[str, Any]] = []
|
||||
for surface in cylinders:
|
||||
if _canonical_axis(surface["axis"]) != dominant_axis:
|
||||
continue
|
||||
enriched = dict(surface)
|
||||
enriched["radial_offset"] = _rounded(
|
||||
_radial_offset(surface["location"], bbox_center, dominant_axis)
|
||||
)
|
||||
parallel_cylinders.append(enriched)
|
||||
if enriched["radial_offset"] <= coaxial_tolerance:
|
||||
coaxial_cylinders.append(enriched)
|
||||
else:
|
||||
off_axis_cylinders.append(enriched)
|
||||
|
||||
distinct_coaxial_radii = {
|
||||
round(surface["radius"], 4) for surface in coaxial_cylinders
|
||||
}
|
||||
repeated_groups: dict[float, list[dict[str, Any]]] = defaultdict(list)
|
||||
for surface in off_axis_cylinders:
|
||||
repeated_groups[round(surface["radius"], 4)].append(surface)
|
||||
repeated_group_sizes = sorted(
|
||||
(len(rows) for rows in repeated_groups.values()), reverse=True
|
||||
)
|
||||
largest_repeated_group = (
|
||||
max(repeated_groups.values(), key=len) if repeated_groups else []
|
||||
)
|
||||
parallel_cones = [
|
||||
item for item in cones if _canonical_axis(item["axis"]) == dominant_axis
|
||||
]
|
||||
has_rotational_stack = len(distinct_coaxial_radii) >= 2
|
||||
has_repeated_axial_holes = bool(repeated_group_sizes and repeated_group_sizes[0] >= 3)
|
||||
has_multi_axis_passages = any(
|
||||
_canonical_axis(item["axis"]) != dominant_axis for item in cylinders
|
||||
)
|
||||
planes = [item for item in surfaces if item["type"] == "plane"]
|
||||
toruses = [item for item in surfaces if item["type"] == "torus"]
|
||||
spheres = [item for item in surfaces if item["type"] == "sphere"]
|
||||
other_surfaces = [item for item in surfaces if item["type"] == "other"]
|
||||
total_area = sum(max(float(item.get("area", 0.0)), 0.0) for item in surfaces)
|
||||
planar_area = sum(max(float(item.get("area", 0.0)), 0.0) for item in planes)
|
||||
cylindrical_area = sum(
|
||||
max(float(item.get("area", 0.0)), 0.0) for item in cylinders
|
||||
)
|
||||
planar_fraction = _ratio(planar_area, total_area)
|
||||
cylindrical_fraction = _ratio(cylindrical_area, total_area)
|
||||
elongated = long_to_middle >= 3.0
|
||||
plate_like = middle_to_short >= 4.0 and long_to_middle < 3.0
|
||||
compact = long_to_middle < 2.0 and middle_to_short < 2.5
|
||||
|
||||
transverse_axes = {
|
||||
"x": (1, 2),
|
||||
"y": (0, 2),
|
||||
"z": (0, 1),
|
||||
}[dominant_axis]
|
||||
transverse_span = max(bbox_size[index] for index in transverse_axes)
|
||||
dominant_span = bbox_size[{"x": 0, "y": 1, "z": 2}[dominant_axis]]
|
||||
largest_coaxial_radius = max(
|
||||
(surface["radius"] for surface in coaxial_cylinders), default=0.0
|
||||
)
|
||||
smallest_coaxial_radius = min(
|
||||
(surface["radius"] for surface in coaxial_cylinders), default=0.0
|
||||
)
|
||||
|
||||
pattern_offsets = [
|
||||
_radial_offset(surface["location"], bbox_center, dominant_axis)
|
||||
for surface in largest_repeated_group
|
||||
]
|
||||
pattern_mean_offset = (
|
||||
sum(pattern_offsets) / len(pattern_offsets) if pattern_offsets else 0.0
|
||||
)
|
||||
pattern_spread = (
|
||||
max(pattern_offsets) - min(pattern_offsets) if pattern_offsets else 0.0
|
||||
)
|
||||
has_circular_pattern = bool(
|
||||
len(pattern_offsets) >= 3
|
||||
and pattern_mean_offset > coaxial_tolerance
|
||||
and pattern_spread / pattern_mean_offset <= 0.08
|
||||
)
|
||||
|
||||
# Multiple radii sharing a transverse center are a robust final-B-Rep
|
||||
# signal for stepped bores/counterbores, without claiming operation order.
|
||||
transverse_groups: dict[tuple[int, int, int], set[float]] = defaultdict(set)
|
||||
axis_index = {"x": 0, "y": 1, "z": 2}[dominant_axis]
|
||||
for surface in parallel_cylinders:
|
||||
location = surface["location"]
|
||||
key = (
|
||||
int(round(location[transverse_axes[0]] / max(coaxial_tolerance, 1e-6))),
|
||||
int(round(location[transverse_axes[1]] / max(coaxial_tolerance, 1e-6))),
|
||||
axis_index,
|
||||
)
|
||||
transverse_groups[key].add(round(surface["radius"], 4))
|
||||
has_stepped_cylindrical_passage = any(
|
||||
len(radii) >= 2 for radii in transverse_groups.values()
|
||||
)
|
||||
|
||||
features: list[dict[str, Any]] = []
|
||||
if has_rotational_stack:
|
||||
features.extend(
|
||||
[
|
||||
{"type": "rotational_body"},
|
||||
{"type": "coaxial_cylindrical_stack"},
|
||||
]
|
||||
)
|
||||
if len(distinct_coaxial_radii) >= 3:
|
||||
features.append({"type": "central_passage_candidate"})
|
||||
if parallel_cones:
|
||||
features.append({"type": "conical_transition"})
|
||||
if has_repeated_axial_holes:
|
||||
features.append({"type": "repeated_axial_hole_pattern"})
|
||||
if has_multi_axis_passages:
|
||||
features.append({"type": "multi_axis_passage"})
|
||||
features.append(
|
||||
{
|
||||
"type": (
|
||||
"elongated_body"
|
||||
if elongated
|
||||
else "plate_like_body"
|
||||
if plate_like
|
||||
else "compact_body"
|
||||
if compact
|
||||
else "moderate_aspect_body"
|
||||
)
|
||||
}
|
||||
)
|
||||
if planar_fraction >= 0.55:
|
||||
features.append({"type": "planar_dominant_body"})
|
||||
if cylindrical_fraction >= 0.55:
|
||||
features.append({"type": "cylindrical_dominant_body"})
|
||||
if len(planes) >= 6:
|
||||
features.append({"type": "multi_level_planar_profile"})
|
||||
if len(cylinders) >= 2:
|
||||
features.append({"type": "cylindrical_feature_network"})
|
||||
if toruses:
|
||||
features.append({"type": "toroidal_blend_or_groove"})
|
||||
if spheres:
|
||||
features.append({"type": "spherical_surface_feature"})
|
||||
if other_surfaces:
|
||||
features.append({"type": "freeform_surface_region"})
|
||||
if has_circular_pattern:
|
||||
features.append({"type": "circular_equal_radius_pattern"})
|
||||
if has_stepped_cylindrical_passage:
|
||||
features.append({"type": "stepped_cylindrical_passage"})
|
||||
if has_rotational_stack:
|
||||
features.append({"type": "stepped_rotational_profile"})
|
||||
if elongated:
|
||||
features.append({"type": "shaft_like_body"})
|
||||
if len(distinct_coaxial_radii) >= 3 and elongated:
|
||||
features.append({"type": "sleeve_or_hollow_shaft_candidate"})
|
||||
if transverse_span > 0 and dominant_span / transverse_span < 0.8:
|
||||
features.append({"type": "flange_like_rotational_body"})
|
||||
if len(surfaces) >= 40:
|
||||
features.append({"type": "high_topological_complexity"})
|
||||
elif len(surfaces) >= 16:
|
||||
features.append({"type": "medium_topological_complexity"})
|
||||
else:
|
||||
features.append({"type": "low_topological_complexity"})
|
||||
|
||||
constraints: list[dict[str, Any]] = []
|
||||
if has_rotational_stack:
|
||||
constraints.extend(
|
||||
[
|
||||
{"id": "dominant_axis_alignment", "type": "axis_alignment"},
|
||||
{"id": "coaxial_stack", "type": "coaxial"},
|
||||
]
|
||||
)
|
||||
if has_repeated_axial_holes:
|
||||
constraints.append(
|
||||
{"id": "repeated_radius_group", "type": "repeated_feature"}
|
||||
)
|
||||
if parallel_cones:
|
||||
constraints.append(
|
||||
{"id": "transition_axis_continuity", "type": "axis_alignment"}
|
||||
)
|
||||
if has_circular_pattern:
|
||||
constraints.append(
|
||||
{"id": "common_pattern_radius", "type": "radial_pattern"}
|
||||
)
|
||||
if has_multi_axis_passages:
|
||||
constraints.append(
|
||||
{"id": "multiple_axis_system", "type": "axis_network"}
|
||||
)
|
||||
if has_stepped_cylindrical_passage:
|
||||
constraints.append(
|
||||
{"id": "shared_passage_axis", "type": "coaxial"}
|
||||
)
|
||||
|
||||
if has_rotational_stack and has_repeated_axial_holes:
|
||||
family = "flanged_rotational_part"
|
||||
elif has_rotational_stack and elongated and len(distinct_coaxial_radii) >= 3:
|
||||
family = "hollow_or_stepped_shaft"
|
||||
elif has_rotational_stack and elongated:
|
||||
family = "shaft"
|
||||
elif has_rotational_stack:
|
||||
family = "rotational_part"
|
||||
elif has_repeated_axial_holes and plate_like:
|
||||
family = "patterned_plate"
|
||||
elif has_repeated_axial_holes:
|
||||
family = "patterned_prismatic_part"
|
||||
elif has_multi_axis_passages and len(cylinders) >= 3:
|
||||
family = "multi_axis_manifold"
|
||||
elif plate_like and planes:
|
||||
family = "plate_or_bracket"
|
||||
elif planar_fraction >= 0.55:
|
||||
family = "prismatic_block"
|
||||
elif other_surfaces or toruses or spheres:
|
||||
family = "hybrid_freeform_part"
|
||||
else:
|
||||
family = "general_mechanical_part"
|
||||
|
||||
observations = [
|
||||
_observation(
|
||||
"bbox_short_to_long",
|
||||
short_to_long,
|
||||
"short_bounding_span",
|
||||
"long_bounding_span",
|
||||
),
|
||||
_observation(
|
||||
"bbox_middle_to_long",
|
||||
_ratio(middle_span, long_span),
|
||||
"middle_bounding_span",
|
||||
"long_bounding_span",
|
||||
),
|
||||
_observation(
|
||||
"planar_area_fraction",
|
||||
planar_fraction,
|
||||
"planar_surface_area",
|
||||
"total_surface_area",
|
||||
),
|
||||
_observation(
|
||||
"cylindrical_area_fraction",
|
||||
cylindrical_fraction,
|
||||
"cylindrical_surface_area",
|
||||
"total_surface_area",
|
||||
),
|
||||
]
|
||||
if largest_coaxial_radius > 0:
|
||||
observations.append(
|
||||
_observation(
|
||||
"largest_coaxial_radius_to_transverse_span",
|
||||
_ratio(largest_coaxial_radius, transverse_span),
|
||||
"largest_coaxial_radius",
|
||||
"transverse_bounding_span",
|
||||
)
|
||||
)
|
||||
if smallest_coaxial_radius > 0 and largest_coaxial_radius > 0:
|
||||
observations.append(
|
||||
_observation(
|
||||
"smallest_to_largest_coaxial_radius",
|
||||
_ratio(smallest_coaxial_radius, largest_coaxial_radius),
|
||||
"smallest_coaxial_radius",
|
||||
"largest_coaxial_radius",
|
||||
)
|
||||
)
|
||||
if pattern_mean_offset > 0:
|
||||
observations.append(
|
||||
_observation(
|
||||
"pattern_radius_to_transverse_span",
|
||||
_ratio(pattern_mean_offset, transverse_span),
|
||||
"pattern_radius",
|
||||
"transverse_bounding_span",
|
||||
)
|
||||
)
|
||||
if largest_repeated_group:
|
||||
observations.append(
|
||||
_observation(
|
||||
"repeated_feature_radius_to_transverse_span",
|
||||
_ratio(largest_repeated_group[0]["radius"], transverse_span),
|
||||
"repeated_feature_radius",
|
||||
"transverse_bounding_span",
|
||||
)
|
||||
)
|
||||
|
||||
summary = {
|
||||
"dominant_axis": dominant_axis,
|
||||
"coaxial_cylinder_surface_count": len(coaxial_cylinders),
|
||||
"distinct_coaxial_radius_count": len(distinct_coaxial_radii),
|
||||
"off_axis_parallel_cylinder_surface_count": len(off_axis_cylinders),
|
||||
"largest_repeated_radius_group": repeated_group_sizes[0]
|
||||
if repeated_group_sizes
|
||||
else 0,
|
||||
"parallel_cone_surface_count": len(parallel_cones),
|
||||
"has_multi_axis_passages": has_multi_axis_passages,
|
||||
"shape_class": (
|
||||
"elongated" if elongated else "plate_like" if plate_like else "compact"
|
||||
if compact
|
||||
else "moderate_aspect"
|
||||
),
|
||||
"normalized_observations": observations,
|
||||
}
|
||||
return family, features, constraints, summary
|
||||
|
||||
|
||||
def _reconstruction_evidence(
|
||||
surfaces: list[dict[str, Any]],
|
||||
features: list[dict[str, Any]],
|
||||
constraints: list[dict[str, Any]],
|
||||
semantic_summary: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Describe a canonical reconstruction grammar without claiming CAD history."""
|
||||
feature_roles = sorted(
|
||||
{
|
||||
str(item.get("type"))
|
||||
for item in features
|
||||
if isinstance(item, dict) and item.get("type")
|
||||
}
|
||||
)
|
||||
relation_roles = sorted(
|
||||
{
|
||||
str(item.get("id") or item.get("type"))
|
||||
for item in constraints
|
||||
if isinstance(item, dict) and (item.get("id") or item.get("type"))
|
||||
}
|
||||
)
|
||||
surface_counts = Counter(item.get("type", "other") for item in surfaces)
|
||||
|
||||
parameter_roles = [
|
||||
"overall_long_span",
|
||||
"overall_middle_span",
|
||||
"overall_short_span",
|
||||
"primary_axis_span",
|
||||
"primary_transverse_span",
|
||||
"secondary_transverse_span",
|
||||
]
|
||||
if "cylindrical_feature_network" in feature_roles:
|
||||
parameter_roles.extend(
|
||||
[
|
||||
"passage_diameter_roles",
|
||||
"passage_axis_offset_roles",
|
||||
"passage_extent_roles",
|
||||
]
|
||||
)
|
||||
if "coaxial_cylindrical_stack" in feature_roles:
|
||||
parameter_roles.extend(
|
||||
[
|
||||
"coaxial_diameter_roles",
|
||||
"axial_segment_length_roles",
|
||||
"shoulder_position_roles",
|
||||
]
|
||||
)
|
||||
if "repeated_axial_hole_pattern" in feature_roles:
|
||||
parameter_roles.extend(
|
||||
[
|
||||
"pattern_member_diameter_role",
|
||||
"pattern_radius_or_spacing_role",
|
||||
"pattern_angular_or_linear_phase_role",
|
||||
]
|
||||
)
|
||||
if "multi_level_planar_profile" in feature_roles:
|
||||
parameter_roles.extend(
|
||||
[
|
||||
"planar_level_offset_roles",
|
||||
"profile_width_roles",
|
||||
"profile_length_roles",
|
||||
]
|
||||
)
|
||||
if "conical_transition" in feature_roles:
|
||||
parameter_roles.append("transition_slope_role")
|
||||
|
||||
stages: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": "establish_reference_frame",
|
||||
"operation": "define_datums",
|
||||
"feature_roles": [],
|
||||
"reference_roles": [
|
||||
"part_center",
|
||||
"primary_axis",
|
||||
"primary_transverse_plane",
|
||||
"secondary_transverse_plane",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "construct_primary_envelope",
|
||||
"operation": (
|
||||
"revolve_profile"
|
||||
if "rotational_body" in feature_roles
|
||||
else "extrude_profile"
|
||||
),
|
||||
"feature_roles": [
|
||||
role
|
||||
for role in (
|
||||
"rotational_body",
|
||||
"planar_dominant_body",
|
||||
"elongated_body",
|
||||
"plate_like_body",
|
||||
"compact_body",
|
||||
"moderate_aspect_body",
|
||||
)
|
||||
if role in feature_roles
|
||||
],
|
||||
"reference_roles": ["part_center", "primary_axis"],
|
||||
},
|
||||
]
|
||||
if "multi_level_planar_profile" in feature_roles:
|
||||
stages.append(
|
||||
{
|
||||
"id": "establish_planar_levels",
|
||||
"operation": "add_or_remove_profile_levels",
|
||||
"feature_roles": ["multi_level_planar_profile"],
|
||||
"reference_roles": ["primary_transverse_plane"],
|
||||
}
|
||||
)
|
||||
if "coaxial_cylindrical_stack" in feature_roles:
|
||||
stages.append(
|
||||
{
|
||||
"id": "construct_coaxial_stack",
|
||||
"operation": "add_or_cut_coaxial_profiles",
|
||||
"feature_roles": ["coaxial_cylindrical_stack"],
|
||||
"reference_roles": ["primary_axis"],
|
||||
}
|
||||
)
|
||||
if "cylindrical_feature_network" in feature_roles:
|
||||
stages.append(
|
||||
{
|
||||
"id": "construct_passage_network",
|
||||
"operation": "cut_semantic_passages",
|
||||
"feature_roles": [
|
||||
role
|
||||
for role in (
|
||||
"cylindrical_feature_network",
|
||||
"central_passage_candidate",
|
||||
"stepped_cylindrical_passage",
|
||||
"multi_axis_passage",
|
||||
)
|
||||
if role in feature_roles
|
||||
],
|
||||
"reference_roles": ["primary_axis", "part_center"],
|
||||
}
|
||||
)
|
||||
if "repeated_axial_hole_pattern" in feature_roles:
|
||||
stages.append(
|
||||
{
|
||||
"id": "construct_repeated_feature_pattern",
|
||||
"operation": "pattern_semantic_feature",
|
||||
"feature_roles": [
|
||||
role
|
||||
for role in (
|
||||
"repeated_axial_hole_pattern",
|
||||
"circular_equal_radius_pattern",
|
||||
)
|
||||
if role in feature_roles
|
||||
],
|
||||
"reference_roles": ["primary_axis", "part_center"],
|
||||
}
|
||||
)
|
||||
transition_roles = [
|
||||
role
|
||||
for role in (
|
||||
"conical_transition",
|
||||
"toroidal_blend_or_groove",
|
||||
"spherical_surface_feature",
|
||||
"freeform_surface_region",
|
||||
)
|
||||
if role in feature_roles
|
||||
]
|
||||
if transition_roles:
|
||||
stages.append(
|
||||
{
|
||||
"id": "resolve_transitions_and_finishing",
|
||||
"operation": "apply_transitions_or_blends",
|
||||
"feature_roles": transition_roles,
|
||||
"reference_roles": ["primary_axis"],
|
||||
}
|
||||
)
|
||||
stages.append(
|
||||
{
|
||||
"id": "validate_reconstructed_shape",
|
||||
"operation": "validate_geometry_and_relations",
|
||||
"feature_roles": feature_roles,
|
||||
"reference_roles": ["part_center", "primary_axis"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"interpretation": "canonical_reconstruction_plan_not_recovered_history",
|
||||
"parameter_roles": sorted(set(parameter_roles)),
|
||||
"datum_roles": [
|
||||
"part_center",
|
||||
"primary_axis",
|
||||
"primary_transverse_plane",
|
||||
"secondary_transverse_plane",
|
||||
],
|
||||
"feature_roles": feature_roles,
|
||||
"relation_roles": relation_roles,
|
||||
"canonical_stages": stages,
|
||||
"private_cardinality_evidence": {
|
||||
"surface_type_counts": dict(sorted(surface_counts.items())),
|
||||
"surface_type_classes": {
|
||||
kind: _cardinality_class(count)
|
||||
for kind, count in sorted(surface_counts.items())
|
||||
},
|
||||
"feature_role_count": len(feature_roles),
|
||||
"feature_role_count_class": _cardinality_class(len(feature_roles)),
|
||||
"coaxial_radius_role_count": int(
|
||||
semantic_summary.get("distinct_coaxial_radius_count", 0)
|
||||
),
|
||||
"coaxial_radius_role_count_class": _cardinality_class(
|
||||
int(semantic_summary.get("distinct_coaxial_radius_count", 0))
|
||||
),
|
||||
"repeated_member_count": int(
|
||||
semantic_summary.get("largest_repeated_radius_group", 0)
|
||||
),
|
||||
"repeated_member_count_class": _cardinality_class(
|
||||
int(semantic_summary.get("largest_repeated_radius_group", 0))
|
||||
),
|
||||
},
|
||||
"validation_roles": sorted(
|
||||
{
|
||||
"closed_solid",
|
||||
"bounding_proportion_consistency",
|
||||
"surface_mix_consistency",
|
||||
*relation_roles,
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
from build123d import import_step
|
||||
|
||||
resolved = source.expanduser().resolve()
|
||||
source_bytes = resolved.read_bytes()
|
||||
shape = import_step(str(resolved))
|
||||
bbox = shape.bounding_box()
|
||||
bbox_min = _point(bbox.min)
|
||||
bbox_max = _point(bbox.max)
|
||||
bbox_size = _point(bbox.size)
|
||||
bbox_center = [
|
||||
_rounded((lower + upper) / 2.0)
|
||||
for lower, upper in zip(bbox_min, bbox_max)
|
||||
]
|
||||
surfaces = [
|
||||
_surface_record(face, index) for index, face in enumerate(shape.faces())
|
||||
]
|
||||
surface_counts = Counter(surface["type"] for surface in surfaces)
|
||||
family, features, constraints, semantic_summary = _semantic_summary(
|
||||
surfaces, bbox_center, bbox_size
|
||||
)
|
||||
reconstruction_evidence = _reconstruction_evidence(
|
||||
surfaces, features, constraints, semantic_summary
|
||||
)
|
||||
digest = hashlib.sha256(source_bytes).hexdigest()
|
||||
normalized_observations = semantic_summary.pop("normalized_observations", [])
|
||||
return {
|
||||
"schema_version": "2.0",
|
||||
"extractor_version": EXTRACTOR_VERSION,
|
||||
"case_kind": "private_step_evidence",
|
||||
"case_id": digest,
|
||||
"provenance": {
|
||||
"source_path": str(resolved),
|
||||
"source_sha256": digest,
|
||||
"source_format": "step",
|
||||
},
|
||||
"geometry_evidence": {
|
||||
"valid": bool(shape.is_valid),
|
||||
"solid_count": len(shape.solids()),
|
||||
"face_count": len(shape.faces()),
|
||||
"volume": _rounded(shape.volume),
|
||||
"bounding_box": {
|
||||
"min": bbox_min,
|
||||
"max": bbox_max,
|
||||
"size": bbox_size,
|
||||
"center": bbox_center,
|
||||
},
|
||||
"analytic_surface_counts": dict(sorted(surface_counts.items())),
|
||||
"analytic_surfaces": surfaces,
|
||||
},
|
||||
"design_ir": {
|
||||
"part_family": family,
|
||||
"features": features,
|
||||
"constraints": constraints,
|
||||
"normalized_observations": normalized_observations,
|
||||
"semantic_summary": semantic_summary,
|
||||
"reconstruction_evidence": reconstruction_evidence,
|
||||
},
|
||||
"experience": {
|
||||
"rules": [],
|
||||
"validation_targets": [],
|
||||
"promotion_state": "evidence_only",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract a private case JSON from a STEP file."
|
||||
)
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args(argv)
|
||||
write_json(args.output, extract_step_case(args.source))
|
||||
print(args.output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "designir_pipeline.py"
|
||||
SPEC = importlib.util.spec_from_file_location("designir_pipeline", SCRIPT)
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class DesignIRPipelineTests(unittest.TestCase):
|
||||
def payload(self) -> dict:
|
||||
return json.loads(
|
||||
(ROOT / "examples" / "raised_hub_flange.designir.json").read_text()
|
||||
)
|
||||
|
||||
def test_example_is_valid_and_compiles(self) -> None:
|
||||
payload = MODULE.validate_designir(self.payload())
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
result = MODULE.compile_designir(payload, output)
|
||||
self.assertTrue(output.is_file())
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
|
||||
def test_teacher_dependency_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["source_step"] = "teacher.step"
|
||||
with self.assertRaisesRegex(MODULE.DesignIRError, "Teacher geometry"):
|
||||
MODULE.validate_designir(payload)
|
||||
|
||||
def test_example_compiles_with_simplecadapi_and_exports_graph(self) -> None:
|
||||
payload = self.payload()
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
result = MODULE.compile_designir(
|
||||
payload, output, backend="simplecadapi"
|
||||
)
|
||||
self.assertEqual("simplecadapi", result["backend"])
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
self.assertTrue(Path(result["model_json"]).is_file())
|
||||
|
||||
def test_embedded_mesh_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["features"][0]["mesh"] = {"vertices": [[0, 0, 0]]}
|
||||
with self.assertRaisesRegex(MODULE.DesignIRError, "Teacher geometry"):
|
||||
MODULE.validate_designir(payload)
|
||||
|
||||
def test_isolated_rebuild_has_no_teacher(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
designir = root / "flange.designir.json"
|
||||
designir.write_text(json.dumps(self.payload()), encoding="utf-8")
|
||||
output = root / "rebuilt.step"
|
||||
result = MODULE.isolated_rebuild(designir, output)
|
||||
self.assertTrue(result["valid"])
|
||||
self.assertFalse(result["teacher_step_available"])
|
||||
self.assertTrue(output.is_file())
|
||||
|
||||
def test_acceptance_passes_identical_designir_rebuild(self) -> None:
|
||||
payload = self.payload()
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
teacher = root / "teacher.step"
|
||||
rebuilt = root / "rebuilt.step"
|
||||
MODULE.compile_designir(payload, teacher)
|
||||
MODULE.compile_designir(payload, rebuilt)
|
||||
report = MODULE.acceptance_report(teacher, rebuilt, payload)
|
||||
self.assertEqual("pass", report["verdict"])
|
||||
self.assertEqual("replay_validated", report["next_state"])
|
||||
self.assertEqual(
|
||||
1.0, report["editability"]["parameter_perturbation_success_rate"]
|
||||
)
|
||||
|
||||
def test_unsupported_vocabulary_is_honest_failure(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["reconstruction_status"] = "unsupported_feature_vocabulary"
|
||||
payload["missing_capabilities"] = ["multi_rail_sweep"]
|
||||
with self.assertRaisesRegex(MODULE.DesignIRError, "multi_rail_sweep"):
|
||||
MODULE.build_shape(payload)
|
||||
|
||||
def test_promotion_requires_independent_replay_validated_cases(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
reports = []
|
||||
for index in range(3):
|
||||
report = root / f"case-{index}.json"
|
||||
report.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model_id": f"case-{index}",
|
||||
"next_state": "replay_validated",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reports.append(report)
|
||||
result = MODULE.promotion_check(
|
||||
reports, ROOT / "config" / "promotion-policy.json"
|
||||
)
|
||||
self.assertTrue(result["eligible"])
|
||||
self.assertEqual("promoted", result["next_state"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,382 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT = (
|
||||
Path(__file__).parents[1]
|
||||
/ "skills"
|
||||
/ "cad-experience-builder"
|
||||
/ "scripts"
|
||||
/ "cad_experience.py"
|
||||
)
|
||||
SPEC = importlib.util.spec_from_file_location("cad_experience", SCRIPT)
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
STEP_SCRIPT = (
|
||||
Path(__file__).parents[1]
|
||||
/ "skills"
|
||||
/ "cad-experience-builder"
|
||||
/ "scripts"
|
||||
/ "step_to_case.py"
|
||||
)
|
||||
STEP_SPEC = importlib.util.spec_from_file_location("step_to_case", STEP_SCRIPT)
|
||||
STEP_MODULE = importlib.util.module_from_spec(STEP_SPEC)
|
||||
assert STEP_SPEC.loader
|
||||
STEP_SPEC.loader.exec_module(STEP_MODULE)
|
||||
|
||||
|
||||
def make_case(index: int) -> dict:
|
||||
return {
|
||||
"provenance": {"source_sha256": f"case_{index}"},
|
||||
"design_ir": {
|
||||
"part_family": "flanged_hub_adapter",
|
||||
"features": [
|
||||
{"type": "base_flange", "center": [91.0, 52.0]},
|
||||
{"type": "hollow_sleeve", "surface_ids": ["face_1"]},
|
||||
{"type": "counterbore"},
|
||||
],
|
||||
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
|
||||
"parameters": {
|
||||
"base_outer_diameter": 91.0,
|
||||
"counterbore_spacing": 52.0,
|
||||
},
|
||||
"normalized_observations": [
|
||||
{
|
||||
"name": "sleeve_od_to_base_od",
|
||||
"value": 0.3 + index * 0.001,
|
||||
"numerator_role": "sleeve_outer_diameter",
|
||||
"denominator_role": "base_outer_diameter",
|
||||
}
|
||||
],
|
||||
"reconstruction_evidence": {
|
||||
"parameter_roles": [
|
||||
"overall_long_span",
|
||||
"overall_middle_span",
|
||||
"overall_short_span",
|
||||
],
|
||||
"datum_roles": ["part_center", "primary_axis"],
|
||||
"feature_roles": ["base_flange", "hollow_sleeve", "counterbore"],
|
||||
"relation_roles": ["coaxial_stack"],
|
||||
"canonical_stages": [
|
||||
{
|
||||
"id": "establish_reference_frame",
|
||||
"operation": "define_datums",
|
||||
"feature_roles": [],
|
||||
"reference_roles": ["part_center", "primary_axis"],
|
||||
},
|
||||
{
|
||||
"id": "construct_primary_envelope",
|
||||
"operation": "revolve_profile",
|
||||
"feature_roles": ["base_flange", "hollow_sleeve"],
|
||||
"reference_roles": ["primary_axis"],
|
||||
},
|
||||
],
|
||||
"private_cardinality_evidence": {
|
||||
"surface_type_classes": {
|
||||
"plane": "repeated",
|
||||
"cylinder": "dense",
|
||||
},
|
||||
"feature_role_count_class": "repeated",
|
||||
},
|
||||
"validation_roles": ["closed_solid", "coaxial_stack"],
|
||||
},
|
||||
},
|
||||
"experience": {
|
||||
"rules": [
|
||||
{
|
||||
"id": "bound_counterbore_to_host",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"statement": "Terminate a counterbore at its host boundary.",
|
||||
"repair": "Bind the cutter extent to the host feature.",
|
||||
}
|
||||
],
|
||||
"validation_targets": [
|
||||
{
|
||||
"id": "host_boundary",
|
||||
"check": "A counterbore does not cross an unrelated adjacent feature.",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CadExperienceTest(unittest.TestCase):
|
||||
def reviewed_proposal(self) -> dict:
|
||||
return {
|
||||
"id": "constraint.flanged_hub_adapter.shared_axis",
|
||||
"kind": "constraint",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"evidence_query": {
|
||||
"required_features": ["base_flange", "hollow_sleeve"],
|
||||
"required_relations": ["coaxial_stack"],
|
||||
},
|
||||
"guidance": "Preserve a shared axis across participating feature roles.",
|
||||
"semantic_rationale": "The relationship transfers across dimensional variants.",
|
||||
}
|
||||
|
||||
def test_semantic_summary_derives_roles_without_copying_dimensions(self):
|
||||
surfaces = [
|
||||
{
|
||||
"type": "cylinder",
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"location": [0.0, 0.0, 0.0],
|
||||
"radius": radius,
|
||||
"area": 100.0,
|
||||
}
|
||||
for radius in (5.0, 10.0, 20.0)
|
||||
]
|
||||
surfaces.extend(
|
||||
{
|
||||
"type": "cylinder",
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"location": [offset, 0.0, 0.0],
|
||||
"radius": 2.0,
|
||||
"area": 20.0,
|
||||
}
|
||||
for offset in (-15.0, 15.0, 16.0)
|
||||
)
|
||||
surfaces.append(
|
||||
{
|
||||
"type": "cone",
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"location": [0.0, 0.0, 0.0],
|
||||
"reference_radius": 10.0,
|
||||
"semi_angle_radians": 0.5,
|
||||
"area": 50.0,
|
||||
}
|
||||
)
|
||||
family, features, constraints, summary = STEP_MODULE._semantic_summary(
|
||||
surfaces, [0.0, 0.0, 0.0], [40.0, 20.0, 40.0]
|
||||
)
|
||||
self.assertEqual("flanged_rotational_part", family)
|
||||
self.assertIn(
|
||||
"coaxial_cylindrical_stack", {item["type"] for item in features}
|
||||
)
|
||||
self.assertIn("coaxial_stack", {item["id"] for item in constraints})
|
||||
self.assertEqual("y", summary["dominant_axis"])
|
||||
self.assertTrue(summary["normalized_observations"])
|
||||
reconstruction = STEP_MODULE._reconstruction_evidence(
|
||||
surfaces, features, constraints, summary
|
||||
)
|
||||
self.assertIn("overall_long_span", reconstruction["parameter_roles"])
|
||||
self.assertTrue(reconstruction["canonical_stages"])
|
||||
self.assertEqual(
|
||||
"canonical_reconstruction_plan_not_recovered_history",
|
||||
reconstruction["interpretation"],
|
||||
)
|
||||
|
||||
def test_conditional_feature_context_does_not_dilute_valid_method(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
proposal["evidence_query"]["context_features"] = [
|
||||
"base_flange",
|
||||
"hollow_sleeve",
|
||||
]
|
||||
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
unrelated = []
|
||||
for index in range(80):
|
||||
payload = make_case(index + 100)
|
||||
payload["design_ir"]["features"] = [{"type": "base_flange"}]
|
||||
payload["design_ir"]["constraints"] = []
|
||||
unrelated.append(MODULE.normalize_case(payload))
|
||||
library = MODULE.build_reviewed_library(
|
||||
target + unrelated,
|
||||
[MODULE.normalize_proposals({
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [proposal],
|
||||
})[0]],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual(1, len(library["experiences"]))
|
||||
|
||||
def test_single_case_never_promotes(self):
|
||||
case = MODULE.normalize_case(make_case(1))
|
||||
library = MODULE.induce(
|
||||
[case],
|
||||
2,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual(library["status"], "collecting_evidence")
|
||||
self.assertEqual(library["experiences"], [])
|
||||
self.assertTrue(library["candidate_experiences"])
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["promotion_state"] == "candidate"
|
||||
for item in library["candidate_experiences"]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["consumer_policy"]
|
||||
== "visible_for_review_but_not_available_to_cad_router"
|
||||
for item in library["candidate_experiences"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_batch_promotes_methods_without_instance_values(self):
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
library = MODULE.induce(
|
||||
cases,
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual(MODULE.audit_library(library), [])
|
||||
rendered = json.dumps(library)
|
||||
self.assertNotIn('"parameters"', rendered)
|
||||
self.assertNotIn("91.0", rendered)
|
||||
self.assertNotIn("52.0", rendered)
|
||||
self.assertNotIn('"center"', rendered)
|
||||
ids = {item["id"] for item in library["experiences"]}
|
||||
self.assertIn("rule.bound_counterbore_to_host", ids)
|
||||
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
|
||||
self.assertIn("distribution.sleeve_od_to_base_od", ids)
|
||||
|
||||
def test_family_support_is_not_diluted_by_unrelated_families(self):
|
||||
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
unrelated = []
|
||||
for index in range(100):
|
||||
payload = make_case(index + 1000)
|
||||
payload["design_ir"]["part_family"] = "gear"
|
||||
payload["design_ir"]["features"] = [
|
||||
{"type": "gear_blank"},
|
||||
{"type": "gear_teeth"},
|
||||
]
|
||||
unrelated.append(MODULE.normalize_case(payload))
|
||||
library = MODULE.induce(
|
||||
target + unrelated,
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
ids = {item["id"] for item in library["experiences"]}
|
||||
self.assertIn(
|
||||
"motif.flanged_hub_adapter.base_flange+hollow_sleeve", ids
|
||||
)
|
||||
|
||||
def test_query_returns_backend_neutral_context(self):
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
library = MODULE.induce(
|
||||
cases,
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
result = MODULE.query_library(
|
||||
library,
|
||||
"flanged_hub_adapter",
|
||||
{"base_flange", "hollow_sleeve", "counterbore"},
|
||||
)
|
||||
self.assertTrue(result["experiences"])
|
||||
self.assertFalse(result["policy"]["contains_instance_parameters"])
|
||||
|
||||
def test_llm_proposal_is_candidate_until_evidence_threshold(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
one_case = MODULE.normalize_case(make_case(1))
|
||||
candidate_library = MODULE.build_reviewed_library(
|
||||
[one_case],
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual([], candidate_library["experiences"])
|
||||
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
|
||||
self.assertTrue(candidate_library["policy"]["router_consumable"])
|
||||
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
promoted_library = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual(1, len(promoted_library["experiences"]))
|
||||
self.assertEqual([], promoted_library["candidate_experiences"])
|
||||
|
||||
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
proposal["guidance"] = "Always use a diameter of 91 millimeters."
|
||||
with self.assertRaisesRegex(ValueError, "instance numbers"):
|
||||
MODULE.normalize_proposals(
|
||||
{
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [proposal],
|
||||
}
|
||||
)
|
||||
|
||||
def test_reconstruction_grammar_promotes_without_instance_values(self):
|
||||
proposal = {
|
||||
"id": "reconstruction.flanged_hub_adapter.primary",
|
||||
"kind": "reconstruction_grammar",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"evidence_query": {
|
||||
"required_features": ["base_flange", "hollow_sleeve"],
|
||||
"required_relations": ["coaxial_stack"],
|
||||
"context_features": ["base_flange", "hollow_sleeve"],
|
||||
},
|
||||
"guidance": "Establish shared datums before composing the primary envelope.",
|
||||
"semantic_rationale": "The symbolic stages transfer across dimensional variants.",
|
||||
"reconstruction_grammar": {
|
||||
"parameter_roles": [
|
||||
"overall_long_span",
|
||||
"overall_middle_span",
|
||||
"overall_short_span",
|
||||
],
|
||||
"datum_roles": ["part_center", "primary_axis"],
|
||||
"feature_roles": ["base_flange", "hollow_sleeve"],
|
||||
"relation_roles": ["coaxial_stack"],
|
||||
"canonical_stages": [
|
||||
{
|
||||
"id": "establish_reference_frame",
|
||||
"operation": "define_datums",
|
||||
"feature_roles": [],
|
||||
"reference_roles": ["part_center", "primary_axis"],
|
||||
},
|
||||
{
|
||||
"id": "construct_primary_envelope",
|
||||
"operation": "revolve_profile",
|
||||
"feature_roles": ["base_flange", "hollow_sleeve"],
|
||||
"reference_roles": ["primary_axis"],
|
||||
},
|
||||
],
|
||||
"cardinality_classes": ["plane_repeated", "cylinder_dense"],
|
||||
"validation_roles": ["closed_solid", "coaxial_stack"],
|
||||
},
|
||||
}
|
||||
normalized = MODULE.normalize_proposals(
|
||||
{
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [proposal],
|
||||
}
|
||||
)
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
library = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
normalized,
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual([], MODULE.audit_library(library))
|
||||
self.assertEqual("reconstruction_grammar", library["experiences"][0]["kind"])
|
||||
rendered = json.dumps(library)
|
||||
self.assertNotIn("91.0", rendered)
|
||||
self.assertNotIn('"parameters"', rendered)
|
||||
|
||||
def test_direct_distill_without_model_draft_is_blocked(self):
|
||||
with self.assertRaisesRegex(SystemExit, "statistical distillation is disabled"):
|
||||
MODULE.main(["distill"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# Distillation Workspace
|
||||
|
||||
This directory is the runtime boundary for uploaded STEP reconstruction and
|
||||
experience distillation.
|
||||
|
||||
## Where to put target STEP files
|
||||
|
||||
Place STEP or STP files to be reconstructed in:
|
||||
|
||||
```text
|
||||
teacher/inbox/
|
||||
```
|
||||
|
||||
Use unique, stable filenames. Batch inputs are intentionally ignored by Git so
|
||||
large private corpora do not inflate the repository.
|
||||
|
||||
## Where each stage is stored
|
||||
|
||||
| Stage | Directory | Contents |
|
||||
| --- | --- | --- |
|
||||
| Source input | `teacher/inbox/` | Original target STEP/STP files. |
|
||||
| Extracted evidence | `teacher/evidence/` | Private geometry facts extracted from each source. |
|
||||
| Agent A input | `reconstruction/inbox/` | Sanitized evidence packets and reconstruction requests. |
|
||||
| Agent A result | `reconstruction/designir/` | Executable `*.designir.json` models. |
|
||||
| Independent rebuild | `reconstruction/output/` | STEP rebuilt without teacher-file access; SimpleCAD jobs may also emit model JSON. |
|
||||
| Agent B input | `acceptance/inbox/` | Acceptance manifests linking teacher, DesignIR, rebuild, and edit contract. |
|
||||
| Acceptance result | `acceptance/reports/` | Geometry, topology, semantic, and parameter-perturbation reports. |
|
||||
| Failed/unsupported | `quarantine/` | Unsupported vocabulary, invalid cases, and failure clusters. |
|
||||
| Experience candidates | `promotion/candidates/` | Generalized methods proposed from validated cases. |
|
||||
| Replay gate | `promotion/replay_validated/` | Methods repeated successfully across independent cases. |
|
||||
| Final distilled result | `promotion/promoted/library.json` | Only deterministic, promoted experience consumed by CAD Router. |
|
||||
|
||||
## Data flow
|
||||
|
||||
```text
|
||||
teacher/inbox/*.step
|
||||
-> teacher/evidence/
|
||||
-> reconstruction/inbox/
|
||||
-> reconstruction/designir/
|
||||
-> reconstruction/output/*.step
|
||||
-> acceptance/inbox/
|
||||
-> acceptance/reports/
|
||||
-> promotion/candidates/
|
||||
-> promotion/replay_validated/
|
||||
-> promotion/promoted/library.json
|
||||
```
|
||||
|
||||
Agent A cannot read `teacher/inbox/`. Agent B may read the teacher STEP and the
|
||||
rebuilt result, but neither agent can directly publish experience. Promotion is
|
||||
performed only by the deterministic policy gate.
|
||||
|
||||
The `.gitkeep` files preserve this layout in Git. Runtime STEP files, evidence,
|
||||
rebuilds, and reports remain local by default.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"induction_mode": "replay_validated_designir_with_deterministic_publication",
|
||||
"status": "empty",
|
||||
"policy": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"single_case_promotion_allowed": false,
|
||||
"teacher_geometry_allowed": false,
|
||||
"llm_semantic_review_required": true,
|
||||
"draft_evidence_verified": true,
|
||||
"router_consumable": true,
|
||||
"minimum_support": 3
|
||||
},
|
||||
"corpus_summary": {
|
||||
"accepted_case_count": 0,
|
||||
"duplicate_case_count": 0,
|
||||
"rejected_case_count": 0,
|
||||
"family_count": 0
|
||||
},
|
||||
"experience_summary": {
|
||||
"llm_proposal_count": 0,
|
||||
"promoted_experience_count": 0,
|
||||
"candidate_experience_count": 0
|
||||
},
|
||||
"experiences": [],
|
||||
"candidate_experiences": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"generated_at": "2026-07-23T08:16:40+00:00",
|
||||
"induction_mode": "llm_proposals_with_deterministic_evidence_verification",
|
||||
"status": "ready",
|
||||
"policy": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"single_case_promotion_allowed": false,
|
||||
"llm_semantic_review_required": true,
|
||||
"draft_evidence_verified": true,
|
||||
"router_consumable": true,
|
||||
"minimum_support": 20,
|
||||
"minimum_confidence": 0.8
|
||||
},
|
||||
"corpus_summary": {
|
||||
"accepted_case_count": 2,
|
||||
"duplicate_case_count": 0,
|
||||
"rejected_case_count": 0,
|
||||
"family_count": 11
|
||||
},
|
||||
"experience_summary": {
|
||||
"llm_proposal_count": 4,
|
||||
"promoted_experience_count": 0,
|
||||
"candidate_experience_count": 4,
|
||||
"by_kind": [],
|
||||
"by_scope": [],
|
||||
"candidate_by_kind": [
|
||||
{
|
||||
"kind": "constraint",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"kind": "feature_motif",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"kind": "validation_rule",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"experiences": [
|
||||
{
|
||||
"id": "constraint.rotational_part.shared_dominant_axis",
|
||||
"kind": "constraint",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"rotational_body"
|
||||
],
|
||||
"relations": [
|
||||
"dominant_axis_alignment"
|
||||
]
|
||||
},
|
||||
"guidance": "Preserve the dominant axis as the organizing datum for rotational feature roles.",
|
||||
"semantic_rationale": "The datum transfers because axis alignment is independent of the individual component dimensions.",
|
||||
"support": 2,
|
||||
"confidence": 1.0,
|
||||
"check": "Confirm that rotational feature roles remain aligned to the same organizing datum.",
|
||||
"promotion_state": "candidate",
|
||||
"required_support": 20,
|
||||
"remaining_support": 18,
|
||||
"required_confidence": 0.8,
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
|
||||
},
|
||||
{
|
||||
"id": "motif.rotational_part.coaxial_core_with_center_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"circular_equal_radius_pattern"
|
||||
],
|
||||
"relations": [
|
||||
"shared_passage_axis"
|
||||
]
|
||||
},
|
||||
"guidance": "Treat the rotational body, coaxial cylindrical stack, and central passage as one shared-axis core.",
|
||||
"semantic_rationale": "The relationship transfers because the participating roles describe axis organization rather than a particular size.",
|
||||
"support": 2,
|
||||
"confidence": 1.0,
|
||||
"check": "Confirm that the rotational body, stacked cylindrical roles, and central passage retain their intended common axis.",
|
||||
"promotion_state": "candidate",
|
||||
"required_support": 20,
|
||||
"remaining_support": 18,
|
||||
"required_confidence": 0.8,
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
|
||||
},
|
||||
{
|
||||
"id": "motif.rotational_part.patterned_axial_mounting",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"repeated_axial_hole_pattern",
|
||||
"rotational_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 55,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.hollow_or_stepped_shaft.coaxial_cylindrical_stack+stepped_cylindrical_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"hollow_or_stepped_shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"stepped_cylindrical_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 46,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.hollow_or_stepped_shaft.rotational_body+coaxial_cylindrical_stack",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"hollow_or_stepped_shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"rotational_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 46,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.hollow_or_stepped_shaft.shaft_like_body+elongated_body",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"hollow_or_stepped_shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"elongated_body",
|
||||
"shaft_like_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 46,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.multi_axis_manifold.cylindrical_feature_network+multi_axis_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"multi_axis_manifold"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"cylindrical_feature_network",
|
||||
"multi_axis_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 55,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.multi_axis_manifold.planar_dominant_body+multi_level_planar_profile",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"multi_axis_manifold"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multi_level_planar_profile",
|
||||
"planar_dominant_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 44,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_plate.cylindrical_feature_network+multi_axis_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_plate"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"cylindrical_feature_network",
|
||||
"multi_axis_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 76,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_plate.planar_dominant_body+multi_level_planar_profile",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_plate"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multi_level_planar_profile",
|
||||
"planar_dominant_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 202,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_plate.repeated_axial_hole_pattern+circular_equal_radius_pattern",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_plate"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"circular_equal_radius_pattern",
|
||||
"repeated_axial_hole_pattern"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 66,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_prismatic_part.cylindrical_feature_network+multi_axis_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_prismatic_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"cylindrical_feature_network",
|
||||
"multi_axis_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 156,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_prismatic_part.planar_dominant_body+multi_level_planar_profile",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_prismatic_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multi_level_planar_profile",
|
||||
"planar_dominant_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 323,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.patterned_prismatic_part.repeated_axial_hole_pattern+circular_equal_radius_pattern",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"patterned_prismatic_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"circular_equal_radius_pattern",
|
||||
"repeated_axial_hole_pattern"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 159,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.plate_or_bracket.planar_dominant_body+multi_level_planar_profile",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"plate_or_bracket"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multi_level_planar_profile",
|
||||
"planar_dominant_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 35,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.prismatic_block.planar_dominant_body+multi_level_planar_profile",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"prismatic_block"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multi_level_planar_profile",
|
||||
"planar_dominant_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 67,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.rotational_part.coaxial_cylindrical_stack+stepped_cylindrical_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"stepped_cylindrical_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 123,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.rotational_part.rotational_body+coaxial_cylindrical_stack",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"rotational_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 123,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.shaft.coaxial_cylindrical_stack+stepped_cylindrical_passage",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"stepped_cylindrical_passage"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 40,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.shaft.rotational_body+coaxial_cylindrical_stack",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"coaxial_cylindrical_stack",
|
||||
"rotational_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 40,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "motif.shaft.shaft_like_body+elongated_body",
|
||||
"kind": "feature_motif",
|
||||
"scope": [
|
||||
"shaft"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"elongated_body",
|
||||
"shaft_like_body"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Treat the participating feature roles as one coordinated composition rather than unrelated primitives.",
|
||||
"semantic_rationale": "The composition recurs across independent dimensional variants in the same family.",
|
||||
"support": 40,
|
||||
"confidence": 1.0
|
||||
},
|
||||
{
|
||||
"id": "reconstruction.flanged_rotational_part.primary",
|
||||
"kind": "reconstruction_grammar",
|
||||
"scope": [
|
||||
"flanged_rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"circular_equal_radius_pattern",
|
||||
"coaxial_cylindrical_stack",
|
||||
"cylindrical_feature_network",
|
||||
"multi_level_planar_profile",
|
||||
"repeated_axial_hole_pattern",
|
||||
"rotational_body",
|
||||
"stepped_cylindrical_passage"
|
||||
],
|
||||
"relations": [
|
||||
"repeated_radius_group"
|
||||
]
|
||||
},
|
||||
"guidance": "Represent repeated axial holes as a symmetric mounting pattern tied to the rotational body.",
|
||||
"semantic_rationale": "The motif remains useful across variants because it captures repeated functional placement around a rotational form.",
|
||||
"support": 2,
|
||||
"confidence": 1.0,
|
||||
"check": "Confirm that repeated axial holes remain grouped consistently around the rotational body.",
|
||||
"promotion_state": "candidate",
|
||||
"required_support": 20,
|
||||
"remaining_support": 18,
|
||||
"required_confidence": 0.8,
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
|
||||
},
|
||||
{
|
||||
"id": "validation.rotational_part.axis_and_pattern_consistency",
|
||||
"kind": "validation_rule",
|
||||
"scope": [
|
||||
"rotational_part"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"central_passage",
|
||||
"repeated_axial_hole_pattern"
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"guidance": "Validate both the shared rotational axis and the grouped repeated-hole relationship before handoff.",
|
||||
"semantic_rationale": "These checks protect transferable intent without relying on instance geometry.",
|
||||
"support": 2,
|
||||
"confidence": 1.0,
|
||||
"check": "Confirm shared-axis alignment and repeated-hole grouping before treating the model as ready.",
|
||||
"promotion_state": "candidate",
|
||||
"required_support": 20,
|
||||
"remaining_support": 18,
|
||||
"required_confidence": 0.8,
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user