Skip to content

PhysicsNeMo-Mesh: improve cache handling APIs - #1867

Open
peterdsharpe wants to merge 9 commits into
NVIDIA:mainfrom
peterdsharpe:codex/mesh-strip-caches
Open

PhysicsNeMo-Mesh: improve cache handling APIs#1867
peterdsharpe wants to merge 9 commits into
NVIDIA:mainfrom
peterdsharpe:codex/mesh-strip-caches

Conversation

@peterdsharpe

@peterdsharpe peterdsharpe commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

PhysicsNeMo Pull Request

Description

Improve the Mesh cache-handling API and route repository consumers through it so cache validity is explicit, reusable, and consistent.

  • Extend Mesh.strip_caches(keep=...) and DomainMesh.strip_caches(keep=...) to retain selected cache entries or complete cache categories.
  • Add Mesh.with_points(...) for coordinate changes that preserve point indexing and connectivity. It invalidates geometry-dependent caches while retaining topology caches by default.
  • Add Mesh.with_cells(...) for connectivity changes that preserve cell indexing and simplex type. It clears cell, point, and topology caches by default, with an explicit expert keep= override.
  • Keep derived Mesh data and cache containers independent, while safely sharing tensor leaves under the documented replacement semantics.
  • Route geometry-preserving transformations, projections, procedural primitives, face reorientation, and winding changes through the appropriate cache-aware API.
  • Route data-only Mesh operations, datapipes, calculus helpers, visualization, GLOBE, and unified aero consumers through with_data so valid caches survive.
  • Continue using specialized operations or explicit construction when point/cell identity, cardinality, or simplex type changes and associated data must be remapped.

The private structural helper is named _new_with_structure to avoid TensorClass generated replace and dataclass __replace__ APIs. Those generic replacements are documented as unsafe for changing points or cells because they do not invalidate caches.

The existing no-argument strip_caches() behavior remains unchanged. Part of #1851.

Validation

pytest -q -s --disable-warnings test/mesh
# 2979 passed, 18 skipped

# Post-with_cells affected Mesh regression suite
# 701 passed

# Earlier combined affected-unit and integration suite
# 609 passed, 17 skipped

pre-commit run --files <changed files>
# all hooks passed

Additional checks cover full-graph torch.compile use of with_cells and DrivAer ground-boundary winding/normals. The GLOBE model file retains its existing symbolic-shape F821 suppressions; all newly changed lines pass Ruff.

Checklist

Dependencies

None.

Review Process

Draft; no reviewers requested yet.

@copy-pr-bot

copy-pr-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@peterdsharpe peterdsharpe changed the title PhysicsNeMo-Mesh: support selective cache stripping PhysicsNeMo-Mesh: improve cache handling APIs Jul 22, 2026
@peterdsharpe
peterdsharpe marked this pull request as ready for review July 24, 2026 22:24
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces Mesh.with_points, Mesh.with_cells, and a selective strip_caches(keep=...) API to make cache invalidation explicit and reusable, then routes ~30 call sites across the codebase through the appropriate method rather than constructing raw Mesh(...) objects.

  • _cache_with_only / _new_with_structure form the shared plumbing: independent TensorDict containers with shared tensor leaves and correct per-category scaffolding.
  • Genuine bug fixes land alongside the API work: lumpy_sphere replaces an in-place mesh.points = ... mutation with with_points, and GLOBE fixes a cache-aliasing issue where _cache=mesh._cache (direct reference) was passed to constructed meshes.
  • Data-only operations (datapipes, calculus helpers, visualization) now route through with_data, so valid geometry caches survive those transforms.

Important Files Changed

