Skip to content

fix(Colocation): robustness and performance of colocation and degeneration inspections - #203

Merged
BotellaA merged 13 commits into
nextfrom
fix/colocation-degeneration-review
Oct 16, 2026
Merged

BotellaA merged 13 commits into
nextfrom
fix/colocation-degeneration-review

Conversation

@BotellaA

Copy link
Copy Markdown
Member

Summary

Robustness and performance fixes for the colocation and degeneration inspections, from a review of OpenGeode-Inspector. There is one commit per fix. The new tests are in tests/inspection/test-brep-colocation-degeneration.cpp.

Robustness

  • Use after scope: the surface small-edges task captured a loop-local flag by reference and read it after the loop iteration had ended. Only a sanitizer can catch this, so there is no deterministic test.
  • Unlinked colocated vertices: colocated vertices without a unique vertex all compared equal (NO_ID), so they were never reported. The filter now does one unique vertex lookup per point instead of one per pair.
  • Block edges: BRepComponentMeshesDegeneration::inspect_edges_degeneration(threshold) now includes block edges, as inspect_elements_degeneration already did.

Performance (identical results on all benchmark models)

  • Squared edge lengths are compared to the threshold.
  • Mesh edges: SurfaceEdges/SolidEdges were enabled on every surface and block, and kept attached to them, only to measure edge lengths. Element edges are now scanned directly. Edges are only built when a small edge is found, so that its index can be reported, and they are only kept on the components that have issues.
  • Polygon and polyhedron minimum heights are computed in parallel.
  • Colocated groups are only built for points that are actually colocated, instead of one vector per mesh vertex.
  • mesh_has_colocated_points stops at the first colocation.
  • Component colocation runs in parallel, with labels formatted once per component that has issues.
  • Unique vertices colocation:
    • component lookups dispatch on the vertex's component type once;
    • the point set is built directly, and index vectors replace GenericMapping;
    • the work is parallel, and lazy: it runs on first use instead of in the constructor.
check, model before after
inspect_elements_degeneration, cva/brep_from_topo 1.81 s 0.38 s
inspect_elements_degeneration, corbi_tet_mesh 0.37 s 0.10 s
inspect_unique_vertices, fimiston 0.19 s 0.085 s
inspect_unique_vertices, cva/brep_from_topo 0.21 s 0.087 s
inspect_meshes_point_colocations, fimiston 0.092 s 0.036 s
inspect_meshes_point_colocations, corbi_tet_mesh 0.055 s 0.013 s

The large models come from the Geode-Conversion, Geode-Explicit and Geode-Background test data.

Not changed

  • < vs <= against the threshold: OpenGeode mixes them too (is_edge_degenerated uses <=, is_polyhedron_degenerated uses <), and the difference only matters for a value exactly equal to the threshold.

🤖 Generated with Claude Code

BotellaA and others added 11 commits October 14, 2026 18:40
The small edges task of each surface captured the loop-local enable_edges
flag by reference and read it after the iteration ended (use after scope).
Not covered by a deterministic test: only sanitizers can catch it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vertices without unique vertex all compared equal (NO_ID) and were filtered
out as if they were the same unique vertex. The filter now keeps one point
per unique vertex plus every unlinked point, with one unique vertex lookup
per point instead of one per pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edges

inspect_edges_degeneration(threshold) only checked line and surface edges,
while inspect_elements_degeneration also reports block small edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Small edges detection computed a square root per edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Small edges detection enabled the SurfaceEdges/SolidEdges of every surface
and block (one hash map insertion per element edge) and kept them attached
to the meshes, only to test edge lengths. Element edges are now scanned
directly; edges are built only when a small edge is found, to report its
index, and only kept on the components having issues.

