Pattern Detection#
Overview#
The pattern detection module recognises parametric arrangements of congruent features (currently holes) in a B-Rep model — for example a 3×4 rectangular grid of M6 holes or a circular array of 9 vent holes. Detected patterns expose the design-intent parameters (origin, axes, spacings, counts, radius, angular spacing) that a CAD modeller would have specified at part-creation time.
The module is part of the volmdlr_tools.features package and lives under
volmdlr_tools.features.patterns. It is consumed downstream by parametric
code-generation, family-of-parts analysis, and feature-aware comparison
tooling.
Module Layout#
File |
Responsibility |
|---|---|
|
Pure-numeric primitives + OCC center/axis helpers |
|
Rectangular-grid detection |
|
Circular-array detection |
Congruence grouping (the upstream stage that selects which features are
candidates for one pattern) lives in volmdlr_tools.features.groups,
not in this module — see volmdlr_tools.features.groups.group_holes().
Pipeline#
Pattern detection is a two-stage pipeline. The first stage groups features by congruence, the second runs both detectors over each group.
features (e.g. all holes)
│
▼
┌──────────────────────┐
│ group_holes(...) │ bucket by (base_node, type, radius, depth,
│ (features.groups) │ is_blind, *per-type extras) — see "Stage 1"
└─────────┬────────────┘
│ list[HoleGroup]
│
│ .holes (a shape-congruent slice)
│
┌─────┴─────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ GridPattern │ │ PolarPattern │
│ Detector.detect()│ │ Detector.detect()│
└─────────┬────────┘ └─────────┬────────┘
│ │
▼ ▼
list[GridPattern] list[PolarPattern]
Why congruence first? A grid or polar array is, by construction, a repetition of the same feature. Mixing dissimilar features into a single candidate set would inflate the false-positive rate and waste work on arrangements that are physically meaningless.
The detectors classify on shape alone. group_holes is also
base-face aware (so two same-shape holes on different faces land in
different HoleGroup buckets) — that face awareness matters for the
reconstruction pipeline but not for the detectors. When a pattern
legitimately spans multiple faces (e.g. an 8-hole polar pattern wrapping
two cylindrical sections of a revolution part), assemble the detector
input by filtering on shape alone rather than passing a single
HoleGroup.holes slice.
Stage 1 — Congruence Grouping#
group_holes (in volmdlr_tools.features.groups.holes)#
group_holes(holes, radius_tolerance, depth_tolerance, angle_tolerance)
builds a hashable congruence signature per hole, buckets by
(first_base_node, signature), and returns a list of
HoleGroup instances. The
signature is type-aware:
Concrete type name (
"RoundHole","CounterboreHole","CountersinkHole","CounterdrillHole") — different types never merge even when their primary radius / depth coincide.radiusanddepth, each rounded to the nearest multiple of the corresponding tolerance.is_blindflag.Per-type extras: counter-bore radius+depth for
CounterboreHole, countersink radius+cone angle forCountersinkHole, entry radius+cone angle forCounterdrillHole.
Holes with an empty base_nodes list are silently dropped (no face to
anchor the group to). Default tolerances are 1e-4 for radius/depth
(metres-scale geometry) and 0.5 degrees for cone angles.
Extending to a new feature type today means adding a sibling
group_<feature> function with the same shape — a signature builder
plus a per-bucket dataclass. The detectors don’t depend on HoleGroup
directly, only on the .holes (or equivalent .features) list it
exposes.
Stage 2 — Pattern Detectors#
Both detectors share the same outer skeleton:
Look up each feature’s center via the AAG (
extract_centers). If any feature lacks a cylindrical face (and therefore a defined center), the whole group is rejected — a partial pattern would be misleading.Project the 3D centers onto their best-fit plane via SVD. Non-coplanar groups are rejected up-front.
Solve a 2D problem in that plane (grid basis or circle fit).
Validate completeness/consistency.
Lift the recovered 2D parameters back to 3D and assemble the result dataclass.
This shared shape is implemented through the primitives in geometry.py,
which are the load-bearing pieces of the algorithm and are described next.
Geometric Primitives#
All pure-numeric primitives live in volmdlr_tools.features.patterns.geometry
and operate on numpy arrays so they are unit-testable without OCC.
project_to_best_fit_plane(points_3d, coplanarity_tol=0.01)#
Centers the points, runs SVD on the centered matrix, and uses the singular-value spectrum to:
Reject non-coplanar inputs: if
s[2] > coplanarity_tol * s[0]the third principal direction carries non-trivial variance, meaning the points are not flat enough to be a planar pattern. The tolerance is relative to the largest in-plane extent, so it is scale-invariant.Build an orthonormal in-plane basis from the first two right-singular vectors (
vt[0],vt[1]) and the plane normal fromvt[2].Project to 2D: each point’s 2D coordinates are its dot-products with the in-plane basis vectors.
The returned PlaneProjection carries the centroid and basis so the
caller can lift 2D results back to 3D without recomputing the SVD.
fit_circle_kasa(xs, ys)#
Algebraic (Kasa) circle fit. Solves the linear least-squares system
[x_i y_i 1] · [A B C]ᵀ ≈ x_i² + y_i²
then recovers cx = A/2, cy = B/2, r² = C + cx² + cy². Returns
None for fewer than 3 points or if the resulting r² is negative
(degenerate). Kasa is biased toward smaller circles when noise is high,
but for clean CAD-derived centers this bias is irrelevant. The follow-up
radial-consistency check in PolarPatternDetector rejects fits
where any point drifts away from the mean radius.
find_grid_basis(pts_2d, collinear_tol=0.01)#
Recovers two non-collinear basis vectors that should generate the lattice:
Enumerate every pairwise vector between distinct points.
Sort by length and pick the shortest vector as
u— in a regular grid the shortest pairwise vector necessarily aligns with one of the lattice axes.Walk the sorted list and pick the first vector whose 2D cross product with
uexceedscollinear_tol * |u| * |v|; this isv.
The cross-product check is relative to vector magnitudes, which makes the collinearity test robust to scale.
compute_grid_coords(pts_2d, origin, u, v, tol_fraction=0.01)#
Snaps each point onto integer (i, j) lattice coordinates by inverting
the basis matrix [u | v] and rounding. If any point’s reconstructed
position deviates from its true position by more than
tol_fraction * min(|u|, |v|), the function returns None —
the candidate pair is not a valid grid basis after all. This snap-and-verify
step is what cleanly rejects patterns that look like grids in some
short-vector basis but actually have off-lattice points.
find_fundamental_spacing(deltas, angle_tol=2.0)#
Given the sorted angular gaps between centers around a circle, decides whether they all reduce to integer multiples of a common fundamental spacing — i.e. whether the array is regular even with missing teeth (e.g. 9 holes spanning 8 of 12 evenly-spaced positions).
Algorithm: the smallest gap is taken as the fundamental candidate; every
other gap must round to an integer multiple of it within angle_tol
degrees. If any gap fails this test, the function returns None.
OCC helpers#
get_hole_center(hole, aag) and get_hole_axis(hole, aag) walk a
hole’s face nodes, locate the first cylindrical face via
BRepAdaptor_Surface, and read off Cylinder.Location() /
Cylinder.Axis().Direction(). extract_centers is a small wrapper
that aborts (returns None) if any feature in the group lacks a
cylindrical face.
To support a non-hole feature type, supply analogous get_*_center and
get_*_axis helpers and route them through extract_centers (or a
sibling), then add a corresponding group_<feature> function under
volmdlr_tools.features.groups returning a per-bucket dataclass
whose .holes (or .features) attribute exposes a shape-congruent
slice.
Detector — GridPatternDetector#
detect(features) →
1. centers ← extract_centers(features) ; require ≥ 4 (a 2×2)
2. proj ← project_to_best_fit_plane(pts_3d)
3. (u, v, origin_idx) ← find_grid_basis(proj.pts_2d)
4. coords ← compute_grid_coords(...) ; integer (i,j) snap
5. count_x, count_y ← spans of i, j
6. validate count_x ≥ 2, count_y ≥ 2,
count_x · count_y == len(features) ; complete grid
7. _build_pattern(...) ; lift 2D → 3D + canonicalise
The completeness check (count_x * count_y == len(features)) is
deliberately strict. Sparse / partial grids are not modelled today; if a
group needs them the validation step is the single place to relax.
A canonicalisation step in _build_pattern enforces
spacing_x >= spacing_y by swapping the basis and counts when needed,
so two parts with the same logical grid produce the same
GridPattern regardless of the order find_grid_basis returned
its vectors.
Detector — PolarPatternDetector#
detect(features) →
1. centers ← extract_centers(features) ; require ≥ 3
2. proj ← project_to_best_fit_plane(pts_3d)
3. (cx, cy, _) ← fit_circle_kasa(...)
4. radial-consistency check:
ptp(distances) ≤ _RADIAL_CONSISTENCY_TOL · mean(distances)
5. angles ← arctan2 of each point about (cx, cy), wrapped to [0, 360)
6. all_deltas ← consecutive sorted-angle gaps + wraparound gap
7. spacing ← find_fundamental_spacing(all_deltas)
8. _build_pattern(...) ; lift 2D → 3D
The wraparound gap (360° − last + first) is appended to the consecutive
deltas before calling find_fundamental_spacing() so a full revolution
is treated as a closed cycle. This means a 9-hole bolt circle and a
9-hole partial arc are both detectable — the difference shows up only in
the gap structure.
The detector returns features sorted by angle, so consumers can iterate the pattern in geometric order without resorting again.
Result dataclasses#
Both detectors return small @dataclass value objects:
GridPattern—features,origin,direction_x,direction_y,spacing_x,spacing_y,count_x,count_y.PolarPattern—features(sorted by angle),axis_origin,axis_direction,radius,count,start_angle,angular_spacing.
These are intentionally plain dataclasses — they hold the recovered parameters and nothing else. Anything that needs a richer representation (serialisation, equality semantics, persistence) wraps them rather than extends them.
Tunables#
Parameter |
Default |
Effect |
|---|---|---|
|
|
Bucket width on rounded radius |
|
|
Bucket width on rounded depth |
|
|
Bucket width on counter-sink / counter-drill cone angle |
|
|
Max |
|
|
Min |
|
|
Snap tolerance, fraction of basis length |
|
|
Angular tolerance for integer-multiple check |
|
|
Relative |
|
|
Minimum points for circle fit |
|
|
Smallest grid is 2×2 |
|
|
Smallest polar array |
|
|
|
All tolerances are relative (or angle-absolute) so they are stable across unit systems and part scales.
Worked Example#
from volmdlr.shapes import Solid
from volmdlr_tools.features.extractors.holes import HoleExtractor
from volmdlr_tools.features.groups import group_holes
from volmdlr_tools.features.patterns import (
GridPatternDetector,
PolarPatternDetector,
)
from volmdlr_tools.graph.faces import AttributedAdjacencyGraph
shape = Solid.from_brep("data/brep/ANC101.brep")
aag = AttributedAdjacencyGraph(shape)
holes = HoleExtractor(aag=aag).result
grid_detector = GridPatternDetector(aag)
polar_detector = PolarPatternDetector(aag)
for group in group_holes(holes):
for grid in grid_detector.detect(group.holes):
print(f"grid: {grid.count_x}x{grid.count_y} spacing="
f"({grid.spacing_x:.2f}, {grid.spacing_y:.2f})")
for polar in polar_detector.detect(group.holes):
print(f"polar: {polar.count} holes, "
f"r={polar.radius:.2f}, step={polar.angular_spacing:.2f}°")
For ANC101 this prints one 2×2 grid and one 9-hole polar array
(13 of the 17 holes participate in patterns; the remaining four are
classified as singleton features).
When a pattern legitimately spans multiple base faces, the per-face
HoleGroup slices are too narrow — assemble the detector input from
the raw hole list by filtering on shape instead:
medium_holes = [h for h in holes if abs(h.radius - 0.015) < 1e-3]
polar_detector.detect(medium_holes)
Extension Points#
- Adding a new feature type
Add a sibling
group_<feature>function undervolmdlr_tools.features.groupswith a type-aware signature builder and a per-bucket dataclass (mirrorgroup_holes/HoleGroup).Provide center / axis helpers analogous to
get_hole_center/get_hole_axis(or generaliseextract_centersto dispatch on type).
- Adding a new arrangement
Mirror the structure of
GridPatternDetector/PolarPatternDetector: reuseproject_to_best_fit_planeto reach 2D, run an arrangement-specific solver (e.g. mirror, linear array, hex grid), and assemble a result dataclass. Keep the solver pure-numeric and put it ingeometry.pyso it stays testable in isolation from OCC.
Limitations#
Only complete grids are detected (no missing positions).
Only planar arrangements are detected — features whose centers are not coplanar are rejected by the SVD step.
Only features with a cylindrical face contribute a center today.
A given group produces at most one pattern per detector — overlapping arrangements within a single congruence bucket are not split apart.
group_holesonly handlesHolesubclasses today. Other feature types need their owngroup_<feature>sibling undervolmdlr_tools.features.groups.
Each of these limits is a single, localised relaxation point in the code and is called out in the corresponding section above.
API Reference#
Feature pattern detection for parametric code generation.
- class volmdlr_tools.features.patterns.GridPattern(features: list, origin: Point3D, direction_x: Vector3D, direction_y: Vector3D, spacing_x: float, spacing_y: float, count_x: int, count_y: int)#
A rectangular grid of congruent features.
- Parameters:
features – List of features in the pattern.
origin – Origin point of the grid (corner with smallest indices).
direction_x – Unit vector along the grid X axis.
direction_y – Unit vector along the grid Y axis.
spacing_x – Spacing between features along X.
spacing_y – Spacing between features along Y.
count_x – Number of features along X.
count_y – Number of features along Y.
- count_x: int#
- count_y: int#
- direction_x: Vector3D#
- direction_y: Vector3D#
- features: list#
- origin: Point3D#
- spacing_x: float#
- spacing_y: float#
- class volmdlr_tools.features.patterns.GridPatternDetector(aag: AttributedAdjacencyGraph)#
Detect rectangular grid patterns among congruent features.
Uses SVD for coplanarity, shortest-vector basis extraction, and integer grid fitting to verify the pattern.
- Parameters:
aag – Attributed adjacency graph of the part.
- detect(features: list, center_fn: ~collections.abc.Callable[[object, ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph], ~volmdlr.core_compiled.Point3D | None] = <function get_hole_center>) list[GridPattern]#
Detect grid patterns in the given feature list.
- Parameters:
features – List of shape-congruent features. Like
PolarPatternDetector, the grid detector classifies on shape alone — cross-face arrays should be assembled by shape rather than viavolmdlr_tools.features.groups.group_holes()’s face-aware buckets.center_fn – Callable mapping
(feature, aag) -> Point3D | None. Defaults to the hole-cylinder extractor; cut callers pass a profile-centroid extractor.
- Returns:
List of detected GridPattern instances (0 or 1).
- class volmdlr_tools.features.patterns.PolarPattern(features: list[object], circle: Circle3D, start_angle: float, angular_spacing: float)#
A circular array of congruent features around a common axis.
- Parameters:
features – Ordered list of features in the pattern, starting at
start_angleand going in increasing angle.circle –
volmdlr.curves.Circle3Dfully describing the pattern’s geometry (centre, normal, radius, in-plane basis). Theaxis_origin/axis_direction/radiusaccessors below derive their values from this single source of truth.start_angle – Angle of the first feature (degrees, 0-360), measured in
circle.frame— i.e. from the canonical +u axis the frame defines.angular_spacing – Fundamental angular spacing (degrees).
- angular_spacing: float#
- property axis_direction: Vector3D#
Plane normal (derived from
circle.frame.w).
- property axis_origin: Point3D#
Centre point of the circular array (derived from
circle.frame.origin).
- circle: Circle3D#
- property count: int#
Number of features in the pattern.
- features: list[object]#
- property radius: float#
Radius of the circle on which features lie (derived from
circle.radius).
- start_angle: float#
- class volmdlr_tools.features.patterns.PolarPatternDetector(aag: AttributedAdjacencyGraph)#
Detect polar (circular) patterns among congruent features.
Uses SVD for coplanarity testing, algebraic circle fitting, a canonical
volmdlr.Frame3Dderived from the fitted plane’s normal for angle measurement, and uniform angular-spacing verification.- Parameters:
aag – Attributed adjacency graph of the part.
- detect(features: list, center_fn: ~collections.abc.Callable[[object, ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph], ~volmdlr.core_compiled.Point3D | None] = <function get_hole_center>) list[PolarPattern]#
Detect polar patterns in the given feature list.
- Parameters:
features – List of shape-congruent features (e.g. all holes of a given radius). When grouping by reconstruction-time face awareness is needed,
volmdlr_tools.features.groups.group_holes()returns oneHoleGroupper (face, signature); the detector only cares about shape congruence, so cross-face polar arrays should be assembled by shape alone.center_fn – Callable mapping
(feature, aag) -> Point3D | None. Defaults to the hole-cylinder extractor; cut callers pass a profile-centroid extractor.
- Returns:
List of detected PolarPattern instances (0 or 1).
Congruence grouping for extracted holes.
Two holes belong in the same HoleGroup when they share the same
concrete type, the same first base face, and the same geometric
parameters within tolerance. The grouping is purely congruence-based:
no AAG traversal, no pattern detection, no base-plane resolution.
Reconstruction-layer wrappers (see volmdlr_tools.reverse_engineering.brep.hole_grouper)
populate the optional HoleGroup.base_plane and
HoleGroup.pattern fields on top of the pure groups produced
here.
- class volmdlr_tools.features.groups.holes.HoleGroup(index: int, holes: list[Hole], base_node: int, locations: list[volmdlr.Point3D] = <factory>, base_plane: volmdlr.Frame3D | None = None, pattern: PolarPattern | GridPattern | None = None)#
A group of congruent holes sharing the same base face.
- Parameters:
index – Sequential group index (0-based), assigned in enumeration order.
holes – The
Holeinstances in this group.base_node – Shared base face index (first
base_nodeof each hole).locations – Entry-point centres, for a
Locations(...)block.base_plane – Entry-face
Frame3D(w= outward normal), populated by the reconstruction-layer wrapper.Nonewhen produced by the puregroup_holes()function.pattern – Parametric pattern fitted to the group’s hole locations, populated by the reconstruction-layer wrapper.
Nonewhen produced bygroup_holes()(which does no pattern detection).
- property hole_type: str#
Class name of the hole type, e.g.
"RoundHole"(derived fromholes[0]).
- volmdlr_tools.features.groups.holes.group_holes(holes: list[Hole], radius_tolerance: float = 0.0001, depth_tolerance: float = 0.0001, angle_tolerance: float = 0.5) list[HoleGroup]#
Group holes by (first base face, congruence signature).
Holes with an empty
base_nodeslist, or with missingradius/depth, are dropped. Each returnedHoleGrouplists the congruent holes in insertion order, assignslocationsfrom each hole’slocationattribute, and leavesbase_planeandpatternunset.- Parameters:
holes – Extracted hole features.
radius_tolerance – Bucket width for radius comparisons (>0).
depth_tolerance – Bucket width for depth comparisons (>0).
angle_tolerance – Bucket width for cone-angle comparisons in degrees (>0).
- Raises:
ValueError – If any tolerance is non-positive.
- Returns:
Congruence groups, enumeration-ordered.
Detect rectangular grid patterns among congruent features.
- class volmdlr_tools.features.patterns.grid.GridPattern(features: list, origin: Point3D, direction_x: Vector3D, direction_y: Vector3D, spacing_x: float, spacing_y: float, count_x: int, count_y: int)#
A rectangular grid of congruent features.
- Parameters:
features – List of features in the pattern.
origin – Origin point of the grid (corner with smallest indices).
direction_x – Unit vector along the grid X axis.
direction_y – Unit vector along the grid Y axis.
spacing_x – Spacing between features along X.
spacing_y – Spacing between features along Y.
count_x – Number of features along X.
count_y – Number of features along Y.
- class volmdlr_tools.features.patterns.grid.GridPatternDetector(aag: AttributedAdjacencyGraph)#
Detect rectangular grid patterns among congruent features.
Uses SVD for coplanarity, shortest-vector basis extraction, and integer grid fitting to verify the pattern.
- Parameters:
aag – Attributed adjacency graph of the part.
- detect(features: list, center_fn: ~collections.abc.Callable[[object, ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph], ~volmdlr.core_compiled.Point3D | None] = <function get_hole_center>) list[GridPattern]#
Detect grid patterns in the given feature list.
- Parameters:
features – List of shape-congruent features. Like
PolarPatternDetector, the grid detector classifies on shape alone — cross-face arrays should be assembled by shape rather than viavolmdlr_tools.features.groups.group_holes()’s face-aware buckets.center_fn – Callable mapping
(feature, aag) -> Point3D | None. Defaults to the hole-cylinder extractor; cut callers pass a profile-centroid extractor.
- Returns:
List of detected GridPattern instances (0 or 1).
Detect circular/polar patterns among congruent features.
- class volmdlr_tools.features.patterns.polar.PolarPattern(features: list[object], circle: Circle3D, start_angle: float, angular_spacing: float)#
A circular array of congruent features around a common axis.
- Parameters:
features – Ordered list of features in the pattern, starting at
start_angleand going in increasing angle.circle –
volmdlr.curves.Circle3Dfully describing the pattern’s geometry (centre, normal, radius, in-plane basis). Theaxis_origin/axis_direction/radiusaccessors below derive their values from this single source of truth.start_angle – Angle of the first feature (degrees, 0-360), measured in
circle.frame— i.e. from the canonical +u axis the frame defines.angular_spacing – Fundamental angular spacing (degrees).
- property axis_direction: Vector3D#
Plane normal (derived from
circle.frame.w).
- property axis_origin: Point3D#
Centre point of the circular array (derived from
circle.frame.origin).
- property count: int#
Number of features in the pattern.
- property radius: float#
Radius of the circle on which features lie (derived from
circle.radius).
- class volmdlr_tools.features.patterns.polar.PolarPatternDetector(aag: AttributedAdjacencyGraph)#
Detect polar (circular) patterns among congruent features.
Uses SVD for coplanarity testing, algebraic circle fitting, a canonical
volmdlr.Frame3Dderived from the fitted plane’s normal for angle measurement, and uniform angular-spacing verification.- Parameters:
aag – Attributed adjacency graph of the part.
- detect(features: list, center_fn: ~collections.abc.Callable[[object, ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph], ~volmdlr.core_compiled.Point3D | None] = <function get_hole_center>) list[PolarPattern]#
Detect polar patterns in the given feature list.
- Parameters:
features – List of shape-congruent features (e.g. all holes of a given radius). When grouping by reconstruction-time face awareness is needed,
volmdlr_tools.features.groups.group_holes()returns oneHoleGroupper (face, signature); the detector only cares about shape congruence, so cross-face polar arrays should be assembled by shape alone.center_fn – Callable mapping
(feature, aag) -> Point3D | None. Defaults to the hole-cylinder extractor; cut callers pass a profile-centroid extractor.
- Returns:
List of detected PolarPattern instances (0 or 1).
Geometric utilities for pattern detection.
Pure functions for SVD projection, circle fitting, grid basis extraction, and angular spacing analysis. OCC-dependent helpers for center/axis extraction.
- class volmdlr_tools.features.patterns.geometry.PlaneProjection(pts_2d: ndarray, centroid: ndarray, u_basis: ndarray, v_basis: ndarray, normal: ndarray)#
Result of projecting 3D points onto their best-fit plane.
- volmdlr_tools.features.patterns.geometry.compute_grid_coords(pts_2d: ndarray, origin: ndarray, u: ndarray, v: ndarray, tol_fraction: float = 0.01) list[tuple[int, int]] | None#
Check all points lie on integer grid coordinates.
- Parameters:
pts_2d – Nx2 array of 2D points.
origin – 2D origin point.
u – First basis vector.
v – Second basis vector.
tol_fraction – Relative tolerance for grid-snap check.
- Returns:
List of (i, j) integer coordinates, or None if any point is off-grid.
- volmdlr_tools.features.patterns.geometry.extract_centers(features: list, aag: ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph, center_fn: ~collections.abc.Callable[[object, ~volmdlr_tools.graph.faces.AttributedAdjacencyGraph], ~volmdlr.core_compiled.Point3D | None] = <function get_hole_center>) list | None#
Extract center points from features. Returns None if any fails.
- Parameters:
center_fn – Callable mapping
(feature, aag) -> Point3D | None. Defaults toget_hole_center()so existing hole-pattern callers remain unchanged; non-hole callers (e.g. cut grouping) pass their own extractor.
- volmdlr_tools.features.patterns.geometry.find_fundamental_spacing(deltas: ndarray, angle_tol: float = 2.0) float | None#
Find the fundamental angular spacing from consecutive angle deltas.
All deltas must be positive integer multiples of the fundamental spacing.
The candidate fundamental is the mode of the deltas (the mean of the most-populated tolerance bin), not the min. The mode is robust against a single near-coincident pair producing a tiny outlier delta that would otherwise hijack a min-based pick and cause every real delta to fail the integer-ratio test.
- Parameters:
deltas – Array of consecutive angular differences (degrees).
angle_tol – Tolerance in degrees for angle comparisons.
- Returns:
Fundamental spacing in degrees, or None if no regular pattern.
- volmdlr_tools.features.patterns.geometry.find_grid_basis(pts_2d: ndarray, collinear_tol: float = 0.01) tuple[ndarray, ndarray, int] | None#
Find two non-collinear basis vectors from shortest pairwise distances.
- Parameters:
pts_2d – Nx2 array of 2D points.
collinear_tol – Relative cross-product threshold for collinearity.
- Returns:
(u, v, origin_index) or None.
- volmdlr_tools.features.patterns.geometry.fit_circle_kasa(xs: ndarray, ys: ndarray) tuple[float, float, float] | None#
Algebraic circle fit (Kasa method).
Near-collinear input makes the algebraic solver return a geometrically valid but practically useless huge-radius circle. The radius/bbox guard below rejects those fits before they reach a downstream consistency check (which would pass — the points really do lie on that huge circle).
- Parameters:
xs – X coordinates of points.
ys – Y coordinates of points.
- Returns:
(cx, cy, r) or None on failure / degenerate fit.
- volmdlr_tools.features.patterns.geometry.get_hole_axis(hole: object, aag: AttributedAdjacencyGraph) Vector3D | None#
Get the axis direction of a hole.
- volmdlr_tools.features.patterns.geometry.get_hole_center(hole: object, aag: AttributedAdjacencyGraph) Point3D | None#
Get the center point of a hole from its cylindrical face.
- volmdlr_tools.features.patterns.geometry.project_to_best_fit_plane(points_3d: ndarray, coplanarity_tol: float = 0.01) PlaneProjection | None#
Project 3D points onto their best-fit plane via SVD.
- Parameters:
points_3d – Nx3 array of 3D points.
coplanarity_tol – Relative tolerance for coplanarity check.
- Returns:
PlaneProjection or None if points are not coplanar, fewer than three points were supplied, or all points coincide.