Filename Overview
physicsnemo/mesh/mesh.py Core change: adds with_points, with_cells, _new_with_structure, _cache_with_only, and extends strip_caches with keep=. The _cache_with_only method has a silent-failure footgun when a bare string or bare nested-key tuple is passed as keep.
physicsnemo/mesh/transformations/geometric.py Routes transform and translate through with_points; correctly retains topology and geometry-invariant caches. Post-construction in-place cache mutation for centroids is functional but a minor style concern.
physicsnemo/mesh/domain_mesh.py Extends strip_caches to accept keep= and delegates to each component mesh; straightforward and correct.
physicsnemo/mesh/repair/orientation.py Routes winding repair through with_cells(...).with_data(...), correctly clearing all caches and preserving cloned data tensors.
physicsnemo/mesh/projections/_embed.py Simplification to mesh.with_points(new_points); topology is retained, geometry caches invalidated — semantically correct for a dimension-expanding operation.
physicsnemo/mesh/projections/_project.py Refactored to with_points then conditional with_data; correctly preserves topology cache and only calls with_data when data transforms were requested.
physicsnemo/experimental/models/globe/model.py Replaces _cache=mesh._cache (direct alias) with with_data (independent copy); fixes a latent cache-aliasing bug where populating one mesh's cache would unexpectedly affect the source.
physicsnemo/datapipes/transforms/mesh/transforms.py All data-only transforms (DropMeshFields, RenameMeshFields, SetGlobalField, NormalizeMeshFields, ComputeSurfaceNormals, MeshToDomainMesh) routed through with_data; caches now survive data-only transforms correctly.
physicsnemo/mesh/primitives/procedural/lumpy_sphere.py Fixes an in-place mesh.points = ... mutation (which silently invalidated caches) by replacing it with the correct with_points API.
test/mesh/utilities/test_cache.py New TestStripCaches, TestWithPoints, TestWithCells suites covering selective keep, independence guarantees, validation rejections, and correct cache semantics.
test/datapipes/transforms/test_mesh_cache_preservation.py New test file verifying that all data-only datapipe transforms preserve caches and produce independent containers.

Reviews (1): Last reviewed commit: "Add cache-aware Mesh cell replacement" | Re-trigger Greptile

Comment thread physicsnemo/mesh/mesh.py
Comment on lines +1617 to +1630
cache = self._cache.select(*keep, strict=False).copy()
device = self.points.device
for category, batch_size in (
("cell", torch.Size([self.n_cells])),
("point", torch.Size([self.n_points])),
("topology", torch.Size([])),
):
if category not in cache:
cache[category] = TensorDict(
{},
batch_size=batch_size,
device=device,
)
return cache

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 String or bare-tuple keep silently produces wrong results

_cache_with_only unpacks keep directly with *keep, so two easy mistakes produce silent wrong behavior rather than an error:

  1. A bare string — keep="topology" — iterates as characters, calling select("t", "o", "p", …, strict=False). None of those keys exist, so the result is an empty cache (all caches stripped) instead of the topology cache being retained.

  2. A bare nested-key tuple — keep=("cell", "areas") — unpacks as two top-level selects: select("cell", "areas", …), retaining the entire cell sub-TensorDict plus a silently-ignored missing "areas" top-level key, rather than retaining only ("cell", "areas").

Both cases are accepted without error because str and tuple both satisfy Sequence[str | tuple]. The upstream public methods strip_caches, with_points, and with_cells all expose the same parameter, so any caller who passes keep="topology" or keep=("cell", "areas") instead of keep=["topology"] or keep=[("cell", "areas")] gets silently incorrect cache state. A short guard at the top of _cache_with_only (e.g. if isinstance(keep, (str, bytes)): raise TypeError(...)) would catch the string case.

Comment thread physicsnemo/mesh/mesh.py
Comment on lines +1611 to +1617
"""Return an independent cache container containing only ``keep``.

Tensor leaves are intentionally shared, but every retained nested
``TensorDict`` container is shallow-copied so populating a cache on a
derived mesh cannot mutate the source mesh's cache structure.
"""
cache = self._cache.select(*keep, strict=False).copy()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Adding an early guard against a bare string prevents the most common misuse of the keep parameter from silently stripping all caches instead of retaining the named category.

