Feature Reverse Engineering#
Overview#
Feature reverse engineering turns recognised features into parametric definitions — the data needed to re-cut each feature on a defeatured solid. For every feature it determines:
Profile: the cross-section contour (
Wire)Depth: the distance the feature was cut to
Axis: the machining direction
Origin: the point the profile plane is placed at
Blends: the fillets belonging to the feature, with their radii
Anatomy: which face plays which role (entry, wall, floor, blends)
Parent: which other feature this one was cut into, if any
The single entry point is FeatureReverseEngineer, in
volmdlr_tools.features.reverse_engineering:
from volmdlr.model import VolumeModel
from volmdlr_tools.features.reverse_engineering import FeatureReverseEngineer
from volmdlr_tools.graph.faces import AttributedAdjacencyGraph
solid = VolumeModel.from_step("part.step").primitives[0]
aag = AttributedAdjacencyGraph(shape=solid, allow_smooth=False)
aag.calculate_faces_angles()
plan = FeatureReverseEngineer(aag).reverse_engineer_all()
print(plan.summary())
for index, definition in enumerate(plan.definitions):
print(index, definition.feature_type, definition.depth, definition.parent_index)
A single part resolved into 26 definitions: 15 pockets (blue), 3 blind slots
(orange), 3 through holes (red, exiting the underside) and 5 blind holes
(green, in the pocket floors). Grey is the surrounding stock, which no feature
owns. Each coloured face set is one definition’s faces_to_defeature().#
A single already-extracted feature can be reversed on its own, without re-running recognition:
definition = FeatureReverseEngineer(aag).reverse_engineer(my_pocket)
reverse_engineer returns None when the feature type has no reverser
mapped, or when the seed adapter declines the feature.
Face Nomenclature#
Every reverser classifies the feature’s faces by their anatomical role before measuring anything. The vocabulary is shared across feature kinds:
Role |
Description |
|---|---|
Entry |
The face the feature opens through — typically a stock surface. It bounds the feature but is not part of the material removed. |
Wall |
Structural faces forming the sides, connecting the entry face to the floor. |
Floor |
The face at the bottom. Through features have none. |
Entry Blend |
Fillet faces at the transition between the entry face and the walls. |
Floor Blend |
Fillet faces at the transition between the walls and the floor. |
Wall Blend |
Fillet faces between adjacent walls, such as corner fillets. |
Faces coloured by role: entry, walls, floor and the blend faces between them.#
Architecture#
Recognition produces features; reverse engineering measures them. The two stay separate, joined by a seed:
InteractingFeatureExtractor recognise features on the AAG
│
▼
seed_from_feature adapt one Feature -> FeatureSeed
│ (classifies faces into roles)
▼
FeatureReverseEngineer dispatch on FeatureType
│
├── PocketReverser -> CavityDefinition
├── SlotReverser -> InteractingFeatureDefinition
├── StepReverser -> InteractingFeatureDefinition
└── HoleReverser -> HoleDefinition
│
▼
FeatureDecompositionPlan ordered definitions + parent wiring
The dispatch table is validated at construction: every feature type the orchestrator promises to reverse must have a reverser registered, so adding a new feature type cannot silently drop features at runtime.
Reversers#
One reverser per feature-type family. Each consumes a FeatureSeed and returns
a definition, delegating all measurement to the shared engines:
Reverser |
Feature types |
Returns |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Add a new feature type by writing a reverser and registering it — never by extending an existing one to cover a second type.
Definitions#
Three definition types, one per reverser family. They differ in what they carry — a hole knows its radius, a slot knows how it is swept — but agree on the parts the downstream pipeline relies on:
|
|
|
Meaning |
|
|---|---|---|---|---|
|
✔ |
✔ |
✔ |
Which kind of feature this is |
|
✔ |
✔ |
✔ |
The swept cross-section and where to sweep it |
|
✔ |
✔ |
The profile recognised as a standard parametric shape, paired with the
sketch frame it was measured in; |
|
|
|
|
|
Face-to-role mapping |
|
✔ |
✔ |
✔ |
The feature’s fillets |
|
✔ |
✔ |
✔ |
Index of the feature this one was cut into; |
|
✔ |
✔ |
✔ |
Faces to hand to defeaturing |
faces_to_defeature() is the shared contract with the defeaturing side: it
returns the feature’s own faces and excludes the entry faces, which belong to
the surrounding stock and must survive defeaturing — they are what closes the
opening on the original stock surface.
Cavity definitions expose their fillet radii directly, so a caller can ask about the fillets without walking the blend list:
definition.has_entry_blend # True when an entry fillet was found
definition.entry_fillet_radius # its radius, or None
definition.has_floor_blend
definition.floor_fillet_radius
The Plan#
reverse_engineer_all returns a FeatureDecompositionPlan: the definitions in
a stable order, with counts per definition kind and a one-line summary().
Two properties matter when consuming it:
Indices are stable.
parent_indexrefers to positions inplan.definitions, so the list must not be reordered in place.The order is not a build order. Sort explicitly — children before parents to defeature, parents before children to reconstruct — using the helpers in the reconstruction module.
Features whose type has no reverser are dropped from the plan rather than being carried as half-measured definitions. When that happens it is logged, so a part that loses features to an unrecognised type is visible rather than silently smaller.
Demo#
scripts/features/demo_interacting_feature_reverse_engineering.py runs the
whole pipeline over a set of STEP files and displays each recognised feature,
coloured by role. Its output on those files is pinned by
tests/features/test_reverse_engineering_orchestrator.py, so the script doubles
as the regression baseline: if the pipeline changes what it produces, that test
fails and the ground truth has to be updated deliberately.