refactor: add DesignIR distillation pipeline
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user