Suggested change
"""Return an independent cache container containing only ``keep``.
Tensor leaves are intentionally shared, but every retained nested
``TensorDict`` container is shallow-copied so populating a cache on a
derived mesh cannot mutate the source mesh's cache structure.
"""
cache = self._cache.select(*keep, strict=False).copy()
"""Return an independent cache container containing only ``keep``.
Tensor leaves are intentionally shared, but every retained nested
``TensorDict`` container is shallow-copied so populating a cache on a
derived mesh cannot mutate the source mesh's cache structure.
"""
if isinstance(keep, (str, bytes)):
raise TypeError(
f"`keep` must be a sequence of strings or nested-key tuples, "
f"not a bare string {keep!r}. "
f"Did you mean keep=[{keep!r}]?"
)
cache = self._cache.select(*keep, strict=False).copy()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 639 to 645
if (v := mesh._cache.get(("cell", "centroids"), None)) is not None:
new_cache["cell", "centroids"] = v + offset
translated_mesh._cache["cell", "centroids"] = v + offset

from physicsnemo.mesh.mesh import Mesh

return Mesh(
points=new_points,
cells=mesh.cells,
point_data=mesh.point_data,
cell_data=mesh.cell_data,
global_data=mesh.global_data,
_cache=new_cache,
)
return translated_mesh


def rotate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 In-place cache mutation after with_points bypasses immutability convention

translated_mesh._cache["cell", "centroids"] = v + offset directly mutates the _cache TensorDict container of an already-constructed Mesh. While safe here (the container is independent after _new_with_structure), the cache state visible inside translated_mesh differs between the moment with_points returns and the moment translate returns. Any intermediate inspection would see stale centroid values. A cleaner pattern would be to build the corrected cache before calling _new_with_structure, keeping the public API the only write path to the cache.

Comment thread physicsnemo/mesh/mesh.py
Comment on lines 3930 to 3975
)
return cleaned

def strip_caches(self) -> "Mesh":
r"""Return a new mesh with all cached values removed.
def strip_caches(
self,
keep: Sequence[str | tuple[str, ...]] = (),
) -> "Mesh":
r"""Return a new mesh with cached values removed.

Cached values (stored under the ``_cache`` key in data TensorDicts) are
computed lazily for expensive operations like normals, areas, and curvature.
This method creates a new mesh without these cached values, which is useful
for:
Cached values stored in the separate :attr:`_cache` field are computed
lazily for expensive operations like normals, areas, and curvature. This
method creates a new mesh without these cached values, except for keys
explicitly listed in ``keep``. This is useful for:

- Accurate benchmarking (prevents false performance benefits from caching)
- Reducing memory usage
- Forcing recomputation of cached values

Parameters
----------
keep : sequence of str or tuple[str, ...], optional
Cache keys to retain. Strings select a complete top-level cache such as
``"topology"``; tuples select a nested entry such as
``("cell", "areas")``. Missing keys are ignored.

Returns
-------
Mesh
A new mesh with the same geometry and data, but without cached values.
A new mesh with the same geometry and data, retaining only the requested
cached values.

Examples
--------
>>> from physicsnemo.mesh.primitives.surfaces import sphere_icosahedral
>>> mesh = sphere_icosahedral.load(subdivisions=2)
>>> _ = mesh.cell_normals # Triggers caching
>>> mesh_clean = mesh.strip_caches() # Remove cached normals
>>> mesh_with_areas = mesh.strip_caches(keep=[("cell", "areas")])
"""
return Mesh(
return self._new_with_structure(
points=self.points,
cells=self.cells,
point_data=self.point_data,
cell_data=self.cell_data,
global_data=self.global_data,
keep=keep,
)


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Behavioral change in no-arg strip_caches() not accurately described

The PR description states "the existing no-argument strip_caches() behavior remains unchanged," but there is a subtle difference. The old implementation passed point_data=self.point_data, cell_data=self.cell_data, global_data=self.global_data directly to the Mesh constructor, which stored those TensorDicts as-is (shared containers). The new path through _new_with_structure calls .copy() on each, producing independent containers. Code that relied on the aliasing would silently behave differently. The change is an improvement in correctness, but the PR description should note it rather than claiming no-arg parity.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant