feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
+203
-16
@@ -4,10 +4,12 @@ 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 OCCT extraction into DesignIR 3.0
|
||||
-> SurfaceIR: independent geometry/topology program
|
||||
-> Semantic FeatureIR: canonical names, features and constraints
|
||||
-> isolated OCCT compiler rebuilds STEP without teacher access
|
||||
-> reconstruction/distillation agent proposes repairs and semantic bindings
|
||||
-> acceptance agent compares geometry and executes parameter perturbations
|
||||
-> deterministic publisher promotes repeated validated experience
|
||||
```
|
||||
|
||||
@@ -16,7 +18,7 @@ teacher STEP
|
||||
| Component | Teacher STEP | Private evidence | DesignIR | Rebuilt STEP |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Ingestion service | read | write | no | no |
|
||||
| Reconstruction agent | denied | read | write | request compilation |
|
||||
| Reconstruction agent | denied | read | write | request compilation/edit |
|
||||
| Isolated compiler | denied | no | read | write |
|
||||
| Acceptance agent | read | read | read | read |
|
||||
| Publisher | denied | aggregate only | schema only | reports only |
|
||||
@@ -35,6 +37,7 @@ designir-pipeline/
|
||||
input/ drop source STEP/STP files here
|
||||
runs/ all private process artifacts
|
||||
output/library.json verified experience consumed at runtime
|
||||
output/distillation-report.json public aggregate result and validation state
|
||||
```
|
||||
|
||||
`input/` and `runs/` are ignored by Git. Only the deterministic result in
|
||||
@@ -53,16 +56,36 @@ needed:
|
||||
The shallow directory structure is for usability, not authorization. Agent and
|
||||
compiler access is enforced by their contracts and by anti-cheat checks.
|
||||
|
||||
## DesignIR 2.0
|
||||
## DesignIR contracts
|
||||
|
||||
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.
|
||||
`contracts/designir-3.0.schema.json` is the authoritative contract for both
|
||||
text/image semantic authoring and uploaded-STEP reconstruction.
|
||||
`contracts/designir-2.0.schema.json` remains only for legacy migration.
|
||||
DesignIR 3.0 combines:
|
||||
|
||||
The initial compiler deliberately supports a small, honest vocabulary through
|
||||
both Build123d and SimpleCADAPI adapters:
|
||||
- an executable OCCT SurfaceIR for independent geometric reconstruction;
|
||||
- a Semantic FeatureIR for paper-style names such as `plate_thickness`,
|
||||
`hole_diameter`, `hole_spacing`, `bolt_circle_diameter`, and `rib_count`;
|
||||
- explicit edit operations and acceptance contracts.
|
||||
|
||||
STEP does not contain the original sketch, parameter names, feature tree, or
|
||||
construction history. Extraction therefore keeps a strict epistemic boundary:
|
||||
|
||||
- analytic surfaces, axes, radii, topology, and bounds are observed evidence;
|
||||
- feature instances, semantic names, constraints, and datums are inferred
|
||||
hypotheses with confidence;
|
||||
- canonical stages are recommended reconstruction order, never recovered
|
||||
source history;
|
||||
- a hypothesis becomes trusted DesignIR only after isolated compilation,
|
||||
geometry comparison, and targeted parameter-edit validation.
|
||||
|
||||
The private evidence names editable candidates using a stable vocabulary such
|
||||
as `plate_thickness`, `hole_diameter`, `hole_spacing`,
|
||||
`bolt_circle_diameter`, and `rib_count`. Ambiguous cylinder groups retain
|
||||
aliases and reduced confidence instead of being silently declared holes.
|
||||
|
||||
The semantic DesignIR 3.0 compiler currently supports this initial vocabulary
|
||||
through Build123d and SimpleCADAPI:
|
||||
|
||||
- `extrude_circle`
|
||||
- `extrude_rectangle`
|
||||
@@ -70,13 +93,138 @@ both Build123d and SimpleCADAPI adapters:
|
||||
- `through_hole`
|
||||
- `polar_hole_pattern`
|
||||
|
||||
Unsupported geometry is quarantined with explicit missing capabilities instead
|
||||
of embedding the source B-Rep.
|
||||
Build123d and SimpleCADAPI are not the geometry fidelity boundary. Analytic and
|
||||
B-Spline surfaces unsupported by those adapters are compiled directly by OCCT
|
||||
from SurfaceIR. Unsupported operations are quarantined 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.
|
||||
|
||||
## DesignIR 3.0 geometry and semantic layers
|
||||
|
||||
DesignIR 3.0 adds an independent low-level parameter layer for STEP regions
|
||||
that are not yet expressible as semantic features. It stores analytic surface
|
||||
parameters, B-Spline poles/weights/knots, exact boundary curves, pcurves, and
|
||||
face-wire-shell-solid topology as JSON. It does not store the STEP file, an
|
||||
import instruction, an embedded B-Rep, or a triangle mesh.
|
||||
|
||||
The semantic layer is the preferred modification interface. Parameters remain
|
||||
non-editable candidates until an executable binding passes a real perturbation.
|
||||
Feature-specific bindings enlarge cylindrical holes and bores by applying
|
||||
deduplicated OCCT cut features to the independently rebuilt SurfaceIR. Every
|
||||
geometry-bearing model also exposes `overall_scale` as a dimensionless minimum
|
||||
editability fallback about a deterministic SurfaceIR datum. This fallback does
|
||||
not claim recovery of feature-specific parameters such as plate thickness,
|
||||
hole spacing, or rib count.
|
||||
|
||||
The acceptance agent checks the requested axes/radii for cylindrical edits,
|
||||
solid validity, declared volume direction, non-target parameter stability, and
|
||||
source independence. For uniform scale it additionally requires volume to
|
||||
change by the scale factor cubed, center and bounds to undergo the same
|
||||
transform, and solid/face/edge counts to remain stable. Equivalent vertex
|
||||
normalization performed by STEP serialization is retained as a diagnostic.
|
||||
|
||||
```bash
|
||||
# Convert every input STEP into an independent DesignIR 3.0 instance program.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py extract-folder \
|
||||
input --output-dir runs/surfaceir
|
||||
|
||||
# Rebuild and compare a deterministic batch without loading STEP at runtime.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py validate-folder \
|
||||
input --designir-dir runs/surfaceir \
|
||||
--rebuilt-dir runs/surfaceir-rebuilt --limit 100
|
||||
|
||||
# Apply one named parameter without loading the teacher STEP in the compiler.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py edit \
|
||||
runs/surfaceir/part.designir.json \
|
||||
--parameter hole_diameter --value 6 \
|
||||
--output-designir runs/edited/part.designir.json \
|
||||
--output-step runs/edited/part.step
|
||||
|
||||
# Agent B runs process-isolated +10% perturbations over a corpus.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
validate-edits-folder input \
|
||||
--designir-dir runs/surfaceir \
|
||||
--output-dir runs/semantic-edit-acceptance \
|
||||
--geometry-report runs/surfaceir-rebuilt/validation-report.json \
|
||||
--workers 4
|
||||
|
||||
# Validate the universal source-independent edit fallback on every non-empty
|
||||
# model. Empty STEP documents remain explicitly non-editable.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
augment-overall-scale runs/surfaceir \
|
||||
--output runs/surfaceir/overall-scale-augmentation.json
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
validate-edits-folder input \
|
||||
--designir-dir runs/surfaceir \
|
||||
--output-dir runs/overall-scale-acceptance \
|
||||
--geometry-report runs/surfaceir/strategy-selection-final.json \
|
||||
--parameter overall_scale --workers 4
|
||||
|
||||
# Merge incremental failure retries into one authoritative acceptance report.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py \
|
||||
merge-acceptance-reports \
|
||||
--baseline runs/semantic-edit-acceptance/batch-acceptance-report.json \
|
||||
--retry runs/semantic-edit-retry/batch-acceptance-report.json \
|
||||
--output runs/semantic-edit-authoritative.json
|
||||
```
|
||||
|
||||
Boundary reconstruction strategies are evaluated by paired held-out A/B. The
|
||||
Promoted methods and their exact held-out evidence are stored in
|
||||
`output/library.json`. Geometry methods and semantic-edit methods have separate
|
||||
counts; a geometry-only result never increases the edit-capability count.
|
||||
|
||||
The current 1,000-file corpus contains 999 geometry-bearing STEP documents and
|
||||
one truly empty STEP document. Of the geometry-bearing documents, 996 contain
|
||||
solids and three contain top-level open shells. SurfaceIR preserves all three
|
||||
open shells through `surface_layer.free_shells`; the empty source is rebuilt as
|
||||
an equivalent empty STEP document. Full-corpus validation therefore produces
|
||||
1,000 independent rebuilt STEP documents from 1,000 DesignIR programs.
|
||||
|
||||
The compiler restores exact 3D curves, binds per-face pcurves, preserves seam
|
||||
branches, keeps vertex and edge tolerances independent, groups shells under
|
||||
their declared solid, and keeps top-level open shells outside fake solids.
|
||||
Validation requires matching solid/face/edge topology, volume or surface area,
|
||||
center and bounds. Boolean symmetric difference is the primary solid check.
|
||||
For two numerically unstable coincident-solid booleans, a bidirectional sample
|
||||
contract checks vertices, edge samples and valid face-interior samples instead;
|
||||
the larger measured maximum distance is `0.0000647066 mm`, below the declared
|
||||
`0.0001 mm` limit.
|
||||
|
||||
Every one of the 999 geometry-bearing documents has at least one non-generic,
|
||||
named parameter with a completed real perturbation contract. The private
|
||||
DesignIR corpus contains 1,213 validated named parameter instances:
|
||||
|
||||
- cylindrical cuts: `hole_diameter` and `bore_diameter`;
|
||||
- primary-axis dimensions: `outer_diameter`, `body_length`, and
|
||||
`plate_thickness`;
|
||||
- canonical envelope dimensions: `overall_size_x`, `overall_size_y`, and
|
||||
`overall_size_z`.
|
||||
|
||||
`overall_scale` remains available as a source-independent fallback, but is not
|
||||
counted in the 999/999 named-parameter coverage. Canonical envelope dimensions
|
||||
describe the DesignIR coordinate-system bounds and are not presented as
|
||||
recovered source sketch history. The one truly empty document has no geometry
|
||||
to modify and intentionally exposes no fabricated parameter.
|
||||
|
||||
```bash
|
||||
# Validate the pcurve-bound strategy over the complete corpus.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py validate-folder \
|
||||
input --designir-dir runs/surfaceir \
|
||||
--rebuilt-dir runs/rebuilt-pcurve --boolean \
|
||||
--boundary-strategy exact_3d_pcurve
|
||||
|
||||
# Compare with the exact-boundary fallback and fix the validated choice in each
|
||||
# source-independent DesignIR document.
|
||||
../text-to-cad/.venv/bin/python scripts/surfaceir_pipeline.py select-strategies \
|
||||
--designir-dir runs/surfaceir \
|
||||
--primary-report runs/rebuilt-pcurve/validation-report.json \
|
||||
--fallback-report runs/rebuilt-exact/validation-report.json \
|
||||
--output runs/surfaceir/strategy-selection-final.json
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
- `agents/reconstruction-agent.md`: Agent A authors DesignIR from private
|
||||
@@ -92,11 +240,27 @@ by deterministic code after several independent cases pass.
|
||||
Drop files into `input/`, then use the CAD Python environment:
|
||||
|
||||
```bash
|
||||
# Build a deterministic, family-stratified 800/75/100 train/validation/test split.
|
||||
node scripts/trajectory_pipeline.mjs init
|
||||
|
||||
# Agent A proposes DesignIR; the isolated compiler and Agent B contract reject
|
||||
# geometry or edit failures. Private attempts are written below runs/trajectory/.
|
||||
node scripts/trajectory_pipeline.mjs run \
|
||||
--split train --limit 10 --attempts 3 --max-faces 24
|
||||
|
||||
# Generalize only replay-validated trajectories into non-consumable candidates.
|
||||
node scripts/trajectory_pipeline.mjs distill
|
||||
|
||||
# Compare the same model on the same held-out cases with and without candidates.
|
||||
node scripts/trajectory_pipeline.mjs benchmark \
|
||||
--split validation --limit 10 --attempts 3 --max-faces 24
|
||||
|
||||
scripts/cad-experience extract-folder
|
||||
scripts/cad-experience prepare
|
||||
# The semantic distiller writes runs/review/experience-draft.json.
|
||||
scripts/cad-experience publish \
|
||||
--draft runs/review/experience-draft.json
|
||||
--draft runs/review/experience-draft.json \
|
||||
--validation-report runs/review/heldout-validation.json
|
||||
scripts/cad-experience audit output/library.json
|
||||
|
||||
../text-to-cad/.venv/bin/python scripts/designir_pipeline.py validate part.designir.json
|
||||
@@ -108,3 +272,26 @@ scripts/cad-experience audit output/library.json
|
||||
--teacher teacher.step --designir part.designir.json \
|
||||
--rebuilt rebuilt.step --report acceptance.json
|
||||
```
|
||||
|
||||
Repeated support only creates a candidate. `output/library.json` receives a
|
||||
method only when its validation report identifies at least three independent
|
||||
teacher hashes, every declared edit contract passes, and the held-out A/B score
|
||||
delta against the empty-library baseline is positive across at least ten paired
|
||||
cases. Until then `router_consumable` remains false and CAD Router cannot use
|
||||
the candidate.
|
||||
|
||||
The execution trajectory is the actual unit of learning:
|
||||
|
||||
```text
|
||||
private B-Rep evidence
|
||||
-> proposed DesignIR
|
||||
-> isolated rebuild without source STEP
|
||||
-> geometry and edit-contract verdict
|
||||
-> validator feedback and retry
|
||||
-> successful trajectory candidate
|
||||
```
|
||||
|
||||
Counts, detector confidence, and repeated geometric motifs are not treated as
|
||||
improvement. Improvement is reported only by a positive paired held-out A/B
|
||||
result. `output/distillation-report.json` separates corpus extraction,
|
||||
untrusted candidates, execution-validated trajectories, and promoted methods.
|
||||
|
||||
@@ -8,7 +8,7 @@ meaningfully editable.
|
||||
## Allowed inputs
|
||||
|
||||
- Teacher STEP from protected `input/`.
|
||||
- Candidate DesignIR from `runs/designir/`.
|
||||
- Candidate DesignIR 3.0 from the private run directory.
|
||||
- Reconstructed STEP from `runs/rebuilt/`.
|
||||
- Deterministic compiler version.
|
||||
- Modification test contract.
|
||||
@@ -19,13 +19,25 @@ Do not read Agent A chain-of-thought or informal rationale.
|
||||
|
||||
Original-state geometry:
|
||||
|
||||
- solid and topology counts;
|
||||
- solid, open-shell, face, edge, and diagnostic vertex counts;
|
||||
- bounding-box error;
|
||||
- volume and center-of-mass error;
|
||||
- symmetric-difference volume;
|
||||
- analytic feature counts and spatial relationships when available;
|
||||
- declared validation invariants.
|
||||
|
||||
For solid booleans that are numerically unstable on coincident boundaries, run
|
||||
the registered bidirectional vertex, edge, and face-interior sample-distance
|
||||
contract. Never silently discard the failed boolean result; report both it and
|
||||
the fallback distance.
|
||||
|
||||
Source independence:
|
||||
|
||||
- move or hide the teacher STEP before reconstruction;
|
||||
- reject source paths, STEP payloads, imported base solids, B-Rep blobs, and
|
||||
mesh substitutes in the candidate;
|
||||
- require a deterministic rebuild from DesignIR plus compiler code only.
|
||||
|
||||
Parametric behavior:
|
||||
|
||||
- rebuild every declared perturbation;
|
||||
@@ -34,6 +46,20 @@ Parametric behavior:
|
||||
- preserve constraints and interfaces;
|
||||
- reject invalid, empty, non-solid, self-intersecting, or unstable results.
|
||||
|
||||
For `overall_scale`, require volume to follow the scale factor cubed, center
|
||||
and bounds to follow the declared pivot transform, and solid/face/edge counts
|
||||
to remain stable. Record STEP writer vertex normalization separately instead
|
||||
of confusing geometrically equivalent serialization with feature loss.
|
||||
|
||||
Producing a solid is not sufficient. Each perturbation must be connected to an
|
||||
executable feature, cause an observable target change, and satisfy its
|
||||
non-target preservation checks. Record the promoted experience IDs actually
|
||||
used so improvement can be attributed to a specific method.
|
||||
|
||||
Report geometry and semantics separately. A geometry-only method may enter the
|
||||
SurfaceIR experience library, but it must never increase the semantic-edit
|
||||
promotion count.
|
||||
|
||||
## Required output
|
||||
|
||||
Write one machine-readable report under `runs/acceptance/` with separate geometry,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Agent A — DesignIR Reconstruction
|
||||
# Agent A — Distillation and Semantic Reconstruction
|
||||
|
||||
## Mission
|
||||
|
||||
Turn deterministic private evidence and promoted generalized experience into a
|
||||
complete executable DesignIR 2.0 model. Produce design logic, not copied
|
||||
geometry.
|
||||
Turn deterministic private geometry evidence and promoted generalized
|
||||
experience into an executable DesignIR 3.0 model. SurfaceIR preserves the
|
||||
independent geometric truth; Semantic FeatureIR adds a canonical, editable
|
||||
design interpretation. Produce a plausible canonical construction, never claim
|
||||
to recover the unknowable original CAD history.
|
||||
|
||||
## Allowed inputs
|
||||
|
||||
- One private evidence packet from `runs/evidence/`.
|
||||
- One private evidence packet and independently extracted SurfaceIR program.
|
||||
- The user modification request.
|
||||
- Promoted generalized experience.
|
||||
- The DesignIR schema and supported operation vocabulary.
|
||||
@@ -16,20 +18,27 @@ geometry.
|
||||
## 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 embed B-Rep bytes, STEP text, full meshes, or triangle soups.
|
||||
- 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 `runs/designir/*.designir.json` containing:
|
||||
Write one private DesignIR 3.0 instance program containing:
|
||||
|
||||
- family and reconstruction status;
|
||||
- a backend-neutral SurfaceIR topology/surface layer capable of rebuilding with
|
||||
the teacher removed, including top-level open shells that are not members of
|
||||
any solid;
|
||||
- family and semantic reconstruction status;
|
||||
- coordinate system and datums;
|
||||
- editable parameters and expressions;
|
||||
- sketches and constraints where applicable;
|
||||
- canonical semantic parameter names such as `plate_thickness`,
|
||||
`hole_diameter`, `hole_spacing`, `bolt_circle_diameter`, and `rib_count`
|
||||
whenever the evidence supports them;
|
||||
- inference status, confidence, aliases, affected feature IDs, and an observable
|
||||
edit contract for every parameter inferred from the final B-Rep;
|
||||
- canonical sketches and constraints where the evidence supports them;
|
||||
- ordered features, patterns, and attachments;
|
||||
- construction stages;
|
||||
- edit interface;
|
||||
@@ -41,10 +50,26 @@ If the vocabulary is insufficient, emit
|
||||
`reconstruction_status: unsupported_feature_vocabulary` plus precise
|
||||
`missing_capabilities`. Do not approximate secretly.
|
||||
|
||||
Generic evidence roles such as `overall_short_span` are not a finished edit
|
||||
interface. Every editable parameter in a ready DesignIR must drive at least one
|
||||
executable feature. `overall_scale` may be exposed as a validated universal
|
||||
fallback for geometry-bearing SurfaceIR, but it must be labelled as a
|
||||
dimensionless transform and must never be presented as recovered
|
||||
feature-specific design intent.
|
||||
|
||||
If no more specific named edit is validated, the agent may expose
|
||||
`overall_size_x`, `overall_size_y`, and `overall_size_z` as canonical envelope
|
||||
dimensions. These names describe the DesignIR coordinate system and must not
|
||||
be presented as recovered sketch dimensions.
|
||||
|
||||
## 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. Put rebuild artifacts in
|
||||
`runs/rebuilt/`.
|
||||
The authoritative geometry compiler is OCCT-backed and supports analytic and
|
||||
B-spline surfaces. Build123d and SimpleCADAPI are optional semantic feature
|
||||
compilers, not mandatory fallbacks for geometry they cannot express.
|
||||
|
||||
Geometry equality alone completes only the geometry gate. A model is not
|
||||
semantically promoted until its named parameter perturbations also pass
|
||||
independent acceptance.
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"minimum_independent_cases": 3,
|
||||
"minimum_heldout_ab_pairs": 10,
|
||||
"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
|
||||
"relative_volume_error_max": 1e-7,
|
||||
"relative_surface_area_error_max": 1e-7,
|
||||
"center_of_mass_error_mm_max": 1e-6,
|
||||
"bounding_box_axis_error_mm_max": 1e-6,
|
||||
"symmetric_difference_ratio_max": 1e-8,
|
||||
"bidirectional_surface_sample_distance_mm_max": 0.0001,
|
||||
"solid_face_edge_topology_match_required": true,
|
||||
"vertex_count_normalization_is_diagnostic": true,
|
||||
"parameter_perturbation_success_rate_min": 1.0,
|
||||
"heldout_baseline_score_delta_min_exclusive": 0.0
|
||||
},
|
||||
"publication_guards": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
"teacher_geometry_allowed": false,
|
||||
"automatic_agent_publish_allowed": false
|
||||
"automatic_agent_publish_allowed": false,
|
||||
"per_method_attribution_required": true,
|
||||
"semantic_parameter_edit_contract_required": true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
"backend_hint": {
|
||||
"enum": ["auto", "build123d", "simplecadapi"]
|
||||
},
|
||||
"applied_experience_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"reconstruction_status": {
|
||||
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
|
||||
},
|
||||
@@ -177,6 +182,12 @@
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"valueVector3": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/value"},
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"parameter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -186,6 +197,28 @@
|
||||
"unit": {"enum": ["mm", "deg", "count", "ratio"]},
|
||||
"editable": {"type": "boolean"},
|
||||
"role": {"type": "string"},
|
||||
"semantic_aliases": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"inference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["status", "confidence"],
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": ["user_specified", "inferred_from_final_brep", "validated"]
|
||||
},
|
||||
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"evidence_kind": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"affects_feature_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"minimum": {"type": "number"},
|
||||
"maximum": {"type": "number"}
|
||||
}
|
||||
@@ -228,8 +261,17 @@
|
||||
"depth": {"$ref": "#/$defs/value"},
|
||||
"count": {"$ref": "#/$defs/value"},
|
||||
"pitch_diameter": {"$ref": "#/$defs/value"},
|
||||
"axis": {"type": "string"},
|
||||
"center": {"$ref": "#/$defs/vector3"},
|
||||
"axis": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"$ref": "#/$defs/vector3"}
|
||||
]
|
||||
},
|
||||
"center": {"$ref": "#/$defs/valueVector3"},
|
||||
"anchor": {
|
||||
"enum": ["base_center", "geometric_center"],
|
||||
"default": "base_center"
|
||||
},
|
||||
"host": {"type": "string"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cadset.local/contracts/designir-3.0.schema.json",
|
||||
"title": "DesignIR 3.0",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"designir_kind",
|
||||
"model_id",
|
||||
"units",
|
||||
"document_status",
|
||||
"reconstruction_mode",
|
||||
"semantic_layer",
|
||||
"edit_interface",
|
||||
"validation_contract"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {"const": "3.0"},
|
||||
"designir_kind": {"const": "independent_parametric_cad"},
|
||||
"model_id": {"type": "string", "minLength": 1},
|
||||
"family": {"type": "string", "minLength": 1},
|
||||
"units": {"const": "mm"},
|
||||
"authoring_mode": {
|
||||
"enum": ["semantic_feature_program", "final_brep_inference"]
|
||||
},
|
||||
"backend_hint": {"enum": ["build123d", "simplecadapi", "auto"]},
|
||||
"document_status": {"enum": ["geometry_present", "empty_geometry"]},
|
||||
"reconstruction_mode": {
|
||||
"enum": [
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric",
|
||||
"surface_parametric"
|
||||
]
|
||||
},
|
||||
"semantic_layer": {
|
||||
"type": "object",
|
||||
"required": ["parameters", "datums", "features", "constraints"],
|
||||
"properties": {
|
||||
"reconstruction_status": {
|
||||
"enum": ["ready", "partial", "unsupported_feature_vocabulary"]
|
||||
},
|
||||
"coordinate_system": {"$ref": "#/$defs/coordinateSystem"},
|
||||
"parameters": {"type": "object"},
|
||||
"datums": {"type": "object"},
|
||||
"expressions": {"type": "object"},
|
||||
"sketches": {"type": "array", "items": {"type": "object"}},
|
||||
"features": {"type": "array", "items": {"type": "object"}},
|
||||
"constraints": {"type": "array", "items": {"type": "object"}},
|
||||
"patterns": {"type": "array", "items": {"type": "object"}},
|
||||
"attachments": {"type": "array", "items": {"type": "object"}},
|
||||
"construction_stages": {"type": "array", "items": {"type": "object"}},
|
||||
"missing_capabilities": {"type": "array", "items": {"type": "string"}},
|
||||
"applied_experience_ids": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"surface_layer": {
|
||||
"type": "object",
|
||||
"required": ["vertices", "solids", "free_shells", "surface_vocabulary", "curve_vocabulary"],
|
||||
"properties": {
|
||||
"vertices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "point", "tolerance"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"point": {"$ref": "#/$defs/point3"},
|
||||
"tolerance": {"type": "number", "minimum": 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
"solids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "shells"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"shells": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "faces"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"faces": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/face"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"free_shells": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "faces"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"faces": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/face"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"surface_vocabulary": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["plane", "cylinder", "cone", "sphere", "torus", "bspline"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"curve_vocabulary": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["line", "circle", "ellipse", "bspline", "degenerate"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"reconstruction_strategy": {
|
||||
"type": "object",
|
||||
"required": ["boundary_strategy", "selection_status"],
|
||||
"properties": {
|
||||
"boundary_strategy": {
|
||||
"enum": [
|
||||
"exact_3d",
|
||||
"exact_3d_pcurve",
|
||||
"first_pcurve",
|
||||
"analytic_uv_rect",
|
||||
"uv_rect_all"
|
||||
]
|
||||
},
|
||||
"selection_status": {
|
||||
"enum": ["default", "geometry_validated", "unresolved"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit_interface": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"semantic_parameters",
|
||||
"surface_parameter_groups",
|
||||
"modification_levels"
|
||||
],
|
||||
"properties": {
|
||||
"semantic_parameters": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"surface_parameter_groups": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"modification_levels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": ["semantic_feature", "analytic_surface", "spline_control"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"preserved_interfaces": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"validation_contract": {
|
||||
"type": "object",
|
||||
"required": ["source_independence", "geometry_checks", "edit_checks"],
|
||||
"properties": {
|
||||
"source_independence": {"const": true},
|
||||
"geometry_checks": {"type": "array", "items": {"type": "string"}},
|
||||
"edit_checks": {"type": "array", "items": {"type": "string"}},
|
||||
"invariants": {"type": "array", "items": {"type": "object"}},
|
||||
"perturbations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["parameter", "scale", "expected_change"],
|
||||
"properties": {
|
||||
"parameter": {"type": "string"},
|
||||
"scale": {"type": "number", "exclusiveMinimum": 0},
|
||||
"expected_change": {"type": "string", "minLength": 1},
|
||||
"preserve": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thresholds": {"type": "object"}
|
||||
}
|
||||
},
|
||||
"compiled_surface_provenance": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source",
|
||||
"teacher_geometry_used",
|
||||
"semantic_layer_authoritative"
|
||||
],
|
||||
"properties": {
|
||||
"source": {"const": "generated_step"},
|
||||
"teacher_geometry_used": {"const": false},
|
||||
"semantic_layer_authoritative": {"const": true}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {"reconstruction_mode": {"const": "surface_parametric"}}
|
||||
},
|
||||
"then": {"required": ["surface_layer"]}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"reconstruction_mode": {
|
||||
"enum": [
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["family", "authoring_mode"],
|
||||
"properties": {
|
||||
"semantic_layer": {
|
||||
"required": [
|
||||
"reconstruction_status",
|
||||
"coordinate_system",
|
||||
"parameters",
|
||||
"datums",
|
||||
"expressions",
|
||||
"sketches",
|
||||
"features",
|
||||
"constraints",
|
||||
"patterns",
|
||||
"attachments",
|
||||
"construction_stages"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"reconstruction_mode": {
|
||||
"const": "hybrid_semantic_surface_parametric"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {"required": ["surface_layer", "compiled_surface_provenance"]}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"point3": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{"type": "number"},
|
||||
{"type": "number"},
|
||||
{"type": "number"}
|
||||
],
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"type": "object",
|
||||
"required": ["origin", "x_axis", "y_axis", "z_axis"],
|
||||
"properties": {
|
||||
"origin": {"$ref": "#/$defs/point3"},
|
||||
"x_axis": {"$ref": "#/$defs/point3"},
|
||||
"y_axis": {"$ref": "#/$defs/point3"},
|
||||
"z_axis": {"$ref": "#/$defs/point3"}
|
||||
}
|
||||
},
|
||||
"face": {
|
||||
"type": "object",
|
||||
"required": ["id", "orientation", "surface", "uv_bounds", "wires"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"surface": {"type": "object"},
|
||||
"uv_bounds": {
|
||||
"type": "array",
|
||||
"items": {"type": "number"},
|
||||
"minItems": 4,
|
||||
"maxItems": 4
|
||||
},
|
||||
"wires": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "orientation", "edges"],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"orientation": {"type": "integer"},
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"topology_edge_id",
|
||||
"vertex_ids",
|
||||
"orientation",
|
||||
"curve"
|
||||
],
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"topology_edge_id": {"type": "string"},
|
||||
"vertex_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"maxItems": 2
|
||||
},
|
||||
"orientation": {"type": "integer"},
|
||||
"curve": {"type": "object"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"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": "raised_hub", "operation": "add_cylinder", "radius": {"parameter": "hub_radius"}, "height": {"parameter": "hub_height"}, "center": [0, 0, {"parameter": "plate_thickness"}], "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"}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "step_reconstruction_distillation_result",
|
||||
"extractor_version": "4.0",
|
||||
"status": "all_input_documents_independently_reconstructed_and_all_geometry_documents_have_validated_named_edits",
|
||||
"formal_promoted_experience_count": 12,
|
||||
"formal_promoted_experience_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
],
|
||||
"corpus": {
|
||||
"input_step_count": 1000,
|
||||
"content_unique_step_count": 1000,
|
||||
"single_solid_accepted_count": 975,
|
||||
"rejected_count": 0,
|
||||
"empty_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"multi_solid_document_count": 21
|
||||
},
|
||||
"semantic_candidate_experience": {
|
||||
"total_count": 26,
|
||||
"promoted_count": 8,
|
||||
"router_consumable": false,
|
||||
"by_kind": {
|
||||
"constraint": 4,
|
||||
"dimensionless_distribution": 4,
|
||||
"edit_strategy": 2,
|
||||
"parameter_naming": 4,
|
||||
"reconstruction_grammar": 9
|
||||
},
|
||||
"promoted_ids": [
|
||||
"edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"edit_strategy.semantic_parameter.canonical_directional_envelope"
|
||||
]
|
||||
},
|
||||
"geometry_reconstruction_experience": {
|
||||
"promoted_count": 4,
|
||||
"router_consumable": false,
|
||||
"surfaceir_consumable": true,
|
||||
"promoted_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"failure_repair.surface_topology.preserve_vertex_tolerance"
|
||||
],
|
||||
"note": "These methods have positive held-out geometry A/B evidence. They do not count as proof of semantic parameter-edit capability."
|
||||
},
|
||||
"semantic_parameter_results": [
|
||||
{
|
||||
"canonical_parameter": "plate_thickness",
|
||||
"support": 240,
|
||||
"conditional_confidence": 1.0,
|
||||
"state": "awaiting_positive_heldout_ab"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "hole_count",
|
||||
"support": 396,
|
||||
"conditional_confidence": 0.673469,
|
||||
"state": "requires_internal_feature_polarity_validation"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "hole_diameter",
|
||||
"support": 396,
|
||||
"conditional_confidence": 0.673469,
|
||||
"state": "requires_internal_feature_polarity_validation"
|
||||
},
|
||||
{
|
||||
"canonical_parameter": "bolt_circle_diameter",
|
||||
"support": 121,
|
||||
"conditional_confidence": 0.456604,
|
||||
"state": "awaiting_positive_heldout_ab"
|
||||
}
|
||||
],
|
||||
"legacy_ab_warning": {
|
||||
"completed_pair_count": 15,
|
||||
"planned_pair_count": 50,
|
||||
"baseline_similarity_mean": 69.11,
|
||||
"experience_similarity_mean": 66.64,
|
||||
"score_delta": -2.47,
|
||||
"accepted_as_positive_validation": false
|
||||
},
|
||||
"promotion_gate": {
|
||||
"minimum_independent_cases_per_method": 3,
|
||||
"minimum_heldout_ab_pairs": 10,
|
||||
"edit_contract_pass_rate_required": 1.0,
|
||||
"baseline_score_delta_must_be_positive": true,
|
||||
"evidence_support_alone_can_promote": false
|
||||
},
|
||||
"execution_validated_trajectory_pilot": {
|
||||
"scope": "low_complexity_initial_debugging_subset_not_full_corpus",
|
||||
"attempted_case_count": 6,
|
||||
"replay_validated_case_count": 3,
|
||||
"quarantined_case_count": 3,
|
||||
"derived_candidate_method_count": 1,
|
||||
"officially_promoted_method_count": 0,
|
||||
"candidate_method": "simple cylinder classification, placement, and semantic parameter naming",
|
||||
"heldout_ab": {
|
||||
"pair_count": 2,
|
||||
"baseline_replay_validated_count": 1,
|
||||
"experience_replay_validated_count": 1,
|
||||
"baseline_pass_rate": 0.5,
|
||||
"experience_pass_rate": 0.5,
|
||||
"pass_rate_delta": 0.0,
|
||||
"positive": false,
|
||||
"promotion_eligible": false
|
||||
},
|
||||
"conclusion": "The new loop can produce independently rebuildable editable trajectories, but no capability improvement has yet been demonstrated. The candidate remains unavailable to cad-router."
|
||||
},
|
||||
"designir_3_surface_program": {
|
||||
"input_step_count": 1000,
|
||||
"instance_program_count": 1000,
|
||||
"extraction_failure_count": 0,
|
||||
"empty_geometry_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"instance_program_bytes": 168346362,
|
||||
"supported_surface_vocabulary": [
|
||||
"plane",
|
||||
"cylinder",
|
||||
"cone",
|
||||
"sphere",
|
||||
"torus",
|
||||
"bspline"
|
||||
],
|
||||
"supported_curve_vocabulary": [
|
||||
"line",
|
||||
"circle",
|
||||
"ellipse",
|
||||
"bspline",
|
||||
"degenerate"
|
||||
],
|
||||
"initial_rebuild_pilot": {
|
||||
"case_count": 1000,
|
||||
"validated_nonempty_count": 896,
|
||||
"empty_geometry_count": 4,
|
||||
"failed_count": 100
|
||||
},
|
||||
"heldout_boundary_strategy_ab": {
|
||||
"pair_count": 100,
|
||||
"baseline_geometry_pass_count": 17,
|
||||
"treatment_geometry_pass_count": 95,
|
||||
"improved_pair_count": 78,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.78
|
||||
},
|
||||
"promoted_experience_ids": [
|
||||
"failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"failure_repair.surface_topology.bind_face_pcurves_and_group_shells"
|
||||
],
|
||||
"post_experience_full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"validated_nonempty_count": 970,
|
||||
"empty_geometry_count": 4,
|
||||
"failed_count": 26,
|
||||
"improved_count": 74,
|
||||
"regressed_count": 0
|
||||
},
|
||||
"measured_geometry_bearing_pass_rate": 0.973896,
|
||||
"pcurve_shell_strategy_ab": {
|
||||
"pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 970,
|
||||
"pcurve_primary_geometry_pass_count": 998,
|
||||
"validated_fallback_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 30,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.03
|
||||
},
|
||||
"vertex_tolerance_strategy_ab": {
|
||||
"pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 996,
|
||||
"treatment_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 4,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.004
|
||||
},
|
||||
"current_full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"validated_geometry_document_count": 999,
|
||||
"validated_empty_document_count": 1,
|
||||
"open_shell_document_count": 3,
|
||||
"unresolved_document_count": 0,
|
||||
"all_document_pass_rate": 1.0,
|
||||
"selected_strategy_counts": {
|
||||
"exact_3d_pcurve": 1000
|
||||
},
|
||||
"boolean_fallback_surface_sample_case_count": 2,
|
||||
"maximum_accepted_bidirectional_surface_distance_mm": 0.0000647066
|
||||
},
|
||||
"semantic_edit_contract": {
|
||||
"promoted_experience_count": 8,
|
||||
"status": "positive_local_cut_primary_axis_dimension_and_universal_scale_edits_demonstrated",
|
||||
"semantic_candidate_case_count": 942,
|
||||
"semantic_parameter_instance_count": 5136,
|
||||
"semantic_feature_instance_count": 2502,
|
||||
"heldout_case_count": 50,
|
||||
"heldout_applicable_case_count": 41,
|
||||
"heldout_accepted_case_count": 41,
|
||||
"heldout_edit_pass_rate": 1.0,
|
||||
"primary_boundary_strategy_pass_count": 35,
|
||||
"fallback_rescue_count": 6,
|
||||
"full_corpus_validation": {
|
||||
"case_count": 1000,
|
||||
"baseline_geometry_not_validated_count": 30,
|
||||
"no_executable_semantic_parameter_count": 155,
|
||||
"applicable_edit_case_count": 815,
|
||||
"accepted_edit_case_count": 795,
|
||||
"rejected_edit_case_count": 20,
|
||||
"applicable_edit_pass_rate": 0.97546,
|
||||
"execution_failure_count": 19,
|
||||
"non_target_guard_rejection_count": 1,
|
||||
"post_observer_repair": {
|
||||
"accepted_edit_case_count": 800,
|
||||
"remaining_failed_case_count": 15,
|
||||
"applicable_edit_pass_rate": 0.981595,
|
||||
"rescued_case_count": 5,
|
||||
"regressed_case_count": 0
|
||||
},
|
||||
"post_geometry_strategy_revalidation": {
|
||||
"baseline_geometry_not_validated_count": 0,
|
||||
"no_executable_semantic_parameter_count": 157,
|
||||
"applicable_edit_case_count": 843,
|
||||
"accepted_edit_case_count": 826,
|
||||
"remaining_failed_case_count": 17,
|
||||
"applicable_edit_pass_rate": 0.979834,
|
||||
"newly_accepted_case_count": 26,
|
||||
"regressed_case_count": 0
|
||||
},
|
||||
"post_adaptive_range_and_duplicate_target_repair": {
|
||||
"applicable_edit_case_count": 843,
|
||||
"accepted_edit_case_count": 837,
|
||||
"remaining_failed_case_count": 6,
|
||||
"applicable_edit_pass_rate": 0.992883,
|
||||
"validated_positive_factors": [
|
||||
1.02,
|
||||
1.05,
|
||||
1.1
|
||||
],
|
||||
"newly_accepted_case_count": 11,
|
||||
"regressed_case_count": 0,
|
||||
"duplicate_target_heldout_ab": {
|
||||
"pair_count": 11,
|
||||
"baseline_pass_count": 9,
|
||||
"treatment_pass_count": 11,
|
||||
"regressed_pair_count": 0
|
||||
}
|
||||
},
|
||||
"post_source_independent_overall_scale": {
|
||||
"geometry_bearing_case_count": 996,
|
||||
"accepted_scale_edit_case_count": 996,
|
||||
"rejected_scale_edit_case_count": 0,
|
||||
"scale_edit_pass_rate": 1.0,
|
||||
"baseline_any_validated_edit_case_count": 842,
|
||||
"treatment_any_validated_edit_case_count": 996,
|
||||
"newly_editable_case_count": 154,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "overall_scale is a source-independent minimum editability fallback and does not claim recovery of feature-specific thickness, spacing or count semantics"
|
||||
},
|
||||
"post_primary_axis_affine_dimensions": {
|
||||
"applicable_case_count": 103,
|
||||
"outer_diameter_accepted_count": 103,
|
||||
"outer_diameter_rejected_count": 0,
|
||||
"body_length_accepted_count": 70,
|
||||
"body_length_rejected_count": 0,
|
||||
"plate_thickness_accepted_count": 33,
|
||||
"plate_thickness_rejected_count": 0,
|
||||
"baseline_any_feature_specific_edit_case_count": 842,
|
||||
"treatment_any_feature_specific_edit_case_count": 942,
|
||||
"newly_feature_editable_case_count": 100,
|
||||
"remaining_scale_only_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "axis-affine dimensions are canonical whole-body edits about an inferred primary cylinder axis; they do not claim recovery of the teacher feature tree"
|
||||
},
|
||||
"post_canonical_directional_dimensions": {
|
||||
"applicable_case_count": 56,
|
||||
"overall_size_x_accepted_count": 56,
|
||||
"overall_size_y_accepted_count": 56,
|
||||
"overall_size_z_accepted_count": 55,
|
||||
"rejected_parameter_count": 0,
|
||||
"baseline_any_validated_named_edit_case_count": 942,
|
||||
"treatment_any_validated_named_edit_case_count": 996,
|
||||
"newly_named_editable_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"validated_factor": 1.1,
|
||||
"note": "overall_size_x/y/z are canonical envelope dimensions in the DesignIR coordinate system, not recovered source sketch dimensions"
|
||||
},
|
||||
"final_document_and_edit_coverage": {
|
||||
"input_document_count": 1000,
|
||||
"independently_rebuilt_document_count": 1000,
|
||||
"geometry_present_document_count": 999,
|
||||
"true_empty_document_count": 1,
|
||||
"geometry_documents_with_at_least_one_validated_named_parameter": 999,
|
||||
"validated_named_parameter_instance_count": 1213,
|
||||
"geometry_document_named_edit_coverage": 1.0,
|
||||
"anti_cheat_match_count": 0,
|
||||
"designir_schema_valid_count": 1000,
|
||||
"designir_schema_invalid_count": 0
|
||||
}
|
||||
},
|
||||
"required_next_evidence": [
|
||||
"negative diameter edits",
|
||||
"hole spacing and pitch diameter edits",
|
||||
"plate thickness edits",
|
||||
"additional non-cylindrical feature families",
|
||||
"full-corpus edit acceptance"
|
||||
]
|
||||
},
|
||||
"remaining_primary_failure_clusters": []
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"induction_mode": "replay_validated_designir_with_deterministic_publication",
|
||||
"status": "empty",
|
||||
"status": "active_for_surfaceir",
|
||||
"policy": {
|
||||
"instance_parameters_allowed": false,
|
||||
"absolute_coordinates_allowed": false,
|
||||
@@ -10,20 +10,391 @@
|
||||
"teacher_geometry_allowed": false,
|
||||
"llm_semantic_review_required": true,
|
||||
"draft_evidence_verified": true,
|
||||
"router_consumable": true,
|
||||
"positive_heldout_ab_required": true,
|
||||
"router_consumable": false,
|
||||
"surfaceir_consumable": true,
|
||||
"minimum_support": 3
|
||||
},
|
||||
"corpus_summary": {
|
||||
"accepted_case_count": 0,
|
||||
"accepted_case_count": 1000,
|
||||
"duplicate_case_count": 0,
|
||||
"rejected_case_count": 0,
|
||||
"family_count": 0
|
||||
"family_count": 11
|
||||
},
|
||||
"experience_summary": {
|
||||
"llm_proposal_count": 0,
|
||||
"promoted_experience_count": 0,
|
||||
"candidate_experience_count": 0
|
||||
"llm_proposal_count": 13,
|
||||
"promoted_experience_count": 12,
|
||||
"candidate_experience_count": 1
|
||||
},
|
||||
"experiences": [],
|
||||
"candidate_experiences": []
|
||||
"experiences": [
|
||||
{
|
||||
"id": "failure_repair.surface_boundary.prefer_exact_3d",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"trimmed_surface",
|
||||
"boundary_curve_network"
|
||||
]
|
||||
},
|
||||
"guidance": "Build trimmed-face wires from their exact three-dimensional boundary curves. Retain parametric-space curves for periodic branch and seam repair instead of selecting the first stored branch for every occurrence.",
|
||||
"check": "Require the rebuilt solid, face, edge and vertex counts to match before accepting the boundary strategy.",
|
||||
"support": 100,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 100,
|
||||
"baseline_geometry_pass_count": 17,
|
||||
"treatment_geometry_pass_count": 95,
|
||||
"improved_pair_count": 78,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.78
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_pipeline"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.periodic_torus.rectangular_uv_patch",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"toroidal_surface",
|
||||
"single_outer_wire",
|
||||
"isoparametric_line_boundary"
|
||||
]
|
||||
},
|
||||
"guidance": "Rebuild a singly bounded toroidal face whose parametric boundary consists only of isoparametric lines directly from its stored parameter bounds. Preserve curve-wire reconstruction for non-rectangular toroidal trims.",
|
||||
"check": "Require the toroidal patch topology, volume, center of mass and bounding box to match the teacher before accepting the repair.",
|
||||
"support": 999,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 999,
|
||||
"baseline_geometry_pass_count": 900,
|
||||
"treatment_geometry_pass_count": 963,
|
||||
"improved_pair_count": 63,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.063063
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_pipeline"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_cylindrical_cut.enlarge_diameter",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"internal_cylindrical_face",
|
||||
"canonical_hole_or_bore_axis",
|
||||
"positive_diameter_increase"
|
||||
]
|
||||
},
|
||||
"guidance": "Represent a positive hole or bore diameter edit as one deduplicated cylindrical cut per canonical axis and axial span. Rebuild the source-independent SurfaceIR first, then apply the declared cuts; keep every non-target semantic parameter unchanged.",
|
||||
"check": "Require source independence, a valid solid count, the requested radius on every declared axis, decreasing volume, no added material, and unchanged non-target parameter values.",
|
||||
"support": 41,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "independent_heldout_capability_ab",
|
||||
"heldout_pair_count": 41,
|
||||
"baseline_annotation_only_edit_pass_count": 0,
|
||||
"treatment_edit_pass_count": 41,
|
||||
"improved_pair_count": 41,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 1.0
|
||||
},
|
||||
"consumer_policy": "enabled_for_positive_hole_and_bore_diameter_edits"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.semantic_edit.boundary_strategy_fallback",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"semantic_cylindrical_cut",
|
||||
"invalid_or_missing_target_after_primary_rebuild"
|
||||
]
|
||||
},
|
||||
"guidance": "Validate the preferred analytic UV boundary strategy first, then try exact three-dimensional boundaries and the remaining registered boundary strategies in isolation. Select the first result that preserves solid count, remains valid, realizes every target axis and changes volume in the declared direction.",
|
||||
"check": "Record every attempted strategy and accept a fallback only when deterministic edit checks pass; never accept an empty shell or zero-solid result.",
|
||||
"support": 41,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_heldout_ab",
|
||||
"heldout_pair_count": 41,
|
||||
"primary_strategy_edit_pass_count": 35,
|
||||
"fallback_policy_edit_pass_count": 41,
|
||||
"improved_pair_count": 6,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.146341
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_semantic_edit_acceptance"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.surface_topology.bind_face_pcurves_and_group_shells",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"shared_topology_edge",
|
||||
"stored_face_pcurve",
|
||||
"periodic_or_trimmed_surface"
|
||||
]
|
||||
},
|
||||
"guidance": "Restore every shared edge from its exact three-dimensional curve and bind each stored parametric curve to the corresponding reconstructed face surface. Preserve both parametric branches for seam edges, then assemble all reconstructed shells under their declared DesignIR solid instead of emitting one solid per shell.",
|
||||
"check": "Validate the pcurve-bound reconstruction against the teacher and retain the prior exact-boundary strategy for any regression. Require matching solid, face, edge and vertex counts plus volume, center, bounds and symmetric-difference acceptance before fixing the selected strategy in the source-independent DesignIR.",
|
||||
"support": 996,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_ab_with_validated_fallback",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 970,
|
||||
"treatment_geometry_pass_count": 996,
|
||||
"improved_pair_count": 26,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.026
|
||||
},
|
||||
"consumer_policy": "enabled_as_teacher_validated_surfaceir_strategy"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.surface_topology.preserve_vertex_tolerance",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"shared_topology_vertex",
|
||||
"edge_endpoint_near_curve_within_vertex_tolerance"
|
||||
]
|
||||
},
|
||||
"guidance": "Preserve each DesignIR vertex tolerance independently when constructing shared edges. Update the edge tolerance on the edge itself and never overwrite a shared vertex tolerance with the tolerance of a later incident edge.",
|
||||
"check": "Run the pcurve-bound strategy and the exact-boundary fallback as a paired teacher-side validation. Fix the selected strategy in DesignIR only when solid, face, edge and vertex counts, volume, center, bounds and symmetric difference all pass.",
|
||||
"support": 1000,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_ab_with_validated_fallback",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_geometry_pass_count": 996,
|
||||
"treatment_geometry_pass_count": 1000,
|
||||
"improved_pair_count": 4,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.004
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_in_surfaceir_edge_construction"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_edit.preserve_validated_geometry_strategy",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"source_independent_designir",
|
||||
"teacher_validated_boundary_strategy",
|
||||
"declared_semantic_edit_operation"
|
||||
]
|
||||
},
|
||||
"guidance": "Keep the boundary strategy already validated for the unedited DesignIR as the first semantic-edit reconstruction candidate. Then try the registered pcurve-bound, exact-boundary and analytic fallbacks; do not replace the validated strategy unconditionally with an older global default.",
|
||||
"check": "Accept the edited model only when the requested target cylinders are observed, solid count and validity are preserved, volume changes in the declared direction, non-target parameters remain unchanged and the edited JSON contains no teacher reference.",
|
||||
"support": 843,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_edit_pass_count": 800,
|
||||
"treatment_edit_pass_count": 826,
|
||||
"improved_pair_count": 26,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.026
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_semantic_edit_reconstruction"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.search_validated_safe_range",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"executable_semantic_parameter",
|
||||
"requested_edit_breaks_topology_or_validity"
|
||||
]
|
||||
},
|
||||
"guidance": "Treat editability as a validated interval rather than a single unrestricted value. If the requested positive perturbation fails, test descending meaningful factors and retain the largest value that passes the complete edit contract; record that bound instead of declaring the parameter globally non-editable.",
|
||||
"check": "Every candidate bound must generate an edited STEP and pass target observation, solid validity and count, declared volume direction, non-target stability and source-independence checks.",
|
||||
"support": 843,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 1000,
|
||||
"baseline_edit_pass_count": 826,
|
||||
"treatment_edit_pass_count": 835,
|
||||
"improved_pair_count": 9,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.009
|
||||
},
|
||||
"consumer_policy": "enabled_for_semantic_edit_range_validation"
|
||||
},
|
||||
{
|
||||
"id": "failure_repair.semantic_edit.merge_duplicate_cylindrical_targets",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"multiple_target_faces",
|
||||
"coincident_cylinder_axis_anchor_and_axial_interval"
|
||||
]
|
||||
},
|
||||
"guidance": "Merge target faces that describe the same physical cylindrical cut by comparing canonical axis, spatial anchor and absolute axial interval within model tolerance. Execute one cut, retain all source face identifiers for traceability, and validate one physical target instead of counting duplicate B-Rep faces as separate holes.",
|
||||
"check": "The merged operation must preserve the requested radius, solid count and validity while passing the same source-independence and non-target guards as an unmerged edit.",
|
||||
"support": 11,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_failure_cluster_ab",
|
||||
"heldout_pair_count": 11,
|
||||
"baseline_edit_pass_count": 9,
|
||||
"treatment_edit_pass_count": 11,
|
||||
"improved_pair_count": 2,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.181818
|
||||
},
|
||||
"consumer_policy": "enabled_by_default_for_cylindrical_edit_binding"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.source_independent_overall_scale",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"source_independent_designir",
|
||||
"geometry_present",
|
||||
"semantic_feature_binding_may_be_unavailable"
|
||||
]
|
||||
},
|
||||
"guidance": "Expose a dimensionless overall_scale parameter about a deterministic datum derived from the independent SurfaceIR bounds. Compile it as a uniform transform of the rebuilt shape, never as a teacher STEP or embedded B-Rep dependency. Treat this as a minimum editability fallback, not as evidence that feature-specific parameters such as thickness, spacing or count were recovered.",
|
||||
"check": "Require valid solids, preserved solid/face/edge counts, volume scaling by the factor cubed, transformed center and bounds within tolerance, unchanged non-target parameter values and a source-independent edited JSON. Record vertex-count normalization after STEP serialization as a diagnostic rather than rejecting otherwise equivalent topology.",
|
||||
"support": 996,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_full_corpus_capability_ab",
|
||||
"heldout_pair_count": 996,
|
||||
"baseline_any_validated_edit_count": 842,
|
||||
"treatment_any_validated_edit_count": 996,
|
||||
"improved_pair_count": 154,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.154618,
|
||||
"standalone_scale_acceptance_count": 996,
|
||||
"standalone_scale_rejection_count": 0
|
||||
},
|
||||
"consumer_policy": "enabled_as_fallback_for_geometry_bearing_surfaceir"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.primary_axis_affine_dimensions",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"primary_external_cylinder",
|
||||
"source_independent_designir",
|
||||
"no_validated_feature_specific_edit"
|
||||
]
|
||||
},
|
||||
"guidance": "Bind outer_diameter to a radial affinity about the inferred primary cylinder axis, and bind body_length or plate_thickness to an axial affinity about the target cylinder midpoint. Preserve the orthogonal dimension, expose the operation explicitly in DesignIR, and treat it as a canonical whole-body dimension edit rather than recovered source history.",
|
||||
"check": "Rebuild without the teacher, apply the declared axis-affine transform, write and reread STEP, independently remeasure the target face boundary along or around the declared axis, and require the requested dimension, valid solids, stable solid/face/edge counts, unchanged non-target parameter values, and normalized expected volume, center and bounds.",
|
||||
"support": 103,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_applicable_corpus_capability_ab",
|
||||
"heldout_outer_diameter_pair_count": 103,
|
||||
"heldout_outer_diameter_pass_count": 103,
|
||||
"heldout_body_length_pair_count": 70,
|
||||
"heldout_body_length_pass_count": 70,
|
||||
"heldout_plate_thickness_pair_count": 33,
|
||||
"heldout_plate_thickness_pass_count": 33,
|
||||
"baseline_any_feature_specific_edit_count": 842,
|
||||
"treatment_any_feature_specific_edit_count": 942,
|
||||
"improved_case_count": 100,
|
||||
"regressed_case_count": 0,
|
||||
"pass_rate_delta": 0.100402
|
||||
},
|
||||
"consumer_policy": "enabled_for_primary_cylindrical_parts_without_a_validated_local_feature_edit"
|
||||
},
|
||||
{
|
||||
"id": "edit_strategy.semantic_parameter.canonical_directional_envelope",
|
||||
"kind": "edit_strategy",
|
||||
"scope": [
|
||||
"surfaceir_semantic_editing"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"geometry_present",
|
||||
"no_validated_named_edit",
|
||||
"nonzero_global_axis_extent"
|
||||
]
|
||||
},
|
||||
"guidance": "Expose nonzero global-axis bounds as overall_size_x, overall_size_y and overall_size_z about a deterministic bounds-center datum. Compile one requested dimension as an axis affinity that leaves the other two global coordinates unchanged. Label these as canonical envelope dimensions, never as recovered sketch constraints or local feature intent.",
|
||||
"check": "Write and reread the independently rebuilt STEP, remeasure the complete vertex bounds along the requested axis, require the requested extent, valid geometry, stable solid/face/edge counts, unchanged non-target parameter values, and normalized expected volume, center and bounds.",
|
||||
"support": 56,
|
||||
"confidence": 1.0,
|
||||
"validation": {
|
||||
"kind": "paired_applicable_corpus_capability_ab",
|
||||
"heldout_x_pair_count": 56,
|
||||
"heldout_x_pass_count": 56,
|
||||
"heldout_y_pair_count": 56,
|
||||
"heldout_y_pass_count": 56,
|
||||
"heldout_z_pair_count": 55,
|
||||
"heldout_z_pass_count": 55,
|
||||
"baseline_any_validated_named_edit_count": 942,
|
||||
"treatment_any_validated_named_edit_count": 996,
|
||||
"improved_case_count": 54,
|
||||
"regressed_case_count": 0,
|
||||
"pass_rate_delta": 0.054217
|
||||
},
|
||||
"consumer_policy": "enabled_for_geometry_without_a_more_specific_validated_parameter"
|
||||
}
|
||||
],
|
||||
"candidate_experiences": [
|
||||
{
|
||||
"id": "failure_repair.surface_topology.preserve_free_shell_documents",
|
||||
"kind": "failure_repair",
|
||||
"scope": [
|
||||
"surfaceir_reconstruction"
|
||||
],
|
||||
"when": {
|
||||
"features": [
|
||||
"step_document_without_solid",
|
||||
"top_level_open_shell"
|
||||
]
|
||||
},
|
||||
"support": 3,
|
||||
"required_heldout_pair_count": 10,
|
||||
"completed_heldout_pair_count": 3,
|
||||
"remaining_heldout_pair_count": 7,
|
||||
"promotion_state": "candidate",
|
||||
"measured_result": {
|
||||
"baseline_independent_rebuild_count": 0,
|
||||
"treatment_independent_rebuild_count": 3,
|
||||
"regressed_pair_count": 0
|
||||
},
|
||||
"consumer_policy": "visible_for_review_but_not_promoted_as_generalized_experience"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile and validate DesignIR 2.0 without depending on teacher geometry."""
|
||||
"""Compile and validate semantic DesignIR 3.0 without teacher geometry.
|
||||
|
||||
Legacy DesignIR 2.0 documents remain accepted and can be migrated explicitly.
|
||||
Surface-parametric DesignIR 3.0 documents are handled by surfaceir_pipeline.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
@@ -15,11 +21,23 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from build123d import Align, Box, CenterOf, Cylinder, Part, Pos, export_step, import_step
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
CenterOf,
|
||||
Cylinder,
|
||||
Part,
|
||||
Plane,
|
||||
Pos,
|
||||
export_step,
|
||||
import_step,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_VERSION = "2.0"
|
||||
DESIGNIR_KIND = "executable_parametric_design"
|
||||
SCHEMA_VERSION = "3.0"
|
||||
DESIGNIR_KIND = "independent_parametric_cad"
|
||||
LEGACY_SCHEMA_VERSION = "2.0"
|
||||
LEGACY_DESIGNIR_KIND = "executable_parametric_design"
|
||||
SUPPORTED_OPERATIONS = {
|
||||
"extrude_circle",
|
||||
"extrude_rectangle",
|
||||
@@ -138,14 +156,14 @@ def _vector3(value: Any, label: str) -> None:
|
||||
raise DesignIRError(f"{label} must be a three-number vector")
|
||||
|
||||
|
||||
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
def _validate_compiler_payload(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:
|
||||
if payload["schema_version"] != LEGACY_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["designir_kind"] != LEGACY_DESIGNIR_KIND:
|
||||
raise DesignIRError(f"designir_kind must be {LEGACY_DESIGNIR_KIND}")
|
||||
if payload["units"] != "mm":
|
||||
raise DesignIRError("Only millimetres are supported")
|
||||
if payload["reconstruction_status"] not in {
|
||||
@@ -196,14 +214,150 @@ def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
unknown = sorted(set(editable) - set(parameters))
|
||||
if unknown:
|
||||
raise DesignIRError("Unknown editable parameters: " + ", ".join(unknown))
|
||||
disconnected = sorted(
|
||||
name
|
||||
for name in editable
|
||||
if not parameters[name].get("editable")
|
||||
or _parameter_reference_count(features, name) == 0
|
||||
)
|
||||
if disconnected:
|
||||
raise DesignIRError(
|
||||
"Editable parameters must drive at least one feature: "
|
||||
+ ", ".join(disconnected)
|
||||
)
|
||||
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')}"
|
||||
)
|
||||
if not str(test.get("expected_change") or "").strip():
|
||||
raise DesignIRError("Every perturbation requires expected_change")
|
||||
return payload
|
||||
|
||||
|
||||
def migrate_designir_2_to_3(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert the legacy flat semantic contract into unified DesignIR 3.0."""
|
||||
legacy = _validate_compiler_payload(copy.deepcopy(payload))
|
||||
editable = list(legacy["edit_interface"].get("editable_parameters", []))
|
||||
validation = legacy["validation_contract"]
|
||||
semantic_layer = {
|
||||
"reconstruction_status": legacy["reconstruction_status"],
|
||||
"coordinate_system": legacy["coordinate_system"],
|
||||
"datums": legacy["datums"],
|
||||
"parameters": legacy["parameters"],
|
||||
"expressions": legacy["expressions"],
|
||||
"sketches": legacy["sketches"],
|
||||
"constraints": legacy["constraints"],
|
||||
"features": legacy["features"],
|
||||
"patterns": legacy["patterns"],
|
||||
"attachments": legacy["attachments"],
|
||||
"construction_stages": legacy["construction_stages"],
|
||||
"missing_capabilities": legacy.get("missing_capabilities", []),
|
||||
"applied_experience_ids": legacy.get("applied_experience_ids", []),
|
||||
}
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"designir_kind": DESIGNIR_KIND,
|
||||
"model_id": legacy["model_id"],
|
||||
"family": legacy["family"],
|
||||
"units": legacy["units"],
|
||||
"document_status": "geometry_present",
|
||||
"reconstruction_mode": "fully_semantic_parametric",
|
||||
"authoring_mode": "semantic_feature_program",
|
||||
"backend_hint": legacy.get("backend_hint", "build123d"),
|
||||
"semantic_layer": semantic_layer,
|
||||
"edit_interface": {
|
||||
"semantic_parameters": editable,
|
||||
"surface_parameter_groups": [],
|
||||
"modification_levels": ["semantic_feature"],
|
||||
"preserved_interfaces": legacy["edit_interface"].get(
|
||||
"preserved_interfaces", []
|
||||
),
|
||||
},
|
||||
"validation_contract": {
|
||||
"source_independence": True,
|
||||
"geometry_checks": validation.get(
|
||||
"geometry_checks",
|
||||
["solid_count", "bounding_box", "volume", "center_of_mass"],
|
||||
),
|
||||
"edit_checks": validation.get(
|
||||
"edit_checks",
|
||||
["parameter_perturbation", "constraint_preservation"],
|
||||
),
|
||||
"invariants": validation.get("invariants", []),
|
||||
"perturbations": validation.get("perturbations", []),
|
||||
"thresholds": validation.get("thresholds", {}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return the stable flat representation consumed by both CAD backends."""
|
||||
version = payload.get("schema_version")
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
return _validate_compiler_payload(copy.deepcopy(payload))
|
||||
if version != SCHEMA_VERSION or payload.get("designir_kind") != DESIGNIR_KIND:
|
||||
raise DesignIRError(
|
||||
"DesignIR must be legacy 2.0 or independent_parametric_cad 3.0"
|
||||
)
|
||||
mode = payload.get("reconstruction_mode")
|
||||
if mode == "surface_parametric":
|
||||
raise DesignIRError(
|
||||
"Surface-parametric DesignIR must be rebuilt with surfaceir_pipeline.py"
|
||||
)
|
||||
if mode not in {
|
||||
"fully_semantic_parametric",
|
||||
"hybrid_semantic_surface_parametric",
|
||||
}:
|
||||
raise DesignIRError(f"Unsupported DesignIR 3.0 reconstruction_mode: {mode}")
|
||||
if payload.get("units") != "mm":
|
||||
raise DesignIRError("Only millimetres are supported")
|
||||
forbidden = find_forbidden(payload.get("semantic_layer", {}))
|
||||
if forbidden:
|
||||
raise DesignIRError(f"Teacher geometry dependency is forbidden at {forbidden}")
|
||||
semantic = payload.get("semantic_layer")
|
||||
if not isinstance(semantic, dict):
|
||||
raise DesignIRError("semantic_layer must be an object")
|
||||
edit_interface = payload.get("edit_interface", {})
|
||||
validation = payload.get("validation_contract", {})
|
||||
flat = {
|
||||
"schema_version": LEGACY_SCHEMA_VERSION,
|
||||
"designir_kind": LEGACY_DESIGNIR_KIND,
|
||||
"model_id": payload.get("model_id"),
|
||||
"family": payload.get("family", "general_mechanical_part"),
|
||||
"units": payload.get("units"),
|
||||
"reconstruction_status": semantic.get("reconstruction_status", "ready"),
|
||||
"coordinate_system": semantic.get("coordinate_system"),
|
||||
"datums": semantic.get("datums", {}),
|
||||
"parameters": semantic.get("parameters", {}),
|
||||
"expressions": semantic.get("expressions", {}),
|
||||
"sketches": semantic.get("sketches", []),
|
||||
"constraints": semantic.get("constraints", []),
|
||||
"features": semantic.get("features", []),
|
||||
"patterns": semantic.get("patterns", []),
|
||||
"attachments": semantic.get("attachments", []),
|
||||
"construction_stages": semantic.get("construction_stages", []),
|
||||
"edit_interface": {
|
||||
"editable_parameters": edit_interface.get("semantic_parameters", []),
|
||||
"preserved_interfaces": edit_interface.get("preserved_interfaces", []),
|
||||
},
|
||||
"validation_contract": {
|
||||
"invariants": validation.get("invariants", []),
|
||||
"perturbations": validation.get("perturbations", []),
|
||||
"thresholds": validation.get("thresholds", {}),
|
||||
},
|
||||
"backend_hint": payload.get("backend_hint", "build123d"),
|
||||
"missing_capabilities": semantic.get("missing_capabilities", []),
|
||||
"applied_experience_ids": semantic.get("applied_experience_ids", []),
|
||||
}
|
||||
return _validate_compiler_payload(flat)
|
||||
|
||||
|
||||
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate either supported version and return its compiler representation."""
|
||||
return compiler_payload(payload)
|
||||
|
||||
|
||||
def _safe_expression(expression: str, values: dict[str, float]) -> float:
|
||||
try:
|
||||
tree = ast.parse(expression, mode="eval")
|
||||
@@ -261,13 +415,69 @@ def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
|
||||
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 resolve_center(
|
||||
value: Any, values: dict[str, float], label: str
|
||||
) -> list[float]:
|
||||
if not isinstance(value, list) or len(value) != 3:
|
||||
raise DesignIRError(f"{label} must contain three values")
|
||||
return [
|
||||
resolve_value(component, values, f"{label}[{index}]")
|
||||
for index, component in enumerate(value)
|
||||
]
|
||||
|
||||
|
||||
def feature_axis(payload: dict[str, Any], feature: dict[str, Any]) -> list[float]:
|
||||
reference = feature.get("axis", "primary_axis")
|
||||
if isinstance(reference, list):
|
||||
axis = reference
|
||||
elif reference in {"x", "y", "z"}:
|
||||
axis = {
|
||||
"x": [1.0, 0.0, 0.0],
|
||||
"y": [0.0, 1.0, 0.0],
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
}[reference]
|
||||
else:
|
||||
datum = payload.get("datums", {}).get(str(reference), {})
|
||||
axis = datum.get("axis")
|
||||
if isinstance(axis, str) and axis in {"x", "y", "z"}:
|
||||
axis = {
|
||||
"x": [1.0, 0.0, 0.0],
|
||||
"y": [0.0, 1.0, 0.0],
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
}[axis]
|
||||
_vector3(axis, f"{feature.get('id', 'feature')}.axis")
|
||||
length = math.sqrt(sum(float(value) ** 2 for value in axis))
|
||||
if length <= 1e-12:
|
||||
raise DesignIRError(f"{feature.get('id', 'feature')}.axis is zero")
|
||||
return [float(value) / length for value in axis]
|
||||
|
||||
|
||||
def _axis_is_z(axis: list[float]) -> bool:
|
||||
return abs(axis[0]) <= 1e-9 and abs(axis[1]) <= 1e-9 and axis[2] > 0.0
|
||||
|
||||
|
||||
def feature_base_center(
|
||||
feature: dict[str, Any],
|
||||
center: list[float],
|
||||
axis: list[float],
|
||||
height: float,
|
||||
) -> list[float]:
|
||||
"""Resolve the primitive's bottom-face center from an explicit anchor."""
|
||||
anchor = str(feature.get("anchor", "base_center"))
|
||||
if anchor == "base_center":
|
||||
return center
|
||||
if anchor == "geometric_center":
|
||||
return [
|
||||
float(center[index]) - float(axis[index]) * height / 2.0
|
||||
for index in range(3)
|
||||
]
|
||||
raise DesignIRError(
|
||||
f"{feature.get('id', 'feature')}.anchor must be base_center or geometric_center"
|
||||
)
|
||||
|
||||
|
||||
def build_shape(payload: dict[str, Any]) -> Part:
|
||||
validate_designir(payload)
|
||||
payload = validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
@@ -278,32 +488,39 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
axis = feature_axis(payload, 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),
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
base_center = feature_base_center(feature, center, axis, height)
|
||||
tool = Plane(origin=base_center, z_dir=axis) * 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),
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
base_center = feature_base_center(feature, center, axis, height)
|
||||
tool = Plane(origin=base_center, z_dir=axis) * 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 not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} hole cutting currently requires positive Z axis"
|
||||
)
|
||||
if shape is None:
|
||||
raise DesignIRError(f"{feature['id']} requires a host solid")
|
||||
diameter = resolve_value(
|
||||
@@ -314,8 +531,11 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
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")
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
@@ -352,7 +572,7 @@ def build_shape(payload: dict[str, Any]) -> Part:
|
||||
|
||||
|
||||
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
validate_designir(payload)
|
||||
payload = validate_designir(payload)
|
||||
if payload["reconstruction_status"] != "ready":
|
||||
missing = ", ".join(payload.get("missing_capabilities", [])) or "unspecified"
|
||||
raise DesignIRError(
|
||||
@@ -373,7 +593,7 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
with scad.GraphSession() as session:
|
||||
for feature in payload["features"]:
|
||||
operation = feature["operation"]
|
||||
_axis_is_z(feature)
|
||||
axis = feature_axis(payload, feature)
|
||||
if operation in {"extrude_circle", "add_cylinder"}:
|
||||
radius = resolve_value(
|
||||
feature["radius"], values, f"{feature['id']}.radius"
|
||||
@@ -381,14 +601,17 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
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))
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
point = tuple(feature_base_center(feature, center, axis, height))
|
||||
tool = scad.make_cylinder_rsolid(
|
||||
radius=radius,
|
||||
height=height,
|
||||
bottom_face_center=point,
|
||||
axis=(0.0, 0.0, 1.0),
|
||||
axis=tuple(axis),
|
||||
)
|
||||
shape = (
|
||||
tool
|
||||
@@ -402,6 +625,10 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation == "extrude_rectangle":
|
||||
if not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} SimpleCAD box currently requires positive Z axis"
|
||||
)
|
||||
width = resolve_value(
|
||||
feature["width"], values, f"{feature['id']}.width"
|
||||
)
|
||||
@@ -411,9 +638,12 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
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))
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
point = tuple(feature_base_center(feature, center, axis, height))
|
||||
tool = scad.make_box_rsolid(
|
||||
width=width,
|
||||
height=depth,
|
||||
@@ -432,14 +662,21 @@ def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
|
||||
else max(z_max, point[2] + height)
|
||||
)
|
||||
elif operation in {"through_hole", "polar_hole_pattern"}:
|
||||
if not _axis_is_z(axis):
|
||||
raise DesignIRError(
|
||||
f"{feature['id']} SimpleCAD hole currently requires positive Z axis"
|
||||
)
|
||||
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")
|
||||
center = resolve_center(
|
||||
feature.get("center", [0, 0, 0]),
|
||||
values,
|
||||
f"{feature['id']}.center",
|
||||
)
|
||||
centers = [(float(center[0]), float(center[1]))]
|
||||
else:
|
||||
count = int(
|
||||
@@ -532,10 +769,69 @@ def compile_designir(
|
||||
}
|
||||
|
||||
|
||||
def materialize_hybrid_designir(
|
||||
payload: dict[str, Any], generated_step: Path
|
||||
) -> dict[str, Any]:
|
||||
"""Attach a compiled SurfaceIR snapshot to a semantic DesignIR 3.0 model.
|
||||
|
||||
The semantic feature program remains authoritative for future edits. The
|
||||
SurfaceIR snapshot is extracted only from the newly generated STEP, never
|
||||
from teacher geometry.
|
||||
"""
|
||||
semantic = (
|
||||
migrate_designir_2_to_3(payload)
|
||||
if payload.get("schema_version") == LEGACY_SCHEMA_VERSION
|
||||
else copy.deepcopy(payload)
|
||||
)
|
||||
compiler_payload(semantic)
|
||||
surface_script = Path(__file__).with_name("surfaceir_pipeline.py")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_designir_surfaceir_pipeline", surface_script
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise DesignIRError("Cannot load SurfaceIR compiler")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
extracted = module.extract_surfaceir(generated_step.expanduser().resolve())
|
||||
semantic["surface_layer"] = extracted["surface_layer"]
|
||||
semantic["reconstruction_strategy"] = extracted.get(
|
||||
"reconstruction_strategy",
|
||||
{
|
||||
"boundary_strategy": "exact_3d",
|
||||
"selection_status": "default",
|
||||
},
|
||||
)
|
||||
semantic["document_status"] = extracted.get(
|
||||
"document_status", "geometry_present"
|
||||
)
|
||||
semantic["reconstruction_mode"] = "hybrid_semantic_surface_parametric"
|
||||
edit_interface = semantic["edit_interface"]
|
||||
edit_interface["surface_parameter_groups"] = extracted.get(
|
||||
"edit_interface", {}
|
||||
).get("surface_parameter_groups", [])
|
||||
edit_interface["modification_levels"] = list(
|
||||
dict.fromkeys(
|
||||
list(edit_interface.get("modification_levels", []))
|
||||
+ ["semantic_feature", "analytic_surface", "spline_control"]
|
||||
)
|
||||
)
|
||||
semantic["compiled_surface_provenance"] = {
|
||||
"source": "generated_step",
|
||||
"teacher_geometry_used": False,
|
||||
"semantic_layer_authoritative": True,
|
||||
}
|
||||
return semantic
|
||||
|
||||
|
||||
def anti_cheat(payload: dict[str, Any], generator: Path | None = None) -> dict[str, Any]:
|
||||
validate_designir(payload)
|
||||
violations: list[str] = []
|
||||
forbidden = find_forbidden(payload)
|
||||
inspected = (
|
||||
payload.get("semantic_layer", {})
|
||||
if payload.get("schema_version") == SCHEMA_VERSION
|
||||
else payload
|
||||
)
|
||||
forbidden = find_forbidden(inspected)
|
||||
if forbidden:
|
||||
violations.append(f"DesignIR teacher dependency at {forbidden}")
|
||||
if generator:
|
||||
@@ -594,14 +890,30 @@ def _symmetric_difference_volume(first: Part, second: Part) -> float:
|
||||
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
|
||||
|
||||
|
||||
def _parameter_reference_count(value: Any, parameter: str) -> int:
|
||||
if isinstance(value, dict):
|
||||
direct = 1 if value.get("parameter") == parameter else 0
|
||||
return direct + sum(
|
||||
_parameter_reference_count(child, parameter)
|
||||
for child in value.values()
|
||||
)
|
||||
if isinstance(value, list):
|
||||
return sum(_parameter_reference_count(child, parameter) for child in value)
|
||||
return 0
|
||||
|
||||
|
||||
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())
|
||||
payload = validate_designir(payload)
|
||||
teacher_resolved = teacher_path.expanduser().resolve()
|
||||
rebuilt_resolved = rebuilt_path.expanduser().resolve()
|
||||
if teacher_resolved == rebuilt_resolved:
|
||||
raise DesignIRError("Teacher and rebuilt STEP must be different files")
|
||||
teacher = import_step(teacher_resolved)
|
||||
rebuilt = import_step(rebuilt_resolved)
|
||||
teacher_facts = geometry_facts(teacher)
|
||||
rebuilt_facts = geometry_facts(rebuilt)
|
||||
volume_base = max(teacher_facts["volume_mm3"], 1e-12)
|
||||
@@ -640,6 +952,38 @@ def acceptance_report(
|
||||
try:
|
||||
output = Path(temporary) / f"perturbation-{index}.step"
|
||||
result = compile_designir(modified, output)
|
||||
modified_shape = import_step(output)
|
||||
changed_volume = _symmetric_difference_volume(
|
||||
rebuilt, modified_shape
|
||||
)
|
||||
change_ratio = changed_volume / max(
|
||||
rebuilt_facts["volume_mm3"], 1e-12
|
||||
)
|
||||
reference_count = _parameter_reference_count(
|
||||
payload.get("features", []), name
|
||||
)
|
||||
preserved = {
|
||||
preserved_name: (
|
||||
preserved_name in payload["parameters"]
|
||||
and preserved_name in modified["parameters"]
|
||||
and payload["parameters"][preserved_name]["value"]
|
||||
== modified["parameters"][preserved_name]["value"]
|
||||
)
|
||||
for preserved_name in test.get("preserve", [])
|
||||
}
|
||||
expected_change = str(test.get("expected_change") or "")
|
||||
expected_change_observed = change_ratio > 1e-9
|
||||
if expected_change == "overall_height_increases":
|
||||
expected_change_observed = (
|
||||
result["facts"]["size_mm"][2]
|
||||
> rebuilt_facts["size_mm"][2] + 1e-9
|
||||
)
|
||||
success = (
|
||||
result["facts"]["solid_count"] > 0
|
||||
and reference_count > 0
|
||||
and expected_change_observed
|
||||
and all(preserved.values())
|
||||
)
|
||||
perturbations.append(
|
||||
{
|
||||
"parameter": name,
|
||||
@@ -647,7 +991,11 @@ def acceptance_report(
|
||||
"modified_value": modified["parameters"][name]["value"],
|
||||
"expected_change": test.get("expected_change"),
|
||||
"preserve": test.get("preserve", []),
|
||||
"success": result["facts"]["solid_count"] > 0,
|
||||
"parameter_reference_count": reference_count,
|
||||
"geometry_change_ratio": change_ratio,
|
||||
"expected_change_observed": expected_change_observed,
|
||||
"preserved_parameter_checks": preserved,
|
||||
"success": success,
|
||||
"facts": result["facts"],
|
||||
}
|
||||
)
|
||||
@@ -678,6 +1026,14 @@ def acceptance_report(
|
||||
"schema_version": "1.0",
|
||||
"report_kind": "independent_designir_acceptance",
|
||||
"model_id": payload["model_id"],
|
||||
"teacher_sha256": hashlib.sha256(teacher_resolved.read_bytes()).hexdigest(),
|
||||
"applied_experience_ids": sorted(
|
||||
{
|
||||
str(item)
|
||||
for item in payload.get("applied_experience_ids", [])
|
||||
if str(item).strip()
|
||||
}
|
||||
),
|
||||
"geometry": geometry,
|
||||
"feature_semantics": {
|
||||
"declared_feature_count": len(payload["features"]),
|
||||
@@ -700,15 +1056,34 @@ def acceptance_report(
|
||||
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
|
||||
methods: dict[str, set[str]] = {}
|
||||
for item in passing:
|
||||
source_hash = str(item.get("teacher_sha256") or "")
|
||||
if not source_hash:
|
||||
continue
|
||||
for experience_id in item.get("applied_experience_ids", []):
|
||||
methods.setdefault(str(experience_id), set()).add(source_hash)
|
||||
validated_methods = sorted(
|
||||
experience_id
|
||||
for experience_id, source_hashes in methods.items()
|
||||
if len(source_hashes) >= minimum
|
||||
)
|
||||
independent_sources = len(
|
||||
{
|
||||
source_hash
|
||||
for source_hashes in methods.values()
|
||||
for source_hash in source_hashes
|
||||
}
|
||||
)
|
||||
eligible = bool(validated_methods)
|
||||
return {
|
||||
"eligible": eligible,
|
||||
"independent_case_count": len(model_ids),
|
||||
"independent_case_count": independent_sources,
|
||||
"passing_case_count": len(passing),
|
||||
"required_case_count": minimum,
|
||||
"validated_experience_ids": validated_methods,
|
||||
"next_state": "promoted" if eligible else "candidate",
|
||||
}
|
||||
|
||||
@@ -720,6 +1095,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
validate = commands.add_parser("validate")
|
||||
validate.add_argument("designir", type=Path)
|
||||
|
||||
migrate = commands.add_parser("migrate")
|
||||
migrate.add_argument("designir", type=Path)
|
||||
migrate.add_argument("--output", type=Path, required=True)
|
||||
|
||||
guard = commands.add_parser("anti-cheat")
|
||||
guard.add_argument("designir", type=Path)
|
||||
guard.add_argument("--generator", type=Path)
|
||||
@@ -736,6 +1115,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
isolated.add_argument("--output", type=Path, required=True)
|
||||
isolated.add_argument("--backend", choices=("build123d", "simplecadapi"))
|
||||
|
||||
materialize = commands.add_parser("materialize-hybrid")
|
||||
materialize.add_argument("designir", type=Path)
|
||||
materialize.add_argument("--step", type=Path, required=True)
|
||||
materialize.add_argument("--output", type=Path, required=True)
|
||||
|
||||
accept = commands.add_parser("accept")
|
||||
accept.add_argument("--teacher", type=Path, required=True)
|
||||
accept.add_argument("--designir", type=Path, required=True)
|
||||
@@ -752,12 +1136,29 @@ 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))
|
||||
original = read_json(args.designir)
|
||||
payload = validate_designir(original)
|
||||
result = {
|
||||
"valid": True,
|
||||
"model_id": payload["model_id"],
|
||||
"input_schema_version": original.get("schema_version"),
|
||||
"supported_operations": sorted(SUPPORTED_OPERATIONS),
|
||||
}
|
||||
elif args.command == "migrate":
|
||||
original = read_json(args.designir)
|
||||
migrated = (
|
||||
migrate_designir_2_to_3(original)
|
||||
if original.get("schema_version") == LEGACY_SCHEMA_VERSION
|
||||
else original
|
||||
)
|
||||
compiler_payload(migrated)
|
||||
write_json(args.output, migrated)
|
||||
result = {
|
||||
"valid": True,
|
||||
"output": str(args.output.expanduser().resolve()),
|
||||
"schema_version": migrated["schema_version"],
|
||||
"reconstruction_mode": migrated["reconstruction_mode"],
|
||||
}
|
||||
elif args.command == "anti-cheat":
|
||||
result = anti_cheat(read_json(args.designir), args.generator)
|
||||
elif args.command == "compile":
|
||||
@@ -768,6 +1169,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
result = isolated_rebuild(
|
||||
args.designir, args.output, backend=args.backend
|
||||
)
|
||||
elif args.command == "materialize-hybrid":
|
||||
materialized = materialize_hybrid_designir(
|
||||
read_json(args.designir), args.step
|
||||
)
|
||||
write_json(args.output, materialized)
|
||||
result = {
|
||||
"valid": True,
|
||||
"output": str(args.output.expanduser().resolve()),
|
||||
"schema_version": materialized["schema_version"],
|
||||
"reconstruction_mode": materialized["reconstruction_mode"],
|
||||
"semantic_layer_authoritative": True,
|
||||
}
|
||||
elif args.command == "accept":
|
||||
result = acceptance_report(
|
||||
args.teacher, args.rebuilt, read_json(args.designir)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,735 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build execution-validated STEP -> DesignIR trajectories.
|
||||
*
|
||||
* Agent A is an OpenAI-compatible language model. Agent B is the deterministic
|
||||
* CAD-kernel acceptance program. The teacher STEP is never included in Agent A
|
||||
* input and is used only by Agent B after isolated reconstruction.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PIPELINE = path.resolve(HERE, "..");
|
||||
const REPO = path.resolve(PIPELINE, "..");
|
||||
const requireFromStudio = createRequire(
|
||||
path.join(REPO, "cad-agent-studio", "package.json"),
|
||||
);
|
||||
const YAML = requireFromStudio("yaml");
|
||||
const CONFIG_PATH = path.join(REPO, "llm.config.yaml");
|
||||
const EVIDENCE_DIR = path.join(PIPELINE, "runs", "evidence");
|
||||
const TRAJECTORY_DIR = path.join(PIPELINE, "runs", "trajectory");
|
||||
const SPLIT_PATH = path.join(TRAJECTORY_DIR, "splits.json");
|
||||
const PYTHON = path.join(REPO, "text-to-cad", ".venv", "bin", "python");
|
||||
const DESIGNIR_CLI = path.join(PIPELINE, "scripts", "designir_pipeline.py");
|
||||
const EXAMPLE_PATH = path.join(
|
||||
PIPELINE,
|
||||
"examples",
|
||||
"raised_hub_flange.designir.json",
|
||||
);
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(file, value) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function argsObject(argv) {
|
||||
const result = { _: [] };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index];
|
||||
if (!value.startsWith("--")) {
|
||||
result._.push(value);
|
||||
continue;
|
||||
}
|
||||
const key = value.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
result[key] = true;
|
||||
} else {
|
||||
result[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function evidenceFiles() {
|
||||
return fs
|
||||
.readdirSync(EVIDENCE_DIR)
|
||||
.filter((name) => name.endsWith(".case.json"))
|
||||
.map((name) => path.join(EVIDENCE_DIR, name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function allocate(total, counts) {
|
||||
const population = Object.values(counts).reduce((sum, value) => sum + value, 0);
|
||||
const rows = Object.entries(counts).map(([family, count]) => {
|
||||
const exact = (count * total) / population;
|
||||
return { family, value: Math.floor(exact), remainder: exact % 1 };
|
||||
});
|
||||
let remaining = total - rows.reduce((sum, row) => sum + row.value, 0);
|
||||
rows.sort((left, right) => right.remainder - left.remainder);
|
||||
for (const row of rows) {
|
||||
if (remaining <= 0) break;
|
||||
row.value += 1;
|
||||
remaining -= 1;
|
||||
}
|
||||
return Object.fromEntries(rows.map((row) => [row.family, row.value]));
|
||||
}
|
||||
|
||||
function initSplits() {
|
||||
const unique = new Map();
|
||||
for (const file of evidenceFiles()) {
|
||||
const payload = readJson(file);
|
||||
const geometry = payload.geometry_evidence || {};
|
||||
if (!geometry.reconstruction_eligible) continue;
|
||||
const id = String(payload.provenance?.source_sha256 || payload.case_id);
|
||||
if (unique.has(id)) continue;
|
||||
unique.set(id, {
|
||||
id,
|
||||
family: String(payload.design_ir?.part_family || "unknown"),
|
||||
evidence: file,
|
||||
teacher: String(payload.provenance?.source_path || ""),
|
||||
face_count: Number(geometry.face_count || 0),
|
||||
});
|
||||
}
|
||||
const byFamily = {};
|
||||
for (const item of unique.values()) {
|
||||
(byFamily[item.family] ||= []).push(item);
|
||||
}
|
||||
for (const items of Object.values(byFamily)) {
|
||||
items.sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
const counts = Object.fromEntries(
|
||||
Object.entries(byFamily).map(([family, items]) => [family, items.length]),
|
||||
);
|
||||
const testQuota = allocate(100, counts);
|
||||
const validationQuota = allocate(75, counts);
|
||||
const splits = { train: [], validation: [], test: [] };
|
||||
for (const [family, items] of Object.entries(byFamily)) {
|
||||
const testCount = testQuota[family] || 0;
|
||||
const validationCount = validationQuota[family] || 0;
|
||||
splits.test.push(...items.slice(0, testCount));
|
||||
splits.validation.push(
|
||||
...items.slice(testCount, testCount + validationCount),
|
||||
);
|
||||
splits.train.push(...items.slice(testCount + validationCount));
|
||||
}
|
||||
for (const values of Object.values(splits)) {
|
||||
values.sort(
|
||||
(left, right) =>
|
||||
left.face_count - right.face_count || left.id.localeCompare(right.id),
|
||||
);
|
||||
}
|
||||
const payload = {
|
||||
schema_version: "1.0",
|
||||
split_kind: "sha256_isolated_stratified_trajectory_split",
|
||||
policy: {
|
||||
train_count: splits.train.length,
|
||||
validation_count: splits.validation.length,
|
||||
test_count: splits.test.length,
|
||||
teacher_step_visible_to_reconstruction_agent: false,
|
||||
heldout_cases_allowed_in_experience_induction: false,
|
||||
},
|
||||
splits,
|
||||
};
|
||||
writeJson(SPLIT_PATH, payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function loadProvider(requestedProvider, requestedModel) {
|
||||
const config = YAML.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
const providerId = requestedProvider || config.defaultProvider;
|
||||
const provider = config.providers[providerId];
|
||||
if (!provider) throw new Error(`Unknown provider ${providerId}`);
|
||||
const apiKey =
|
||||
String(provider.apiKey || "").trim() ||
|
||||
process.env[String(provider.apiKeyEnv || "")] ||
|
||||
"";
|
||||
if (!apiKey) throw new Error(`No API key configured for ${providerId}`);
|
||||
const defaultModel = String(config.defaultModel || "").split(":").pop();
|
||||
const model =
|
||||
requestedModel || provider.models?.default || defaultModel;
|
||||
return {
|
||||
providerId,
|
||||
model,
|
||||
apiKey,
|
||||
baseURL: String(provider.baseURL).replace(/\/+$/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function compactEvidence(payload, maxSurfaces = 80) {
|
||||
const geometry = payload.geometry_evidence || {};
|
||||
const design = payload.design_ir || {};
|
||||
return {
|
||||
evidence_kind: "final_brep_observation_not_source_history",
|
||||
geometry: {
|
||||
solid_count: geometry.solid_count,
|
||||
face_count: geometry.face_count,
|
||||
volume: geometry.volume,
|
||||
bounding_box: geometry.bounding_box,
|
||||
analytic_surface_counts: geometry.analytic_surface_counts,
|
||||
analytic_surfaces: (geometry.analytic_surfaces || []).slice(0, maxSurfaces),
|
||||
},
|
||||
inferred_hypotheses: {
|
||||
family: design.part_family,
|
||||
features: design.features,
|
||||
constraints: design.constraints,
|
||||
semantic_parameter_candidates: design.semantic_parameter_candidates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function reconstructionPrompt(evidence, previous, feedback, experience = []) {
|
||||
const example = readJson(EXAMPLE_PATH);
|
||||
return [
|
||||
"You are Agent A, a STEP reverse-engineering program synthesizer.",
|
||||
"Generate an independent executable DesignIR 2.0 JSON object from final-BRep evidence.",
|
||||
"The source STEP is unavailable. Never reference, import, embed, or describe it as a base feature.",
|
||||
"STEP does not contain source history. Use canonical reconstruction order and explicitly inferred semantic names.",
|
||||
"Current executable operations are exactly: extrude_circle, extrude_rectangle, add_cylinder, through_hole, polar_hole_pattern.",
|
||||
"Cylinder and extrusion primitives may use any observed axis. Hole cutters currently require the positive Z axis.",
|
||||
"Preserve the observed absolute coordinate frame and orientation.",
|
||||
"For cylinder and extrusion features, set anchor to geometric_center and center to the observed 3D bounding-box center unless there is strong evidence for another placement.",
|
||||
"The legacy/default anchor is base_center; never rely on that default for reverse engineering.",
|
||||
"A value reference object may contain exactly one key: parameter or expression. Do not add scale or arithmetic keys.",
|
||||
"Do not expose an editable parameter unless at least one feature directly references it. Non-driving measurements must be editable false or omitted.",
|
||||
"Use named editable parameters such as plate_thickness, hole_diameter, hole_count, bolt_circle_diameter, bore_diameter, overall_length, overall_width.",
|
||||
"Every editable parameter must be referenced by a feature.",
|
||||
"Every perturbation must contain parameter, scale, expected_change, and preserve.",
|
||||
"Use an empty list for constraints when none are justified.",
|
||||
"Return JSON only, without Markdown.",
|
||||
"",
|
||||
"REFERENCE FORMAT:",
|
||||
JSON.stringify(example),
|
||||
"",
|
||||
"PRIVATE GEOMETRY EVIDENCE:",
|
||||
JSON.stringify(evidence),
|
||||
experience.length
|
||||
? `\nVALIDATED CANDIDATE METHODS (apply only when relevant):\n${JSON.stringify(experience)}`
|
||||
: "",
|
||||
previous ? `\nPREVIOUS CANDIDATE:\n${JSON.stringify(previous)}` : "",
|
||||
feedback ? `\nVALIDATOR FEEDBACK:\n${feedback}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function callModel(provider, prompt) {
|
||||
const response = await fetch(`${provider.baseURL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${provider.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: provider.model,
|
||||
temperature: 0.2,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Produce normalized, editable parametric CAD design states. Output strict JSON.",
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
}),
|
||||
signal: AbortSignal.timeout(90_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).slice(0, 600);
|
||||
throw new Error(`LLM HTTP ${response.status}: ${detail}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
const content = payload.choices?.[0]?.message?.content;
|
||||
if (!content) throw new Error("LLM returned no message content");
|
||||
const cleaned = String(content)
|
||||
.replace(/^```(?:json)?\s*/i, "")
|
||||
.replace(/\s*```$/, "");
|
||||
return {
|
||||
designir: JSON.parse(cleaned),
|
||||
usage: payload.usage || {},
|
||||
};
|
||||
}
|
||||
|
||||
function runPython(arguments_) {
|
||||
const result = spawnSync(PYTHON, [DESIGNIR_CLI, ...arguments_], {
|
||||
cwd: REPO,
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
});
|
||||
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
||||
return { ok: result.status === 0, status: result.status, output };
|
||||
}
|
||||
|
||||
async function runOne(job, provider, maxAttempts, options = {}) {
|
||||
const shortId = job.id.slice(0, 16);
|
||||
const root = path.join(
|
||||
options.attemptsRoot || path.join(TRAJECTORY_DIR, "attempts"),
|
||||
shortId,
|
||||
);
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const evidencePayload = readJson(job.evidence);
|
||||
const evidence = compactEvidence(evidencePayload);
|
||||
const unsupportedSurfaces = Object.entries(
|
||||
evidence.geometry.analytic_surface_counts || {},
|
||||
)
|
||||
.filter(([kind, count]) => count > 0 && !["plane", "cylinder"].includes(kind))
|
||||
.map(([kind]) => kind);
|
||||
if (unsupportedSurfaces.length) {
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
attempts: [],
|
||||
state: "unsupported_feature_vocabulary",
|
||||
missing_capabilities: unsupportedSurfaces.map(
|
||||
(kind) => `reconstruct_${kind}_surface`,
|
||||
),
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
let previous = null;
|
||||
let feedback = "";
|
||||
const attempts = [];
|
||||
for (let index = 1; index <= maxAttempts; index += 1) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const generated = await callModel(
|
||||
provider,
|
||||
reconstructionPrompt(
|
||||
evidence,
|
||||
previous,
|
||||
feedback,
|
||||
options.experience || [],
|
||||
),
|
||||
);
|
||||
const designir = generated.designir;
|
||||
designir.model_id ||= `trajectory_${shortId}_${index}`;
|
||||
designir.applied_experience_ids = (options.experience || []).map(
|
||||
(method) => method.id,
|
||||
);
|
||||
const designirPath = path.join(root, `attempt-${index}.designir.json`);
|
||||
const rebuiltPath = path.join(root, `attempt-${index}.step`);
|
||||
const reportPath = path.join(root, `attempt-${index}.acceptance.json`);
|
||||
writeJson(designirPath, designir);
|
||||
const rebuild = runPython([
|
||||
"isolated-rebuild",
|
||||
designirPath,
|
||||
"--output",
|
||||
rebuiltPath,
|
||||
"--backend",
|
||||
"build123d",
|
||||
]);
|
||||
if (!rebuild.ok) {
|
||||
feedback = `Isolated rebuild failed: ${rebuild.output}`;
|
||||
attempts.push({
|
||||
index,
|
||||
state: "compile_failed",
|
||||
duration_ms: Date.now() - started,
|
||||
usage: generated.usage,
|
||||
feedback,
|
||||
});
|
||||
previous = designir;
|
||||
continue;
|
||||
}
|
||||
const acceptance = runPython([
|
||||
"accept",
|
||||
"--teacher",
|
||||
job.teacher,
|
||||
"--designir",
|
||||
designirPath,
|
||||
"--rebuilt",
|
||||
rebuiltPath,
|
||||
"--report",
|
||||
reportPath,
|
||||
]);
|
||||
const report = fs.existsSync(reportPath) ? readJson(reportPath) : null;
|
||||
const state =
|
||||
acceptance.ok && report?.verdict === "pass"
|
||||
? "replay_validated"
|
||||
: "acceptance_failed";
|
||||
attempts.push({
|
||||
index,
|
||||
state,
|
||||
duration_ms: Date.now() - started,
|
||||
usage: generated.usage,
|
||||
report: reportPath,
|
||||
});
|
||||
if (state === "replay_validated") {
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
split: job.split,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
selected_attempt: index,
|
||||
designir: designirPath,
|
||||
rebuilt_step: rebuiltPath,
|
||||
acceptance_report: reportPath,
|
||||
attempts,
|
||||
state,
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
feedback = `Acceptance failed: ${JSON.stringify({
|
||||
threshold_checks: report?.threshold_checks,
|
||||
geometry: report?.geometry
|
||||
? {
|
||||
relative_volume_error: report.geometry.relative_volume_error,
|
||||
bounding_box_axis_error_mm:
|
||||
report.geometry.bounding_box_axis_error_mm,
|
||||
symmetric_difference_ratio:
|
||||
report.geometry.symmetric_difference_ratio,
|
||||
teacher_facts: report.geometry.teacher,
|
||||
rebuilt_facts: report.geometry.rebuilt,
|
||||
}
|
||||
: null,
|
||||
editability: report?.editability,
|
||||
})}`;
|
||||
previous = designir;
|
||||
} catch (error) {
|
||||
feedback = error instanceof Error ? error.message : String(error);
|
||||
attempts.push({
|
||||
index,
|
||||
state: "agent_failed",
|
||||
duration_ms: Date.now() - started,
|
||||
feedback,
|
||||
});
|
||||
}
|
||||
}
|
||||
const trajectory = {
|
||||
schema_version: "1.0",
|
||||
trajectory_kind: "execution_validated_step_to_designir",
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
attempts,
|
||||
state: "quarantine",
|
||||
};
|
||||
writeJson(path.join(root, "trajectory.json"), trajectory);
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
function summarize() {
|
||||
const root = path.join(TRAJECTORY_DIR, "attempts");
|
||||
const rows = fs.existsSync(root)
|
||||
? fs
|
||||
.readdirSync(root)
|
||||
.map((name) => path.join(root, name, "trajectory.json"))
|
||||
.filter((file) => fs.existsSync(file))
|
||||
.map(readJson)
|
||||
: [];
|
||||
const states = {};
|
||||
for (const row of rows) states[row.state] = (states[row.state] || 0) + 1;
|
||||
return {
|
||||
schema_version: "1.0",
|
||||
report_kind: "trajectory_run_summary",
|
||||
case_count: rows.length,
|
||||
states,
|
||||
validated_trajectory_count: states.replay_validated || 0,
|
||||
quarantined_count: states.quarantine || 0,
|
||||
rows: rows.map((row) => ({
|
||||
case_id: row.case_id,
|
||||
family: row.family,
|
||||
state: row.state,
|
||||
attempt_count: row.attempts?.length || 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function distillValidatedTrajectories() {
|
||||
const root = path.join(TRAJECTORY_DIR, "attempts");
|
||||
const trajectories = fs.existsSync(root)
|
||||
? fs
|
||||
.readdirSync(root)
|
||||
.map((name) => path.join(root, name, "trajectory.json"))
|
||||
.filter((file) => fs.existsSync(file))
|
||||
.map(readJson)
|
||||
.filter((row) => row.state === "replay_validated")
|
||||
: [];
|
||||
const groups = new Map();
|
||||
for (const trajectory of trajectories) {
|
||||
const designir = readJson(trajectory.designir);
|
||||
const signature = designir.features
|
||||
.map((feature) =>
|
||||
["add_cylinder", "extrude_circle"].includes(feature.operation)
|
||||
? "cylinder"
|
||||
: feature.operation,
|
||||
)
|
||||
.sort()
|
||||
.join("+");
|
||||
const key = `${designir.family}::${signature}`;
|
||||
const group = groups.get(key) || {
|
||||
family: designir.family,
|
||||
signature,
|
||||
cases: [],
|
||||
parameterNames: new Map(),
|
||||
anchors: new Map(),
|
||||
operations: new Set(),
|
||||
};
|
||||
group.cases.push(trajectory.case_id);
|
||||
for (const name of Object.keys(designir.parameters || {})) {
|
||||
group.parameterNames.set(name, (group.parameterNames.get(name) || 0) + 1);
|
||||
}
|
||||
for (const feature of designir.features || []) {
|
||||
group.operations.add(feature.operation);
|
||||
const anchor = feature.anchor || "base_center";
|
||||
group.anchors.set(anchor, (group.anchors.get(anchor) || 0) + 1);
|
||||
}
|
||||
groups.set(key, group);
|
||||
}
|
||||
const candidates = [...groups.values()].map((group) => {
|
||||
const simpleCylinder =
|
||||
group.family === "simple_cylinder" && group.signature === "cylinder";
|
||||
return {
|
||||
id: `trajectory.${group.family}.${group.signature.replaceAll("+", "_")}`,
|
||||
kind: "reconstruction_grammar",
|
||||
promotion_state: "candidate",
|
||||
applicability: simpleCylinder
|
||||
? {
|
||||
family: group.family,
|
||||
evidence_conditions: [
|
||||
"one dominant axis",
|
||||
"one distinct coaxial radius",
|
||||
"low topological complexity",
|
||||
"solid volume approximately pi * radius^2 * axial_length",
|
||||
],
|
||||
}
|
||||
: {
|
||||
family: group.family,
|
||||
operations: [...group.operations].sort(),
|
||||
},
|
||||
method: {
|
||||
guidance: simpleCylinder
|
||||
? "Interpret the single cylindrical surface as the outer body, not a bore. Build one cylinder from the observed outer radius, axial length, axis, and pose. Name the driving parameters outer_radius and overall_length. Preserve the absolute frame with an explicit center and anchor; do not expose duplicate width/thickness/inner-diameter parameters."
|
||||
: "Reconstruct from analytic dimensions and axis evidence; preserve the observed absolute frame with an explicit center and anchor; expose only parameters that directly drive executable features.",
|
||||
canonical_parameter_names: simpleCylinder
|
||||
? ["outer_radius", "overall_length"]
|
||||
: [],
|
||||
rejected_parameter_hypotheses: simpleCylinder
|
||||
? ["coaxial_inner_diameter", "duplicate_transverse_envelope_parameters"]
|
||||
: [],
|
||||
observed_parameter_names: Object.fromEntries(
|
||||
[...group.parameterNames.entries()].sort(),
|
||||
),
|
||||
observed_anchor_modes: Object.fromEntries(
|
||||
[...group.anchors.entries()].sort(),
|
||||
),
|
||||
},
|
||||
validation: {
|
||||
independent_replay_validated_case_count: new Set(group.cases).size,
|
||||
source_hashes: [...new Set(group.cases)].sort(),
|
||||
edit_contract_pass_required: true,
|
||||
positive_heldout_ab_required: true,
|
||||
heldout_ab_status: "not_run",
|
||||
},
|
||||
};
|
||||
});
|
||||
const payload = {
|
||||
schema_version: "1.0",
|
||||
library_kind: "execution_validated_trajectory_candidates",
|
||||
status: candidates.length ? "candidates_only" : "empty",
|
||||
policy: {
|
||||
router_consumable: false,
|
||||
automatic_publish_allowed: false,
|
||||
minimum_independent_cases: 3,
|
||||
positive_heldout_ab_required: true,
|
||||
},
|
||||
validated_trajectory_count: trajectories.length,
|
||||
candidate_count: candidates.length,
|
||||
candidates,
|
||||
};
|
||||
const output = path.join(TRAJECTORY_DIR, "candidate-experience.json");
|
||||
writeJson(output, payload);
|
||||
return { output, payload };
|
||||
}
|
||||
|
||||
function sanitizedMethods(candidatePath) {
|
||||
if (!candidatePath) return [];
|
||||
return (readJson(candidatePath).candidates || []).map((candidate) => ({
|
||||
id: candidate.id,
|
||||
kind: candidate.kind,
|
||||
applicability: candidate.applicability,
|
||||
method: candidate.method,
|
||||
}));
|
||||
}
|
||||
|
||||
function benchmarkScore(rows) {
|
||||
const passed = rows.filter((row) => row.state === "replay_validated").length;
|
||||
return {
|
||||
case_count: rows.length,
|
||||
replay_validated_count: passed,
|
||||
pass_rate: rows.length ? passed / rows.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function runBenchmark(args) {
|
||||
const split = String(args.split || "validation");
|
||||
if (!["validation", "test"].includes(split)) {
|
||||
throw new Error("Benchmark split must be validation or test");
|
||||
}
|
||||
const limit = Math.max(1, Number(args.limit || 10));
|
||||
const maxAttempts = Math.max(1, Number(args.attempts || 3));
|
||||
const maxFaces = Math.max(1, Number(args["max-faces"] || 24));
|
||||
const requestedFamily = String(args.family || "").trim();
|
||||
const manifest = fs.existsSync(SPLIT_PATH) ? readJson(SPLIT_PATH) : initSplits();
|
||||
const jobs = (manifest.splits?.[split] || [])
|
||||
.filter((job) => !requestedFamily || job.family === requestedFamily)
|
||||
.filter((job) => job.face_count <= maxFaces)
|
||||
.slice(0, limit);
|
||||
const provider = loadProvider(args.provider, args.model);
|
||||
const candidatePath = String(
|
||||
args.experience ||
|
||||
path.join(TRAJECTORY_DIR, "candidate-experience.json"),
|
||||
);
|
||||
const experience = sanitizedMethods(candidatePath);
|
||||
const benchmarkRoot = path.join(
|
||||
TRAJECTORY_DIR,
|
||||
"benchmarks",
|
||||
`${split}-${requestedFamily || "all"}`,
|
||||
);
|
||||
const baseline = [];
|
||||
const treatment = [];
|
||||
for (const job of jobs) {
|
||||
baseline.push(
|
||||
await runOne({ ...job, split }, provider, maxAttempts, {
|
||||
attemptsRoot: path.join(benchmarkRoot, "baseline"),
|
||||
}),
|
||||
);
|
||||
treatment.push(
|
||||
await runOne({ ...job, split }, provider, maxAttempts, {
|
||||
attemptsRoot: path.join(benchmarkRoot, "experience"),
|
||||
experience,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const baselineScore = benchmarkScore(baseline);
|
||||
const treatmentScore = benchmarkScore(treatment);
|
||||
const report = {
|
||||
schema_version: "1.0",
|
||||
report_kind: "heldout_trajectory_ab",
|
||||
split,
|
||||
family: requestedFamily || "all",
|
||||
provider: provider.providerId,
|
||||
model: provider.model,
|
||||
candidate_method_ids: experience.map((method) => method.id),
|
||||
baseline: baselineScore,
|
||||
experience: treatmentScore,
|
||||
pass_rate_delta: treatmentScore.pass_rate - baselineScore.pass_rate,
|
||||
positive: treatmentScore.pass_rate > baselineScore.pass_rate,
|
||||
promotion_eligible:
|
||||
jobs.length >= 10 && treatmentScore.pass_rate > baselineScore.pass_rate,
|
||||
case_results: jobs.map((job, index) => ({
|
||||
case_id: job.id,
|
||||
baseline_state: baseline[index].state,
|
||||
experience_state: treatment[index].state,
|
||||
})),
|
||||
};
|
||||
writeJson(path.join(benchmarkRoot, "report.json"), report);
|
||||
return report;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = argsObject(process.argv.slice(2));
|
||||
const command = args._[0];
|
||||
if (command === "init") {
|
||||
const payload = initSplits();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ split_path: SPLIT_PATH, policy: payload.policy }, null, 2)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "summary") {
|
||||
const report = summarize();
|
||||
writeJson(path.join(TRAJECTORY_DIR, "summary.json"), report);
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "distill") {
|
||||
const result = distillValidatedTrajectories();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
output: result.output,
|
||||
validated_trajectory_count: result.payload.validated_trajectory_count,
|
||||
candidate_count: result.payload.candidate_count,
|
||||
router_consumable: result.payload.policy.router_consumable,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "benchmark") {
|
||||
const report = await runBenchmark(args);
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command !== "run") {
|
||||
throw new Error(
|
||||
"Usage: trajectory_pipeline.mjs init|run|summary|distill|benchmark",
|
||||
);
|
||||
}
|
||||
const split = String(args.split || "train");
|
||||
const limit = Math.max(1, Number(args.limit || 5));
|
||||
const maxAttempts = Math.max(1, Number(args.attempts || 3));
|
||||
const maxFaces = Math.max(1, Number(args["max-faces"] || 24));
|
||||
const requestedFamily = String(args.family || "").trim();
|
||||
const rerun = Boolean(args.rerun);
|
||||
const manifest = fs.existsSync(SPLIT_PATH) ? readJson(SPLIT_PATH) : initSplits();
|
||||
const jobs = (manifest.splits?.[split] || [])
|
||||
.filter((job) => !requestedFamily || job.family === requestedFamily)
|
||||
.filter((job) => job.face_count <= maxFaces)
|
||||
.filter(
|
||||
(job) =>
|
||||
rerun ||
|
||||
!fs.existsSync(
|
||||
path.join(
|
||||
TRAJECTORY_DIR,
|
||||
"attempts",
|
||||
job.id.slice(0, 16),
|
||||
"trajectory.json",
|
||||
),
|
||||
),
|
||||
)
|
||||
.slice(0, limit);
|
||||
const provider = loadProvider(args.provider, args.model);
|
||||
const results = [];
|
||||
for (const job of jobs) {
|
||||
const result = await runOne({ ...job, split }, provider, maxAttempts);
|
||||
results.push({
|
||||
case_id: job.id,
|
||||
family: job.family,
|
||||
face_count: job.face_count,
|
||||
state: result.state,
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(results.at(-1))}\n`);
|
||||
}
|
||||
const report = summarize();
|
||||
writeJson(path.join(TRAJECTORY_DIR, "summary.json"), report);
|
||||
process.stdout.write(`${JSON.stringify({ completed: results.length, report }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+11
@@ -63,6 +63,17 @@ 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.
|
||||
|
||||
The library also supports:
|
||||
|
||||
- `parameter_naming`: canonical editable names inferred from repeated B-Rep
|
||||
evidence;
|
||||
- `edit_strategy`: expected-change and non-target-preservation behavior for a
|
||||
semantic parameter.
|
||||
|
||||
Evidence support alone never makes a method router-consumable. Promotion
|
||||
requires a positive held-out A/B result on independent source cases and full
|
||||
edit-contract success for that method.
|
||||
|
||||
## Reconstruction grammar
|
||||
|
||||
`reconstruction_grammar` is the executable bridge between generalized
|
||||
|
||||
@@ -30,7 +30,17 @@ Write `runs/review/experience-draft.json` with this shape:
|
||||
|
||||
Allowed kinds are `feature_motif`, `constraint`, `design_rule`,
|
||||
`validation_rule`, `failure_repair`, `dimensionless_distribution`, and
|
||||
`reconstruction_grammar`.
|
||||
`reconstruction_grammar`, plus:
|
||||
|
||||
- `parameter_naming`: maps repeated final-B-Rep evidence to a canonical
|
||||
editable name such as `plate_thickness`, `hole_spacing`,
|
||||
`bolt_circle_diameter`, or `rib_count`;
|
||||
- `edit_strategy`: defines the expected target change and preservation contract
|
||||
for a named semantic parameter.
|
||||
|
||||
These proposal kinds use `evidence_query.required_parameters`. A name is a
|
||||
reconstruction-system convention, never a claim that the STEP contained the
|
||||
source parameter.
|
||||
Distribution proposals also provide `observation_name`; the publisher computes
|
||||
the distribution from private evidence and never accepts values from the draft.
|
||||
|
||||
@@ -86,6 +96,9 @@ Follow these rules:
|
||||
- Avoid dimensions, coordinates, source identifiers, face references, and
|
||||
numeric literals in prose.
|
||||
- Do not infer build order or causal failure mechanisms from final geometry.
|
||||
- Do not present inferred parameter names as recovered source parameters.
|
||||
- Prefer specific editable names over generic span roles only when repeated
|
||||
geometric evidence supports the distinction.
|
||||
- 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
|
||||
|
||||
@@ -65,7 +65,17 @@ ALLOWED_NUMERIC_KEYS = {
|
||||
"remaining_support",
|
||||
"required_confidence",
|
||||
"llm_proposal_count",
|
||||
"independent_case_count",
|
||||
"edit_contract_pass_rate",
|
||||
"baseline_score_delta",
|
||||
}
|
||||
ALLOWED_NUMERIC_SUFFIXES = (
|
||||
"_count",
|
||||
"_rate",
|
||||
"_ratio",
|
||||
"_delta",
|
||||
"_support",
|
||||
)
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
@@ -209,7 +219,12 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
experience = payload.get("experience") if isinstance(payload.get("experience"), dict) else {}
|
||||
provenance = payload.get("provenance") if isinstance(payload.get("provenance"), dict) else {}
|
||||
supplied_id = provenance.get("source_sha256") or payload.get("case_id")
|
||||
case_id = token(supplied_id, content_hash(payload))
|
||||
raw_id = str(supplied_id or "").strip().lower()
|
||||
case_id = (
|
||||
raw_id
|
||||
if re.fullmatch(r"[a-f0-9]{64}|[a-z][a-z0-9_.+-]*", raw_id)
|
||||
else content_hash(payload)
|
||||
)
|
||||
family = token(design_ir.get("part_family") or payload.get("part_family"))
|
||||
if family.endswith("_candidate"):
|
||||
family = family.removesuffix("_candidate")
|
||||
@@ -287,14 +302,67 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
|
||||
semantic_parameters: list[dict[str, Any]] = []
|
||||
raw_parameters = design_ir.get("semantic_parameter_candidates", [])
|
||||
if isinstance(raw_parameters, list):
|
||||
for raw in raw_parameters:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
name = token(raw.get("name"))
|
||||
if name == "unknown":
|
||||
continue
|
||||
edit_contract = (
|
||||
raw.get("edit_contract")
|
||||
if isinstance(raw.get("edit_contract"), dict)
|
||||
else {}
|
||||
)
|
||||
confidence = raw.get("confidence", 0.0)
|
||||
if not isinstance(confidence, (int, float)):
|
||||
confidence = 0.0
|
||||
semantic_parameters.append(
|
||||
{
|
||||
"name": name,
|
||||
"unit": token(raw.get("unit")),
|
||||
"semantic_aliases": list_tokens(raw.get("semantic_aliases")),
|
||||
"affects_feature_roles": list_tokens(
|
||||
raw.get("affects_feature_roles")
|
||||
),
|
||||
"expected_change": token(edit_contract.get("expected_change")),
|
||||
"confidence_class": (
|
||||
"high"
|
||||
if float(confidence) >= 0.8
|
||||
else "medium"
|
||||
if float(confidence) >= 0.6
|
||||
else "low"
|
||||
),
|
||||
"epistemic_status": token(raw.get("epistemic_status")),
|
||||
}
|
||||
)
|
||||
|
||||
geometry = (
|
||||
payload.get("geometry_evidence")
|
||||
if isinstance(payload.get("geometry_evidence"), dict)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"case_id": case_id,
|
||||
"extractor_version": str(payload.get("extractor_version") or "0"),
|
||||
"family": family,
|
||||
"features": sorted(features),
|
||||
"relations": sorted(relations),
|
||||
"rules": rules,
|
||||
"validations": validations,
|
||||
"normalized_observations": observations,
|
||||
"semantic_parameters": semantic_parameters,
|
||||
"reconstruction_eligible": bool(
|
||||
geometry.get(
|
||||
"reconstruction_eligible",
|
||||
geometry.get("valid", True)
|
||||
and int(geometry.get("solid_count", 1)) == 1
|
||||
and float(geometry.get("volume", 1.0)) > 0.0,
|
||||
)
|
||||
),
|
||||
"rejection_reasons": list_tokens(geometry.get("rejection_reasons")),
|
||||
"reconstruction": normalize_reconstruction(
|
||||
design_ir.get("reconstruction_evidence")
|
||||
),
|
||||
@@ -311,11 +379,18 @@ def load_cases(root: Path) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("root must be an object")
|
||||
case = normalize_case(payload)
|
||||
if not case["reconstruction_eligible"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if not case["features"] and not case["rules"] and not case["relations"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if case["case_id"] in cases:
|
||||
duplicates += 1
|
||||
if case["extractor_version"] > cases[case["case_id"]][
|
||||
"extractor_version"
|
||||
]:
|
||||
cases[case["case_id"]] = case
|
||||
continue
|
||||
cases[case["case_id"]] = case
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
@@ -555,7 +630,9 @@ def audit_value(value: Any, path: tuple[str, ...], errors: list[str]) -> None:
|
||||
audit_value(child, path + (str(index),), errors)
|
||||
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
leaf = path[-1] if path else ""
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS:
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS and not leaf.endswith(
|
||||
ALLOWED_NUMERIC_SUFFIXES
|
||||
):
|
||||
errors.append(f"{'.'.join(path)}: numeric value is not generalized metadata")
|
||||
|
||||
|
||||
@@ -646,7 +723,7 @@ def extract_folder(input_dir: Path, output_dir: Path) -> dict[str, Any]:
|
||||
digest = payload.get("provenance", {}).get("source_sha256")
|
||||
if (
|
||||
isinstance(digest, str)
|
||||
and payload.get("extractor_version") == "3.0"
|
||||
and payload.get("extractor_version") == "4.0"
|
||||
):
|
||||
known_hashes[digest] = path
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
@@ -711,6 +788,18 @@ def prepare_semantic_batch(
|
||||
"candidate_rules": case["rules"],
|
||||
"validation_targets": case["validations"],
|
||||
"reconstruction_evidence": case["reconstruction"],
|
||||
"semantic_parameter_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"unit": item["unit"],
|
||||
"semantic_aliases": item["semantic_aliases"],
|
||||
"affects_feature_roles": item["affects_feature_roles"],
|
||||
"expected_change": item["expected_change"],
|
||||
"confidence_class": item["confidence_class"],
|
||||
"epistemic_status": item["epistemic_status"],
|
||||
}
|
||||
for item in case["semantic_parameters"]
|
||||
],
|
||||
"dimensionless_observation_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
@@ -753,6 +842,8 @@ def prepare_semantic_batch(
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
],
|
||||
},
|
||||
@@ -770,6 +861,8 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
}
|
||||
for index, raw in enumerate(payload.get("proposals", [])):
|
||||
@@ -785,6 +878,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
)
|
||||
required_features = list_tokens(evidence.get("required_features"))
|
||||
required_relations = list_tokens(evidence.get("required_relations"))
|
||||
required_parameters = list_tokens(evidence.get("required_parameters"))
|
||||
context_features = list_tokens(
|
||||
evidence.get("context_features") or required_features
|
||||
)
|
||||
@@ -800,6 +894,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if (
|
||||
not required_features
|
||||
and not required_relations
|
||||
and not required_parameters
|
||||
and observation_name == "unknown"
|
||||
):
|
||||
raise ValueError(f"proposal {proposal_id} has no evidence query")
|
||||
@@ -814,6 +909,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"evidence_query": {
|
||||
"required_features": required_features,
|
||||
"required_relations": required_relations,
|
||||
"required_parameters": required_parameters,
|
||||
"context_features": context_features,
|
||||
},
|
||||
"guidance": guidance,
|
||||
@@ -833,6 +929,24 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
f"proposal {proposal_id} has an incomplete reconstruction grammar"
|
||||
)
|
||||
proposal["reconstruction_grammar"] = grammar
|
||||
if kind in {"parameter_naming", "edit_strategy"}:
|
||||
canonical_parameter = token(raw.get("canonical_parameter"))
|
||||
if canonical_parameter == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires canonical_parameter"
|
||||
)
|
||||
proposal["canonical_parameter"] = canonical_parameter
|
||||
proposal["semantic_aliases"] = list_tokens(
|
||||
raw.get("semantic_aliases")
|
||||
)
|
||||
if kind == "edit_strategy":
|
||||
expected_change = token(raw.get("expected_change"))
|
||||
if expected_change == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires expected_change"
|
||||
)
|
||||
proposal["expected_change"] = expected_change
|
||||
proposal["preserve_roles"] = list_tokens(raw.get("preserve_roles"))
|
||||
for key in ("check", "repair", "failure_signature"):
|
||||
value = string_without_instance_numbers(raw.get(key))
|
||||
if value is not None:
|
||||
@@ -849,10 +963,20 @@ def build_reviewed_library(
|
||||
min_support: int,
|
||||
min_confidence: float,
|
||||
stats: dict[str, int],
|
||||
validation_report: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
promoted: list[dict[str, Any]] = []
|
||||
candidates: list[dict[str, Any]] = []
|
||||
family_counts = Counter(case["family"] for case in cases)
|
||||
validated_methods = {
|
||||
token(item.get("experience_id")): item
|
||||
for item in (validation_report or {}).get("method_results", [])
|
||||
if isinstance(item, dict)
|
||||
and item.get("passed") is True
|
||||
and int(item.get("independent_case_count", 0)) >= 3
|
||||
and float(item.get("edit_contract_pass_rate", 0.0)) >= 1.0
|
||||
and float(item.get("baseline_score_delta", 0.0)) > 0.0
|
||||
}
|
||||
|
||||
def supports_reconstruction_grammar(
|
||||
case: dict[str, Any], grammar: dict[str, Any]
|
||||
@@ -901,6 +1025,9 @@ def build_reviewed_library(
|
||||
required_relations = set(
|
||||
proposal["evidence_query"]["required_relations"]
|
||||
)
|
||||
required_parameters = set(
|
||||
proposal["evidence_query"].get("required_parameters", [])
|
||||
)
|
||||
context_features = set(
|
||||
proposal["evidence_query"].get("context_features", required_features)
|
||||
)
|
||||
@@ -941,7 +1068,22 @@ def build_reviewed_library(
|
||||
"p90": round(percentile(values, 0.90), 6),
|
||||
"max": round(max(values), 6),
|
||||
}
|
||||
if eligible(support, item["confidence"], min_support, min_confidence):
|
||||
evidence_eligible = eligible(
|
||||
support, item["confidence"], min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(
|
||||
validation["independent_case_count"]
|
||||
),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(
|
||||
validation["baseline_score_delta"]
|
||||
),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.pop("distribution", None)
|
||||
@@ -951,6 +1093,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -966,6 +1113,9 @@ def build_reviewed_library(
|
||||
for case in contextual
|
||||
if required_features.issubset(case["features"])
|
||||
and required_relations.issubset(case["relations"])
|
||||
and required_parameters.issubset(
|
||||
{item["name"] for item in case.get("semantic_parameters", [])}
|
||||
)
|
||||
and (
|
||||
proposal["kind"] != "reconstruction_grammar"
|
||||
or supports_reconstruction_grammar(
|
||||
@@ -982,6 +1132,7 @@ def build_reviewed_library(
|
||||
"when": {
|
||||
"features": sorted(required_features),
|
||||
"relations": sorted(required_relations),
|
||||
"semantic_parameters": sorted(required_parameters),
|
||||
},
|
||||
"guidance": proposal["guidance"],
|
||||
"semantic_rationale": proposal["semantic_rationale"],
|
||||
@@ -993,7 +1144,24 @@ def build_reviewed_library(
|
||||
item[key] = proposal[key]
|
||||
if proposal["kind"] == "reconstruction_grammar":
|
||||
item["reconstruction_grammar"] = proposal["reconstruction_grammar"]
|
||||
if eligible(support, confidence, min_support, min_confidence):
|
||||
if proposal["kind"] in {"parameter_naming", "edit_strategy"}:
|
||||
item["canonical_parameter"] = proposal["canonical_parameter"]
|
||||
item["semantic_aliases"] = proposal["semantic_aliases"]
|
||||
if proposal["kind"] == "edit_strategy":
|
||||
item["expected_change"] = proposal["expected_change"]
|
||||
item["preserve_roles"] = proposal["preserve_roles"]
|
||||
evidence_eligible = eligible(
|
||||
support, confidence, min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(validation["independent_case_count"]),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(validation["baseline_score_delta"]),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.update(
|
||||
@@ -1002,6 +1170,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -1026,7 +1199,8 @@ def build_reviewed_library(
|
||||
"single_case_promotion_allowed": False,
|
||||
"llm_semantic_review_required": True,
|
||||
"draft_evidence_verified": True,
|
||||
"router_consumable": True,
|
||||
"positive_heldout_ab_required": True,
|
||||
"router_consumable": bool(promoted),
|
||||
"minimum_support": min_support,
|
||||
"minimum_confidence": min_confidence,
|
||||
},
|
||||
@@ -1115,6 +1289,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
publish_parser.add_argument("--min-support", type=int, default=20)
|
||||
publish_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
publish_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
distill_parser = subparsers.add_parser("distill")
|
||||
distill_parser.add_argument(
|
||||
@@ -1130,6 +1309,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
distill_parser.add_argument("--min-support", type=int, default=20)
|
||||
distill_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
distill_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@@ -1205,12 +1389,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
draft_path = args.draft
|
||||
draft_payload = json.loads(draft_path.read_text(encoding="utf-8"))
|
||||
proposals = normalize_proposals(draft_payload)
|
||||
validation_report = None
|
||||
if args.validation_report is not None:
|
||||
validation_report = json.loads(
|
||||
args.validation_report.read_text(encoding="utf-8")
|
||||
)
|
||||
library = build_reviewed_library(
|
||||
cases,
|
||||
proposals,
|
||||
args.min_support,
|
||||
args.min_confidence,
|
||||
stats,
|
||||
validation_report,
|
||||
)
|
||||
errors = audit_library(library)
|
||||
if errors:
|
||||
|
||||
@@ -11,7 +11,7 @@ from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
EXTRACTOR_VERSION = "3.0"
|
||||
EXTRACTOR_VERSION = "4.0"
|
||||
|
||||
|
||||
def _rounded(value: float) -> float:
|
||||
@@ -71,6 +71,184 @@ def _cardinality_class(count: int) -> str:
|
||||
return "dense"
|
||||
|
||||
|
||||
def _parameter_candidate(
|
||||
name: str,
|
||||
value: float | int,
|
||||
unit: str,
|
||||
confidence: float,
|
||||
affects: list[str],
|
||||
expected_change: str,
|
||||
evidence_kind: str,
|
||||
aliases: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create an explicitly inferred, editable semantic parameter candidate."""
|
||||
return {
|
||||
"name": name,
|
||||
"value": value if unit == "count" else _rounded(float(value)),
|
||||
"unit": unit,
|
||||
"editable_candidate": True,
|
||||
"epistemic_status": "inferred_from_final_brep",
|
||||
"confidence": _rounded(confidence),
|
||||
"semantic_aliases": sorted(set(aliases or [])),
|
||||
"affects_feature_roles": sorted(set(affects)),
|
||||
"edit_contract": {
|
||||
"expected_change": expected_change,
|
||||
"preserve_roles": ["part_center", "primary_reference_frame"],
|
||||
},
|
||||
"source_evidence": {"kind": evidence_kind},
|
||||
}
|
||||
|
||||
|
||||
def _semantic_parameter_candidates(
|
||||
surfaces: list[dict[str, Any]],
|
||||
bbox_center: list[float],
|
||||
bbox_size: list[float],
|
||||
features: list[dict[str, Any]],
|
||||
semantic_summary: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Name editable dimensions without pretending that STEP stored parameters."""
|
||||
if not bbox_size or max(bbox_size) <= 1e-9:
|
||||
return []
|
||||
feature_roles = {
|
||||
str(item.get("type"))
|
||||
for item in features
|
||||
if isinstance(item, dict) and item.get("type")
|
||||
}
|
||||
ordered_axes = sorted(
|
||||
zip(("x", "y", "z"), bbox_size), key=lambda item: item[1], reverse=True
|
||||
)
|
||||
candidates = [
|
||||
_parameter_candidate(
|
||||
"overall_length",
|
||||
ordered_axes[0][1],
|
||||
"mm",
|
||||
0.98,
|
||||
["primary_envelope"],
|
||||
"overall_length_changes",
|
||||
f"bounding_box_{ordered_axes[0][0]}_span",
|
||||
),
|
||||
_parameter_candidate(
|
||||
"overall_width",
|
||||
ordered_axes[1][1],
|
||||
"mm",
|
||||
0.92,
|
||||
["primary_envelope"],
|
||||
"overall_width_changes",
|
||||
f"bounding_box_{ordered_axes[1][0]}_span",
|
||||
),
|
||||
_parameter_candidate(
|
||||
"overall_thickness",
|
||||
ordered_axes[2][1],
|
||||
"mm",
|
||||
0.88,
|
||||
["primary_envelope"],
|
||||
"overall_thickness_changes",
|
||||
f"bounding_box_{ordered_axes[2][0]}_span",
|
||||
["plate_thickness"]
|
||||
if "plate_like_body" in feature_roles
|
||||
else ["minimum_envelope_span"],
|
||||
),
|
||||
]
|
||||
|
||||
dominant_axis = str(semantic_summary.get("dominant_axis", "z"))
|
||||
transverse_axes = {"x": (1, 2), "y": (0, 2), "z": (0, 1)}[dominant_axis]
|
||||
scale = max(bbox_size)
|
||||
center_tolerance = max(scale * 0.005, 1e-4)
|
||||
repeated_groups: dict[float, list[dict[str, Any]]] = defaultdict(list)
|
||||
coaxial: list[dict[str, Any]] = []
|
||||
for surface in surfaces:
|
||||
if (
|
||||
surface.get("type") != "cylinder"
|
||||
or _canonical_axis(surface["axis"]) != dominant_axis
|
||||
):
|
||||
continue
|
||||
radial_offset = _radial_offset(
|
||||
surface["location"], bbox_center, dominant_axis
|
||||
)
|
||||
if radial_offset <= center_tolerance:
|
||||
coaxial.append(surface)
|
||||
else:
|
||||
repeated_groups[round(float(surface["radius"]), 4)].append(surface)
|
||||
|
||||
repeated = max(repeated_groups.values(), key=len) if repeated_groups else []
|
||||
unique_centers = {
|
||||
(
|
||||
round(float(item["location"][transverse_axes[0]]), 4),
|
||||
round(float(item["location"][transverse_axes[1]]), 4),
|
||||
)
|
||||
for item in repeated
|
||||
}
|
||||
if len(unique_centers) >= 3:
|
||||
member_radius = float(repeated[0]["radius"])
|
||||
offsets = [
|
||||
math.hypot(
|
||||
first - bbox_center[transverse_axes[0]],
|
||||
second - bbox_center[transverse_axes[1]],
|
||||
)
|
||||
for first, second in unique_centers
|
||||
]
|
||||
mean_offset = sum(offsets) / len(offsets)
|
||||
circular = (
|
||||
mean_offset > center_tolerance
|
||||
and (max(offsets) - min(offsets)) / mean_offset <= 0.08
|
||||
)
|
||||
candidates.extend(
|
||||
[
|
||||
_parameter_candidate(
|
||||
"repeated_feature_count",
|
||||
len(unique_centers),
|
||||
"count",
|
||||
0.72,
|
||||
["repeated_cylindrical_feature_pattern"],
|
||||
"pattern_member_count_changes",
|
||||
"unique_equal_radius_parallel_cylinder_axes",
|
||||
["hole_count", "boss_count"],
|
||||
),
|
||||
_parameter_candidate(
|
||||
"repeated_feature_diameter",
|
||||
member_radius * 2.0,
|
||||
"mm",
|
||||
0.68,
|
||||
["repeated_cylindrical_feature_pattern"],
|
||||
"pattern_member_diameter_changes",
|
||||
"equal_radius_cylindrical_surfaces",
|
||||
["hole_diameter", "boss_diameter"],
|
||||
),
|
||||
]
|
||||
)
|
||||
if circular:
|
||||
candidates.append(
|
||||
_parameter_candidate(
|
||||
"bolt_circle_diameter",
|
||||
mean_offset * 2.0,
|
||||
"mm",
|
||||
0.82,
|
||||
["circular_equal_radius_pattern"],
|
||||
"pattern_radius_changes",
|
||||
"equal_radial_offset_cylinder_axes",
|
||||
["hole_spacing", "pattern_diameter"],
|
||||
)
|
||||
)
|
||||
|
||||
coaxial_radii = sorted(
|
||||
{round(float(item["radius"]), 6) for item in coaxial}
|
||||
)
|
||||
if coaxial_radii:
|
||||
candidates.append(
|
||||
_parameter_candidate(
|
||||
"coaxial_inner_diameter",
|
||||
coaxial_radii[0] * 2.0,
|
||||
"mm",
|
||||
0.58,
|
||||
["coaxial_cylindrical_stack"],
|
||||
"coaxial_inner_diameter_changes",
|
||||
"smallest_centered_cylindrical_surface",
|
||||
["bore_diameter", "shaft_diameter"],
|
||||
)
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def _surface_record(face: Any, index: int) -> dict[str, Any]:
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import (
|
||||
@@ -702,8 +880,18 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
reconstruction_evidence = _reconstruction_evidence(
|
||||
surfaces, features, constraints, semantic_summary
|
||||
)
|
||||
semantic_parameters = _semantic_parameter_candidates(
|
||||
surfaces,
|
||||
bbox_center,
|
||||
bbox_size,
|
||||
features,
|
||||
semantic_summary,
|
||||
)
|
||||
digest = hashlib.sha256(source_bytes).hexdigest()
|
||||
normalized_observations = semantic_summary.pop("normalized_observations", [])
|
||||
solid_count = len(shape.solids())
|
||||
volume = _rounded(shape.volume)
|
||||
usable_solid = bool(shape.is_valid) and solid_count > 0 and volume > 0.0
|
||||
return {
|
||||
"schema_version": "2.0",
|
||||
"extractor_version": EXTRACTOR_VERSION,
|
||||
@@ -715,10 +903,22 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
"source_format": "step",
|
||||
},
|
||||
"geometry_evidence": {
|
||||
"valid": bool(shape.is_valid),
|
||||
"solid_count": len(shape.solids()),
|
||||
"kernel_valid": bool(shape.is_valid),
|
||||
"valid": usable_solid,
|
||||
"reconstruction_eligible": usable_solid and solid_count == 1,
|
||||
"rejection_reasons": [
|
||||
reason
|
||||
for condition, reason in (
|
||||
(not bool(shape.is_valid), "kernel_invalid"),
|
||||
(solid_count == 0, "no_closed_solid"),
|
||||
(volume <= 0.0, "zero_volume"),
|
||||
(solid_count > 1, "multi_solid_requires_segmentation"),
|
||||
)
|
||||
if condition
|
||||
],
|
||||
"solid_count": solid_count,
|
||||
"face_count": len(shape.faces()),
|
||||
"volume": _rounded(shape.volume),
|
||||
"volume": volume,
|
||||
"bounding_box": {
|
||||
"min": bbox_min,
|
||||
"max": bbox_max,
|
||||
@@ -729,9 +929,11 @@ def extract_step_case(source: Path) -> dict[str, Any]:
|
||||
"analytic_surfaces": surfaces,
|
||||
},
|
||||
"design_ir": {
|
||||
"epistemic_status": "inferred_design_hypothesis_not_source_history",
|
||||
"part_family": family,
|
||||
"features": features,
|
||||
"constraints": constraints,
|
||||
"semantic_parameter_candidates": semantic_parameters,
|
||||
"normalized_observations": normalized_observations,
|
||||
"semantic_summary": semantic_summary,
|
||||
"reconstruction_evidence": reconstruction_evidence,
|
||||
|
||||
@@ -31,6 +31,41 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
|
||||
def test_legacy_model_migrates_to_semantic_designir_3(self) -> None:
|
||||
migrated = MODULE.migrate_designir_2_to_3(self.payload())
|
||||
self.assertEqual("3.0", migrated["schema_version"])
|
||||
self.assertEqual(
|
||||
"fully_semantic_parametric", migrated["reconstruction_mode"]
|
||||
)
|
||||
self.assertIn(
|
||||
"outer_radius", migrated["edit_interface"]["semantic_parameters"]
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
result = MODULE.compile_designir(migrated, output)
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
|
||||
def test_generated_step_materializes_hybrid_surface_snapshot(self) -> None:
|
||||
migrated = MODULE.migrate_designir_2_to_3(self.payload())
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
MODULE.compile_designir(migrated, output)
|
||||
hybrid = MODULE.materialize_hybrid_designir(migrated, output)
|
||||
self.assertEqual(
|
||||
"hybrid_semantic_surface_parametric",
|
||||
hybrid["reconstruction_mode"],
|
||||
)
|
||||
self.assertTrue(hybrid["surface_layer"]["solids"])
|
||||
self.assertTrue(
|
||||
hybrid["compiled_surface_provenance"][
|
||||
"semantic_layer_authoritative"
|
||||
]
|
||||
)
|
||||
self.assertTrue(MODULE.anti_cheat(hybrid)["valid"])
|
||||
rebuilt = Path(temporary) / "hybrid-rebuilt.step"
|
||||
MODULE.compile_designir(hybrid, rebuilt)
|
||||
self.assertTrue(rebuilt.is_file())
|
||||
|
||||
def test_teacher_dependency_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["source_step"] = "teacher.step"
|
||||
@@ -48,6 +83,51 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
self.assertGreater(result["facts"]["volume_mm3"], 30_000)
|
||||
self.assertTrue(Path(result["model_json"]).is_file())
|
||||
|
||||
def test_arbitrary_axis_geometric_center_preserves_pose(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["family"] = "axis_aligned_cylinder"
|
||||
payload["parameters"] = {
|
||||
"outer_radius": {"value": 10, "unit": "mm", "editable": True},
|
||||
"overall_length": {"value": 400, "unit": "mm", "editable": True},
|
||||
}
|
||||
payload["expressions"] = {}
|
||||
payload["features"] = [
|
||||
{
|
||||
"id": "main_body",
|
||||
"operation": "extrude_circle",
|
||||
"radius": {"parameter": "outer_radius"},
|
||||
"height": {"parameter": "overall_length"},
|
||||
"axis": [0, -1, 0],
|
||||
"center": [7, 11, 13],
|
||||
"anchor": "geometric_center",
|
||||
}
|
||||
]
|
||||
payload["edit_interface"] = {
|
||||
"editable_parameters": ["outer_radius", "overall_length"],
|
||||
"preserved_interfaces": ["part_center", "primary_axis"],
|
||||
}
|
||||
payload["validation_contract"]["perturbations"] = [
|
||||
{
|
||||
"parameter": "outer_radius",
|
||||
"scale": 1.1,
|
||||
"expected_change": "radius",
|
||||
"preserve": ["overall_length", "part_center"],
|
||||
},
|
||||
{
|
||||
"parameter": "overall_length",
|
||||
"scale": 1.1,
|
||||
"expected_change": "length",
|
||||
"preserve": ["outer_radius", "part_center"],
|
||||
},
|
||||
]
|
||||
shape = MODULE.build_shape(payload)
|
||||
bounds = shape.bounding_box()
|
||||
self.assertAlmostEqual(-189, bounds.min.Y, places=6)
|
||||
self.assertAlmostEqual(211, bounds.max.Y, places=6)
|
||||
self.assertAlmostEqual(7, shape.center(MODULE.CenterOf.MASS).X, places=6)
|
||||
self.assertAlmostEqual(11, shape.center(MODULE.CenterOf.MASS).Y, places=6)
|
||||
self.assertAlmostEqual(13, shape.center(MODULE.CenterOf.MASS).Z, places=6)
|
||||
|
||||
def test_embedded_mesh_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["features"][0]["mesh"] = {"vertices": [[0, 0, 0]]}
|
||||
@@ -97,6 +177,10 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
json.dumps(
|
||||
{
|
||||
"model_id": f"case-{index}",
|
||||
"teacher_sha256": f"teacher-{index}",
|
||||
"applied_experience_ids": [
|
||||
"reconstruction.flange.primary"
|
||||
],
|
||||
"next_state": "replay_validated",
|
||||
}
|
||||
),
|
||||
@@ -107,6 +191,10 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
reports, ROOT / "config" / "promotion-policy.json"
|
||||
)
|
||||
self.assertTrue(result["eligible"])
|
||||
self.assertEqual(
|
||||
["reconstruction.flange.primary"],
|
||||
result["validated_experience_ids"],
|
||||
)
|
||||
self.assertEqual("promoted", result["next_state"])
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -106,6 +107,22 @@ def make_case(index: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def positive_validation(*experience_ids: str) -> dict:
|
||||
return {
|
||||
"validation_kind": "heldout_experience_ab",
|
||||
"method_results": [
|
||||
{
|
||||
"experience_id": experience_id,
|
||||
"independent_case_count": 3,
|
||||
"edit_contract_pass_rate": 1.0,
|
||||
"baseline_score_delta": 0.5,
|
||||
"passed": True,
|
||||
}
|
||||
for experience_id in experience_ids
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class CadExperienceTest(unittest.TestCase):
|
||||
def test_default_data_paths_use_shallow_local_layout(self):
|
||||
pipeline_root = Path(__file__).parents[1].resolve()
|
||||
@@ -179,6 +196,47 @@ class CadExperienceTest(unittest.TestCase):
|
||||
"canonical_reconstruction_plan_not_recovered_history",
|
||||
reconstruction["interpretation"],
|
||||
)
|
||||
parameters = STEP_MODULE._semantic_parameter_candidates(
|
||||
surfaces,
|
||||
[0.0, 0.0, 0.0],
|
||||
[40.0, 20.0, 40.0],
|
||||
features,
|
||||
summary,
|
||||
)
|
||||
names = {item["name"] for item in parameters}
|
||||
self.assertIn("repeated_feature_count", names)
|
||||
self.assertIn("repeated_feature_diameter", names)
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["epistemic_status"] == "inferred_from_final_brep"
|
||||
for item in parameters
|
||||
)
|
||||
)
|
||||
|
||||
def test_non_solid_teacher_is_rejected_from_induction(self):
|
||||
payload = make_case(1)
|
||||
payload["geometry_evidence"] = {
|
||||
"valid": False,
|
||||
"reconstruction_eligible": False,
|
||||
"solid_count": 0,
|
||||
"volume": 0.0,
|
||||
"rejection_reasons": ["no_closed_solid", "zero_volume"],
|
||||
}
|
||||
normalized = MODULE.normalize_case(payload)
|
||||
self.assertFalse(normalized["reconstruction_eligible"])
|
||||
self.assertIn("no_closed_solid", normalized["rejection_reasons"])
|
||||
|
||||
def test_sha256_identity_deduplicates_renamed_sources(self):
|
||||
digest = "0" * 64
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for index in range(2):
|
||||
payload = make_case(index)
|
||||
payload["provenance"]["source_sha256"] = digest
|
||||
(root / f"renamed-{index}.json").write_text(json.dumps(payload))
|
||||
cases, stats = MODULE.load_cases(root)
|
||||
self.assertEqual(1, len(cases))
|
||||
self.assertEqual(1, stats["duplicate_case_count"])
|
||||
|
||||
def test_conditional_feature_context_does_not_dilute_valid_method(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
@@ -202,6 +260,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual(1, len(library["experiences"]))
|
||||
|
||||
@@ -249,6 +308,29 @@ class CadExperienceTest(unittest.TestCase):
|
||||
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
|
||||
self.assertIn("distribution.sleeve_od_to_base_od", ids)
|
||||
|
||||
def test_audit_allows_aggregate_validation_counts_only(self):
|
||||
library = {
|
||||
"schema_version": "2.0",
|
||||
"library_kind": "generalized_cad_experience",
|
||||
"experiences": [
|
||||
{
|
||||
"id": "edit_strategy.test",
|
||||
"support": 20,
|
||||
"validation": {
|
||||
"heldout_pair_count": 20,
|
||||
"treatment_pass_count": 20,
|
||||
"regressed_pair_count": 0,
|
||||
"pass_rate_delta": 0.25,
|
||||
},
|
||||
}
|
||||
],
|
||||
"candidate_experiences": [],
|
||||
}
|
||||
self.assertEqual([], MODULE.audit_library(library))
|
||||
library["experiences"][0]["validation"]["radius"] = 3.0
|
||||
errors = MODULE.audit_library(library)
|
||||
self.assertTrue(any("forbidden instance key" in error for error in errors))
|
||||
|
||||
def test_family_support_is_not_diluted_by_unrelated_families(self):
|
||||
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
unrelated = []
|
||||
@@ -299,7 +381,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual([], candidate_library["experiences"])
|
||||
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
|
||||
self.assertTrue(candidate_library["policy"]["router_consumable"])
|
||||
self.assertFalse(candidate_library["policy"]["router_consumable"])
|
||||
|
||||
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
|
||||
promoted_library = MODULE.build_reviewed_library(
|
||||
@@ -308,10 +390,71 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual(1, len(promoted_library["experiences"]))
|
||||
self.assertEqual([], promoted_library["candidate_experiences"])
|
||||
|
||||
def test_parameter_naming_requires_positive_heldout_validation(self):
|
||||
cases = []
|
||||
for index in range(20):
|
||||
payload = make_case(index)
|
||||
payload["design_ir"]["semantic_parameter_candidates"] = [
|
||||
{
|
||||
"name": "bolt_circle_diameter",
|
||||
"unit": "mm",
|
||||
"confidence": 0.9,
|
||||
"semantic_aliases": ["hole_spacing"],
|
||||
"affects_feature_roles": ["mounting_pattern"],
|
||||
"epistemic_status": "inferred_from_final_brep",
|
||||
"edit_contract": {
|
||||
"expected_change": "pattern_radius_changes"
|
||||
},
|
||||
}
|
||||
]
|
||||
cases.append(MODULE.normalize_case(payload))
|
||||
raw = {
|
||||
"id": "parameter.flange.bolt_circle_diameter",
|
||||
"kind": "parameter_naming",
|
||||
"scope": ["flanged_hub_adapter"],
|
||||
"evidence_query": {
|
||||
"required_features": ["base_flange"],
|
||||
"required_relations": [],
|
||||
"required_parameters": ["bolt_circle_diameter"],
|
||||
},
|
||||
"guidance": "Expose the circular mounting pattern diameter as an editable semantic parameter.",
|
||||
"semantic_rationale": "The named role preserves edit intent across dimensional variants.",
|
||||
"canonical_parameter": "bolt_circle_diameter",
|
||||
"semantic_aliases": ["hole_spacing"],
|
||||
}
|
||||
proposal = MODULE.normalize_proposals(
|
||||
{
|
||||
"draft_kind": "llm_generalized_experience_proposals",
|
||||
"proposals": [raw],
|
||||
}
|
||||
)[0]
|
||||
unvalidated = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
)
|
||||
self.assertEqual([], unvalidated["experiences"])
|
||||
self.assertEqual(
|
||||
"awaiting_positive_heldout_ab",
|
||||
unvalidated["candidate_experiences"][0]["validation_state"],
|
||||
)
|
||||
validated = MODULE.build_reviewed_library(
|
||||
cases,
|
||||
[proposal],
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(raw["id"]),
|
||||
)
|
||||
self.assertEqual("parameter_naming", validated["experiences"][0]["kind"])
|
||||
|
||||
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
|
||||
proposal = self.reviewed_proposal()
|
||||
proposal["guidance"] = "Always use a diameter of 91 millimeters."
|
||||
@@ -375,6 +518,7 @@ class CadExperienceTest(unittest.TestCase):
|
||||
20,
|
||||
0.8,
|
||||
{"duplicate_case_count": 0, "rejected_case_count": 0},
|
||||
positive_validation(proposal["id"]),
|
||||
)
|
||||
self.assertEqual([], MODULE.audit_library(library))
|
||||
self.assertEqual("reconstruction_grammar", library["experiences"][0]["kind"])
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from build123d import Plane, Rectangle
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
SURFACEIR = load_module(
|
||||
"surfaceir_pipeline", ROOT / "scripts" / "surfaceir_pipeline.py"
|
||||
)
|
||||
DESIGNIR = load_module(
|
||||
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
|
||||
)
|
||||
|
||||
|
||||
class SurfaceIRPipelineTests(unittest.TestCase):
|
||||
def test_independent_json_roundtrip_preserves_exact_geometry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "teacher.step"
|
||||
portable_json = directory / "model.designir.json"
|
||||
rebuilt = directory / "rebuilt.step"
|
||||
expected = DESIGNIR.Box(10, 20, 30)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
surfaceir = SURFACEIR.extract_surfaceir(teacher)
|
||||
SURFACEIR.write_json(portable_json, surfaceir)
|
||||
teacher.unlink()
|
||||
|
||||
serialized = portable_json.read_text(encoding="utf-8").lower()
|
||||
self.assertNotIn("teacher.step", serialized)
|
||||
self.assertNotIn("source_path", serialized)
|
||||
self.assertNotIn("import_step", serialized)
|
||||
self.assertNotIn('"mesh"', serialized)
|
||||
|
||||
rebuilt_shape = SURFACEIR.build_surfaceir(surfaceir)
|
||||
SURFACEIR.export_step_shape(rebuilt_shape, rebuilt)
|
||||
self.assertTrue(rebuilt.is_file())
|
||||
|
||||
actual = DESIGNIR.import_step(rebuilt)
|
||||
self.assertEqual(len(expected.solids()), len(actual.solids()))
|
||||
self.assertEqual(len(expected.faces()), len(actual.faces()))
|
||||
self.assertEqual(len(expected.edges()), len(actual.edges()))
|
||||
self.assertLess(
|
||||
abs(expected.volume - actual.volume) / expected.volume, 1e-10
|
||||
)
|
||||
self.assertLess(
|
||||
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
|
||||
)
|
||||
|
||||
def test_empty_step_document_is_represented_honestly(self) -> None:
|
||||
payload = {
|
||||
"schema_version": "3.0",
|
||||
"designir_kind": "independent_parametric_cad",
|
||||
"document_status": "empty_geometry",
|
||||
"surface_layer": {
|
||||
"vertices": [],
|
||||
"solids": [],
|
||||
"free_shells": [],
|
||||
},
|
||||
}
|
||||
rebuilt = SURFACEIR.build_surfaceir(payload)
|
||||
self.assertEqual(SURFACEIR._facts_from_shape(rebuilt)["face_count"], 0)
|
||||
|
||||
def test_open_shell_roundtrip_preserves_surface_geometry(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "surface.step"
|
||||
rebuilt_path = directory / "surface-rebuilt.step"
|
||||
face = (Plane.XY * Rectangle(10, 20)).face()
|
||||
builder = SURFACEIR.BRep_Builder()
|
||||
shell = SURFACEIR.TopoDS_Shell()
|
||||
builder.MakeShell(shell)
|
||||
builder.Add(shell, face.wrapped)
|
||||
SURFACEIR.export_step_shape(shell, teacher)
|
||||
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
self.assertEqual(payload["document_status"], "geometry_present")
|
||||
self.assertEqual(len(payload["surface_layer"]["solids"]), 0)
|
||||
self.assertEqual(len(payload["surface_layer"]["free_shells"]), 1)
|
||||
rebuilt = SURFACEIR.build_surfaceir(
|
||||
payload, "exact_3d_pcurve"
|
||||
)
|
||||
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
|
||||
_, teacher_facts = SURFACEIR._shape_facts(teacher)
|
||||
_, rebuilt_facts = SURFACEIR._shape_facts(rebuilt_path)
|
||||
self.assertEqual(teacher_facts["face_count"], 1)
|
||||
self.assertEqual(
|
||||
teacher_facts["face_count"], rebuilt_facts["face_count"]
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
teacher_facts["surface_area"],
|
||||
rebuilt_facts["surface_area"],
|
||||
places=8,
|
||||
)
|
||||
|
||||
def test_overall_scale_is_executable_and_independently_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "teacher.step"
|
||||
designir_path = directory / "box.designir.json"
|
||||
expected = DESIGNIR.Box(10, 20, 30)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
self.assertIn(
|
||||
"overall_scale",
|
||||
payload["edit_interface"]["semantic_parameters"],
|
||||
)
|
||||
|
||||
edited = SURFACEIR.apply_semantic_parameter(
|
||||
payload, "overall_scale", 1.1
|
||||
)
|
||||
self.assertEqual(
|
||||
edited["edit_operations"],
|
||||
[
|
||||
{
|
||||
"operation": "uniform_scale",
|
||||
"factor": 1.1,
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertIsNotNone(SURFACEIR.build_surfaceir(edited))
|
||||
|
||||
SURFACEIR.write_json(designir_path, payload)
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
designir_path,
|
||||
"overall_scale",
|
||||
1.1,
|
||||
directory / "acceptance",
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertTrue(
|
||||
acceptance["checks"]["target_parameter"]["pass"]
|
||||
)
|
||||
|
||||
def test_primary_axis_dimensions_are_executable_and_validated(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "shaft.step"
|
||||
designir_path = directory / "shaft.designir.json"
|
||||
DESIGNIR.export_step(DESIGNIR.Cylinder(10, 40), teacher)
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
SURFACEIR.write_json(designir_path, payload)
|
||||
|
||||
for parameter, value in (
|
||||
("outer_diameter", 22.0),
|
||||
("body_length", 44.0),
|
||||
):
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
designir_path,
|
||||
parameter,
|
||||
value,
|
||||
directory / parameter,
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertTrue(
|
||||
acceptance["checks"]["target_parameter"]["pass"]
|
||||
)
|
||||
|
||||
def test_canonical_semantics_name_bore_and_polar_hole_pattern(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
teacher = Path(temporary) / "flange.step"
|
||||
expected = DESIGNIR.Cylinder(20, 5) - DESIGNIR.Cylinder(5, 5)
|
||||
for x, y in ((10, 0), (0, 10), (-10, 0), (0, -10)):
|
||||
expected -= DESIGNIR.Pos(x, y, 0) * DESIGNIR.Cylinder(1, 5)
|
||||
DESIGNIR.export_step(expected, teacher)
|
||||
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
semantic = payload["semantic_layer"]
|
||||
parameters = semantic["parameters"]
|
||||
|
||||
self.assertEqual(parameters["hole_count"]["value"], 4)
|
||||
self.assertAlmostEqual(parameters["hole_diameter"]["value"], 2)
|
||||
self.assertAlmostEqual(
|
||||
parameters["bolt_circle_diameter"]["value"], 20
|
||||
)
|
||||
self.assertAlmostEqual(parameters["bore_diameter"]["value"], 10)
|
||||
self.assertAlmostEqual(parameters["outer_diameter"]["value"], 40)
|
||||
self.assertAlmostEqual(parameters["plate_thickness"]["value"], 5)
|
||||
self.assertTrue(
|
||||
any(
|
||||
feature["operation"] == "polar_hole_pattern"
|
||||
for feature in semantic["features"]
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
semantic["inference_policy"]["claims_original_feature_history"]
|
||||
)
|
||||
|
||||
edited = SURFACEIR.apply_semantic_parameter(
|
||||
payload, "hole_diameter", 3
|
||||
)
|
||||
self.assertEqual(len(edited["edit_operations"]), 4)
|
||||
self.assertEqual(
|
||||
edited["active_edit"]["acceptance_status"], "pending"
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "increases only"):
|
||||
SURFACEIR.apply_semantic_parameter(
|
||||
payload, "hole_diameter", 1.5
|
||||
)
|
||||
edited_step = Path(temporary) / "flange-edited.step"
|
||||
edited_shape = SURFACEIR.build_surfaceir(edited)
|
||||
SURFACEIR.export_step_shape(edited_shape, edited_step)
|
||||
edited_payload = SURFACEIR.extract_surfaceir(edited_step)
|
||||
edited_parameters = edited_payload["semantic_layer"]["parameters"]
|
||||
self.assertAlmostEqual(
|
||||
edited_parameters["hole_diameter"]["value"], 3
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
edited_parameters["bore_diameter"]["value"], 10
|
||||
)
|
||||
self.assertEqual(edited_parameters["hole_count"]["value"], 4)
|
||||
actual = DESIGNIR.import_step(edited_step)
|
||||
self.assertTrue(actual.is_valid)
|
||||
self.assertLess(actual.volume, expected.volume)
|
||||
|
||||
original_ir = Path(temporary) / "flange.designir.json"
|
||||
SURFACEIR.write_json(original_ir, payload)
|
||||
acceptance = SURFACEIR.validate_semantic_edit(
|
||||
teacher,
|
||||
original_ir,
|
||||
"hole_diameter",
|
||||
2.5,
|
||||
Path(temporary) / "acceptance",
|
||||
)
|
||||
self.assertTrue(acceptance["accepted"])
|
||||
self.assertEqual(acceptance["state"], "accepted")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user