cva/brep_from_topo 1.81s -> 1.55s, corbi_tet_mesh 0.37s -> 0.36s
(inspect_elements_degeneration, identical results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
small_height_polygons/polyhedra computed the minimum height of each element
serially. Heights are now computed in parallel, issues are added in element
order afterwards.

cva/brep_from_topo 1.55s -> 0.38s, corbi_tet_mesh 0.36s -> 0.10s
(inspect_elements_degeneration, identical results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A vector was allocated for every unique point (one allocation per mesh
vertex) before dropping the groups with a single point. Points are now
counted per unique point and groups only built when needed, in the same
order. cva/brep_from_topo 0.114s -> 0.092s (identical results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

The boolean query computed the full colocation mapping. It now looks for a
second point within GLOBAL_EPSILON among the two nearest neighbors of each
point, in parallel, and stops as soon as one is found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The line, surface and block colocation loops were three copies of the same
code, each formatting the component name and uuid twice per issue. They
share one helper which builds the label once, only for components having
colocated vertices. Messages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each line, surface and block built its NNSearch serially. Components are now
inspected in parallel tasks; issues are added in the same order.
corbi_tet_mesh 0.037s -> 0.013s, fimiston 0.073s -> 0.036s (identical
results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- component lookups: model.component(id) tried every component type before a
  second typed lookup; the cmv type is now dispatched once
- the active unique vertices PointSet is built directly instead of being
  created for all unique vertices then shrunk with delete_vertices, and the
  GenericMapping is replaced by index vectors
- points and per unique vertex checks are computed in parallel
- the work is done on first use instead of in the constructor, which
  BRepMeshesInspector always calls

fimiston 0.19s -> 0.085s, cva/brep_from_topo 0.21s -> 0.087s
(inspect_unique_vertices, identical results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BotellaA
BotellaA requested a review from panquez October 14, 2026 17:33
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-tidy (v21.1.8) reports: 32 concern(s)
  • include/geode/inspector/inspection/criterion/internal/component_meshes_degeneration.hpp:41:1: warning: [modernize-concat-nested-namespaces]

    nested namespaces can be concatenated

       41 | namespace geode
          | ^~~~~~~~~~~~~~~
       42 | {
          | ~
       43 |     namespace internal
          |     ~~~~~~~~~~~~~~~~~~
          |     namespace geode::internal
  • include/geode/inspector/inspection/criterion/internal/component_meshes_degeneration.hpp:50:15: warning: [cppcoreguidelines-special-member-functions]

    class 'ComponentMeshesDegeneration' defines a destructor, a copy constructor and a copy assignment operator but does not define a move constructor or a move assignment operator

       50 |         class ComponentMeshesDegeneration
          |               ^
  • include/geode/inspector/inspection/criterion/internal/degeneration_impl.hpp:29:1: warning: [modernize-concat-nested-namespaces]

    nested namespaces can be concatenated

       29 | namespace geode
          | ^~~~~~~~~~~~~~~
       30 | {
          | ~
       31 |     namespace internal
          |     ~~~~~~~~~~~~~~~~~~
          |     namespace geode::internal
  • include/geode/inspector/inspection/criterion/internal/degeneration_impl.hpp:37:15: warning: [cppcoreguidelines-special-member-functions]

    class 'DegenerationImpl' defines a destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator

       37 |         class DegenerationImpl
          |               ^
  • src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:54:9: warning: [readability-function-cognitive-complexity]

    function 'filter_colocated_points_with_same_unique_vertex' has cognitive complexity of 11 (threshold 10)

       54 |         filter_colocated_points_with_same_unique_vertex( const Model& model,
          |         ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:61:9: note: +1, including nesting penalty of 0, nesting level increased to 1
       61 |         for( const auto& point_group : colocated_points_groups )
          |         ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:68:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       68 |             if( first_uv != geode::NO_ID )
          |             ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:73:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       73 |             for( const auto pt_id : geode::Range{ 1, point_group.size() } )
          |             ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:77:17: note: +3, including nesting penalty of 2, nesting level increased to 3
       77 |                 if( uv == geode::NO_ID || unique_vertices.insert( uv ).second )
          |                 ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:77:40: note: +1
       77 |                 if( uv == geode::NO_ID || unique_vertices.insert( uv ).second )
          |                                        ^
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:82:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       82 |             if( !colocated_points.empty() )
          |             ^
  • src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:75:28: warning: [readability-identifier-length]

    variable name 'uv' is too short, expected at least 3 characters

       75 |                 const auto uv =
          |                            ^
  • src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:107:21: warning: [readability-identifier-length]

    variable name 'id' is too short, expected at least 3 characters

      107 |         const auto& id = component.id();
          |                     ^
  • src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:149:19: warning: [readability-identifier-length]

    variable name 'id' is too short, expected at least 3 characters

      149 |             auto [id, issues] = task.get();
          |                   ^
  • src/geode/inspector/inspection/criterion/colocation/component_meshes_colocation.cpp:217:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

      217 |         Impl( const Model& model ) : model_( model ) {}
          |         ^
          |         explicit 
  • src/geode/inspector/inspection/criterion/colocation/unique_vertices_colocation.cpp:70:21: warning: [readability-identifier-length]

    variable name 'id' is too short, expected at least 3 characters

       70 |         const auto& id = cmv.component_id.id;
          |                     ^
  • src/geode/inspector/inspection/criterion/colocation/unique_vertices_colocation.cpp:157:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

      157 |         Impl( const Model& model ) : model_( model ) {}
          |         ^
          |         explicit 
  • src/geode/inspector/inspection/criterion/colocation/unique_vertices_colocation.cpp:275:21: warning: [readability-implicit-bool-conversion]

    implicit conversion 'value_type' (aka 'char') -> 'bool'

      275 |                 if( linked_to_different_points[unique_vertex_id] )
          |                     ^
          |                                                                  != 0
  • src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:72:44: warning: [cppcoreguidelines-special-member-functions]

    class 'Impl' defines a non-default destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator

       72 |     class BRepComponentMeshesDegeneration::Impl
          |                                            ^
  • src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:76:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

       76 |         Impl( const BRep& brep )
          |         ^
          |         explicit 
  • src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:81:9: warning: [cppcoreguidelines-explicit-virtual-functions]

    annotate this function with 'override' or (rarely) 'final'

       81 |         ~Impl()
          |         ^      
          |                 override
  • src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:132:13: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'add_solid_small_elements' of similar type ('InspectionIssuesMap<index_t> &') are easily swapped by mistake

      132 |             InspectionIssuesMap< index_t >& small_edges_map,
          |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      133 |             InspectionIssuesMap< index_t >& small_polyhedra_map,
          |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:132:45: note: the first parameter in the range is 'small_edges_map'
      132 |             InspectionIssuesMap< index_t >& small_edges_map,
          |                                             ^~~~~~~~~~~~~~~
    /__w/OpenGeode-Inspector/OpenGeode-Inspector/src/geode/inspector/inspection/criterion/degeneration/brep_meshes_degeneration.cpp:133:45: note: the last parameter in the range is 'small_polyhedra_map'
      133 |             InspectionIssuesMap< index_t >& small_polyhedra_map,
          |                                             ^~~~~~~~~~~~~~~~~~~
  • src/geode/inspector/inspection/criterion/degeneration/solid_degeneration.cpp:46:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

       46 |         Impl( const SolidMesh< dimension >& mesh )
          |         ^
          |         explicit 
  • src/geode/inspector/inspection/criterion/degeneration/solid_degeneration.cpp:51:9: warning: [modernize-use-nodiscard]

    function 'is_mesh_degenerated' should be marked [[nodiscard]]

       51 |         bool is_mesh_degenerated() const final
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/degeneration/solid_degeneration.cpp:69:9: warning: [modernize-use-nodiscard]

    function 'small_height_polyhedra' should be marked [[nodiscard]]

       69 |         InspectionIssues< index_t > small_height_polyhedra(
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/degeneration/solid_degeneration.cpp:85:21: warning: [readability-implicit-bool-conversion]

    implicit conversion 'value_type' (aka 'char') -> 'bool'

       85 |                 if( is_small[element_id] )
          |                     ^
          |                                          != 0
  • src/geode/inspector/inspection/criterion/degeneration/solid_degeneration.cpp:94:9: warning: [modernize-use-nodiscard]

    function 'degenerated_polyhedra' should be marked [[nodiscard]]

       94 |         InspectionIssues< index_t > degenerated_polyhedra() const
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/degeneration/surface_degeneration.cpp:46:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

       46 |         Impl( const SurfaceMesh< dimension >& mesh )
          |         ^
          |         explicit 
  • src/geode/inspector/inspection/criterion/degeneration/surface_degeneration.cpp:51:9: warning: [modernize-use-nodiscard]

    function 'is_mesh_degenerated' should be marked [[nodiscard]]

       51 |         bool is_mesh_degenerated() const final
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/degeneration/surface_degeneration.cpp:68:9: warning: [modernize-use-nodiscard]

    function 'small_height_polygons' should be marked [[nodiscard]]

       68 |         InspectionIssues< index_t > small_height_polygons(
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/degeneration/surface_degeneration.cpp:84:21: warning: [readability-implicit-bool-conversion]

    implicit conversion 'value_type' (aka 'char') -> 'bool'

       84 |                 if( is_small[element_id] )
          |                     ^
          |                                          != 0
  • src/geode/inspector/inspection/criterion/degeneration/surface_degeneration.cpp:93:9: warning: [modernize-use-nodiscard]

    function 'degenerated_polygons' should be marked [[nodiscard]]

       93 |         InspectionIssues< index_t > degenerated_polygons() const
          |         ^
          |         [[nodiscard]] 
  • src/geode/inspector/inspection/criterion/internal/colocation_impl.cpp:122:1: warning: [modernize-concat-nested-namespaces]

    nested namespaces can be concatenated

      122 | namespace geode
          | ^~~~~~~~~~~~~~~
      123 | {
          | ~
      124 |     namespace internal
          |     ~~~~~~~~~~~~~~~~~~
          |     namespace geode::internal
  • src/geode/inspector/inspection/criterion/internal/component_meshes_degeneration.cpp:40:1: warning: [modernize-concat-nested-namespaces]

    nested namespaces can be concatenated

       40 | namespace geode
          | ^~~~~~~~~~~~~~~
       41 | {
          | ~
       42 |     namespace internal
          |     ~~~~~~~~~~~~~~~~~~
          |     namespace geode::internal
  • src/geode/inspector/inspection/criterion/internal/degeneration_impl.cpp:82:1: warning: [modernize-concat-nested-namespaces]

    nested namespaces can be concatenated

       82 | namespace geode
          | ^~~~~~~~~~~~~~~
       83 | {
          | ~
       84 |     namespace internal
          |     ~~~~~~~~~~~~~~~~~~
          |     namespace geode::internal
  • tests/inspection/test-brep-colocation-degeneration.cpp:88:6: warning: [misc-use-internal-linkage]

    function 'check_colocation_without_unique_vertices' can be made static or moved into an anonymous namespace to enforce internal linkage

       88 | void check_colocation_without_unique_vertices()
          |      ^
          | static 
  • tests/inspection/test-brep-colocation-degeneration.cpp:124:6: warning: [misc-use-internal-linkage]

    function 'check_block_small_edges' can be made static or moved into an anonymous namespace to enforce internal linkage

      124 | void check_block_small_edges()
          |      ^
          | static 
  • tests/inspection/test-brep-colocation-degeneration.cpp:131:51: warning: [cppcoreguidelines-avoid-magic-numbers]

    1e-3 is a magic number; consider replacing it with a named constant

      131 |     mesh_builder->create_point( geode::Point3D{ { 1e-3, 0, 0 } } );
          |                                                   ^

Have any feedback or feature suggestions? Share it here.

Comment on lines +63 to +66
if( Vector< dimension >{ mesh_.point( edge_vertices[0] ),
mesh_.point( edge_vertices[1] ) }
.length2()
<= threshold * threshold )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the performance improvement sufficient enough to trade off with the much less readibility of this proposal? if such strategy is deployed very enough why not add a edge_length2(..) method in meshes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not worth it here: the gain is one sqrt per edge, negligible next to the rest of the loop. Reverted to mesh_.edge_length( edge_id ) in 3d6658c. The squared comparison stays only in DegenerationImpl::edge_is_smaller_than_threshold, where it is hidden behind a named helper, so no edge_length2 needed in OpenGeode for now.

small_edge_indices.push_back( edge_index.value() );
}
}
absl::c_sort( small_edge_indices );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it better to use a c_sort on a flat_hash_set, or to use a linked_hash_set?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sort is not on the set itself but on the edge indices obtained afterwards via edge_from_vertices, so issues are reported in edge index order (same order as before this PR, and deterministic). A linked_hash_set would keep insertion order, i.e. polygon/polyhedron traversal order, which is a different order and costs more memory per entry. Since the number of small edges is usually tiny, sorting that small vector is cheap; keeping flat_hash_set + sort.

surface_id, ") colocated vertices" ) )
&& absl::StrContains( message,
absl::StrCat( "Surface ", surface_id, " (", surface_id,
") has vertices 1 0 colocated at position [0 0 0]" ) ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two sucessive space between "vertices" and "1 0"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3d6658c: the separator was added both by the literal and by the vertex list. Removed the trailing space in the three colocation messages (component meshes, mesh colocation, unique vertices) and updated the test.

BotellaA and others added 2 commits October 16, 2026 08:43
Remove the double space in colocated vertices messages and use
EdgedCurve::edge_length for readability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BotellaA
BotellaA merged commit 820f528 into next Oct 16, 2026
45 checks passed
@BotellaA
BotellaA deleted the fix/colocation-degeneration-review branch October 16, 2026 08:04
@BotellaA

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 6.12.4-rc.4 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@BotellaA

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 6.12.4 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants