Skip to content

🐛[BUG]: Several physicsnemo.mesh primitives load with reversed cell orientation #2135

Closed

Description

@peterdsharpe

Version

2.3.0a0 (commit 7e646b4)

On which installation method(s) does this occur?

Source

Describe the issue

Several primitives in physicsnemo.mesh.primitives wind their cells the opposite way from the rest of the library.

The convention elsewhere is clear:

  • A cell of a Mesh[n, n] has a positive signed volume, det(v1 - v0, ..., vn - v0) > 0. validate(check_inverted_cells=True) flags every other cell as inverted.
  • sphere_volume, cylinder_volume and lumpy_ball call orient_positive to enforce this.
  • A closed Mesh[2, 3] has outward normals, as in sphere_icosahedral, sphere_uv, cube_surface, cylinder and lumpy_sphere.
  • The cube_surface docstring states this convention: "counter-clockwise winding when viewed from outside".
  • A triangle in the xy plane wound counterclockwise has normal +z, as basic.single_triangle_3d shows.

I loaded every primitive with its default arguments and measured the orientation. These disagree with the convention:

Primitive Measurement Cause
planar.unit_square 8 of 8 cells inverted planar/unit_square.py:61-62
planar.rectangle 72 of 72 cells inverted planar/rectangle.py:72-73
planar.structured_grid 200 of 200 cells inverted planar/structured_grid.py:87-88
planar.l_shape 150 of 150 cells inverted planar/l_shape.py:88-89, 113-114
procedural.perturbed_grid 173 of 200 inverted, 27 positive Inherits structured_grid. The default perturbation_scale=0.05 folds 27 cells.
volumes.cube_volume 375 of 750 inverted (24 of 48 at subdivisions=2) volumes/cube_volume.py:90-95. Patterns [0, 1, 5, 7], [0, 2, 3, 7] and [0, 4, 6, 7] are odd permutations.
surfaces.torus Normals point into the tube. Mean curvature is -1.588, signed volume -1.751. surfaces/torus.py:86-87
surfaces.hemisphere All 840 normals point toward the center surfaces/hemisphere.py:81-82
surfaces.disk, surfaces.plane Normals are -z for a mesh in the xy plane surfaces/disk.py:78-85, 102-103, surfaces/plane.py:107-108
pyvista_datasets.tetbeam 200 of 200 cells inverted The PyVista data is inverted too. The wrapper does not call orient_positive.

The torus signed volume should be +2π²Rr² = +1.777 for the defaults.

These primitives agree with the convention:

  • basic: all
  • planar: annulus_2d, circle_2d, equilateral_triangle, regular_polygon
  • surfaces: cone, cube_surface, cylinder, cylinder_open, icosahedron_surface, octahedron_surface, sphere_icosahedral, sphere_uv, tetrahedron_surface
  • volumes: cylinder_volume, tetrahedron_volume
  • procedural: lumpy_ball, lumpy_sphere
  • pyvista_datasets: hexbeam, ant, cow

Two related observations:

  • curves.circle_2d and curves.ellipse_2d run counterclockwise. The 2D edge normal rotates the tangent counterclockwise, so their normals point inward.
  • volumes.sphere_volume has 4 of 1164 cells with negative volume. Each has |V| < 1.5e-12, so these are slivers from delaunay_3d, not a winding error. PR Mesh: fix degenerate and overlapping cells in primitive generators #2129 replaces that construction.

Expected: every primitive follows the convention above. Actual: the primitives in the table load reversed. Tests and examples built on them see inverted cells, inward normals and negative curvature.

Minimum reproducible example

python - <<'EOF'
from physicsnemo.mesh.primitives import planar, surfaces, volumes

sq = planar.unit_square.load(subdivisions=1)
print("unit_square inverted:", sq.validate(check_inverted_cells=True)["n_inverted_cells"], "of", sq.n_cells)

cube = volumes.cube_volume.load(subdivisions=2)
print("cube_volume inverted:", cube.validate(check_inverted_cells=True)["n_inverted_cells"], "of", cube.n_cells)

for name in ["sphere_icosahedral", "torus"]:
    m = getattr(surfaces, name).load()
    print(name, "mean curvature:", round(m.mean_curvature_vertices.mean().item(), 3))
EOF

Relevant log output

unit_square inverted: 8 of 8
cube_volume inverted: 24 of 48
sphere_icosahedral mean curvature: 1.0
torus mean curvature: -1.588

Environment details

+ Environment location: Bare-metal, CPU only (CUDA_VISIBLE_DEVICES unset)
+ Python 3.13.8, torch 2.13.0+cu130, tensordict 0.15, warp 1.17.0

Activity

  1. dhvani7014 commented on Oct 19, 2026

    @dhvani7014

    Hi @peterdsharpe, I'd like to work on this unless you're already planning to as part of the mesh audit series. I'd fix the winding in each listed generator (and call orient_positive in the tetbeam wrapper), with a per-primitive test that checks validate(check_inverted_cells=True) and outward normals. I'll rebase around #2129 for hemisphere.py. Is that okay?

  2. peterdsharpe commented on Oct 20, 2026

    @peterdsharpe
    CollaboratorAuthor

    Hi @dhvani7014 - thanks for asking! At the time, we have plans to fix this ourselves so PRs are not needed on this at the moment.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ? - Needs TriageNeed team to review and classifybugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions