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

geometry.py

Pure-numeric primitives + OCC center/axis helpers

grid.py

Rectangular-grid detection

polar.py

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.

  • radius and depth, each rounded to the nearest multiple of the corresponding tolerance.

  • is_blind flag.

  • Per-type extras: counter-bore radius+depth for CounterboreHole, countersink radius+cone angle for CountersinkHole, entry radius+cone angle for CounterdrillHole.

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:

  1. 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.

  2. Project the 3D centers onto their best-fit plane via SVD. Non-coplanar groups are rejected up-front.

  3. Solve a 2D problem in that plane (grid basis or circle fit).

  4. Validate completeness/consistency.

  5. 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 from vt[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, = C + cx² + cy². Returns None for fewer than 3 points or if the resulting 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:

  1. Enumerate every pairwise vector between distinct points.

  2. 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.

  3. Walk the sorted list and pick the first vector whose 2D cross product with u exceeds collinear_tol * |u| * |v|; this is v.

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:

  • GridPatternfeatures, origin, direction_x, direction_y, spacing_x, spacing_y, count_x, count_y.

  • PolarPatternfeatures (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

group_holes(radius_tolerance)

1e-4

Bucket width on rounded radius

group_holes(depth_tolerance)

1e-4

Bucket width on rounded depth

group_holes(angle_tolerance)

0.5°

Bucket width on counter-sink / counter-drill cone angle

project_to_best_fit_plane (coplanarity_tol)

0.01

Max s[2]/s[0] for coplanarity

find_grid_basis (collinear_tol)

0.01

Min |u×v| (relative) for non-collinearity

compute_grid_coords (tol_fraction)

0.01

Snap tolerance, fraction of basis length

find_fundamental_spacing (angle_tol)

2.0 (degrees)

Angular tolerance for integer-multiple check

_RADIAL_CONSISTENCY_TOL

0.01

Relative ptp/mean for circle-fit acceptance

_KASA_MIN_POINTS

3

Minimum points for circle fit

_MIN_FEATURES (grid)

4

Smallest grid is 2×2

_MIN_FEATURES (polar)

3

Smallest polar array

_MIN_GRID_DIM

2

count_x / count_y minimum

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
  1. Add a sibling group_<feature> function under volmdlr_tools.features.groups with a type-aware signature builder and a per-bucket dataclass (mirror group_holes / HoleGroup).

  2. Provide center / axis helpers analogous to get_hole_center / get_hole_axis (or generalise extract_centers to dispatch on type).

Adding a new arrangement

Mirror the structure of GridPatternDetector / PolarPatternDetector: reuse project_to_best_fit_plane to 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 in geometry.py so 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_holes only handles Hole subclasses today. Other feature types need their own group_<feature> sibling under volmdlr_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 via volmdlr_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_angle and going in increasing angle.

  • circlevolmdlr.curves.Circle3D fully describing the pattern’s geometry (centre, normal, radius, in-plane basis). The axis_origin / axis_direction / radius accessors 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.Frame3D derived 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 one HoleGroup per (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 Hole instances in this group.

  • base_node – Shared base face index (first base_node of each hole).

  • locations – Entry-point centres, for a Locations(...) block.

  • base_plane – Entry-face Frame3D (w = outward normal), populated by the reconstruction-layer wrapper. None when produced by the pure group_holes() function.

  • pattern – Parametric pattern fitted to the group’s hole locations, populated by the reconstruction-layer wrapper. None when produced by group_holes() (which does no pattern detection).

property hole_type: str#

Class name of the hole type, e.g. "RoundHole" (derived from holes[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_nodes list, or with missing radius / depth, are dropped. Each returned HoleGroup lists the congruent holes in insertion order, assigns locations from each hole’s location attribute, and leaves base_plane and pattern unset.

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 via volmdlr_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_angle and going in increasing angle.

  • circlevolmdlr.curves.Circle3D fully describing the pattern’s geometry (centre, normal, radius, in-plane basis). The axis_origin / axis_direction / radius accessors 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.Frame3D derived 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 one HoleGroup per (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 to get_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.