Skip to content

Cache cell-facet topological data on meshes - #5298

Open
achanbour wants to merge 8 commits into
mainfrom
achanbour/precompute-mesh-topo-data
Open

Cache cell-facet topological data on meshes#5298
achanbour wants to merge 8 commits into
mainfrom
achanbour/precompute-mesh-topo-data

Conversation

@achanbour

Copy link
Copy Markdown
Contributor

Compute and cache properties derived from cell-facet adjacency data on meshes:

  • cell_facet_neighbours: maps each cell to its neighbouring cells across each of its local facets
  • cell_facet_coord_transforms: stores affine reference-coordinate transforms between neighbouring cells across interior facets
  • cell_facet_exterior_mask: marks cell-local facets lying on the exterior domain boundary (distinguishes exterior facets from rank partition boundaries)

Comment thread firedrake/mesh.py
plex = self.topology_dm
cStart, cEnd = plex.getHeightStratum(0) # range of DMPlex point numbers representing cells

for c_plex_point in range(cStart, cEnd):

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.

We want to avoid Python loops over the mesh. This should go in some Cython.

Comment thread firedrake/mesh.py
# NOTE: A fresh Dat has its halo entries marked as invalid -> next with_halos access can trigger a collective exchange.
# An exchange can overrite a rank's data with the owning rank's local data which is wrong.
# We avoid that by marking halos as valid.
cell_facet_neighbours.halo_valid = True

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.

Does this mean that the Dat is logically over COMM_SELF? If so then that's what we should do. That may be a pyop3 thing (definitely easy there, not sure with PyOP2)

Comment thread firedrake/mesh.py
V = functionspaceimpl.WithGeometry(coordinates.function_space(), self)
self._coordinates_function = function.Function(V, val=coordinates)

self._topology_version = 0

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.

Rogue entry from another PR?

Comment thread firedrake/mesh.py
cid = self._cell_numbering.getOffset(c_plex)

if cid < 0 or cid >= self.cell_set.total_size:
continue

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.

when does this happen?

Comment thread firedrake/mesh.py
ext_plex_points = frozenset()

cstart, cend = plex.getHeightStratum(0)
for c_plex in range(cstart, cend):

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.

Same comment about Cython

Comment thread firedrake/mesh.py
topology = FIAT.ufc_cell(self.ufl_cell()).get_topology()
return sum(len(topology[d]) for d in range(facet_dim))

def _cell_facet_point(self, cell, local_facet):

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.

You are calling this in a tight loop so probably best to not dispatch to a Python call

Comment thread firedrake/mesh.py

for lf in range(num_facets):
f_point = self._cell_facet_point(cid, lf)
mask[cid, lf] = f_point in ext_plex_points

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.

I wonder if this could all be numpified, instead of cythonified. Use np.intersect1d between a slice of cell_closure and exterior_facets.getStratumIS(1).

Comment thread firedrake/mesh.py

# inverse coord. map
# o0 -> canonical orientation
Q0_inv = np.linalg.inv(Q0)

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.

Is this not really slow? I think you're calling this for every facet.

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.

you can batch these things as done in https://github.com/firedrakeproject/firedrake/pull/5034/changes#diff-e45e704e2c1f2f7f0bd632f90a71ce568a47c880cdf097394d8bdf4ccb0332b6R104

However, the geometric computation done there is avoidable and there is a purely topological way of achieving the goal that we inteded there (permuting an array of coordinates). Perhaps you can achieve your goal (which is not clear from the PR description) via a purely topological approach, in a coordinate-free way.

Comment thread firedrake/mesh.py

return (A, b, A_inv)

def _get_facet_orientation_coord_maps(self):

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.

This only uses self in one location, and only to get the UFL cell. This seems like it doesn't need to be a method of the mesh.
I wonder if we should have a firedrake/facets.py file to collect these routines. It could then be folded into the mesh subpackage when we get around to doing that.

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.

This should definitely live in FIAT

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.

Specifically FIAT/reference_element.py

Comment thread firedrake/mesh.py
(A[lf], b[lf]) defines the affine embedding of facet reference coordinates into the cell reference frame,
and A_inv[lf] defines the pseudoinverse for mapping cell coordinates on the facet back to facet-local coordinates.
"""
import FIAT

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.

top-level import

Comment thread firedrake/mesh.py
and A_inv[lf] defines the pseudoinverse for mapping cell coordinates on the facet back to facet-local coordinates.
"""
import FIAT
ref_cell = FIAT.ufc_cell(self.ufl_cell())

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.

I think these functions live in FIAT, these are very fundamental operations at the reference elemenet level

Comment thread firedrake/mesh.py
return A, b, A_inv

for lf in range(num_facets):
phi = ref_cell.get_entity_transform(facet_dim, lf)

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.

Is this reverse-engineering get_entity_transform? I.e. are you assuming an affine mapping for the entity transform and rederiving by sampling it at the vertices?

@pbrubeck pbrubeck Jul 30, 2026

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.

Tensor product cells will not give you affine mappings. This has to be carefully thought. What is the goal/motivation for storing the facet data on the mesh?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I store the facet data on the mesh as my particle trajectory algorithm solves for mesh entity collisions (identifies which facet was crossed by each particle and its crossing position on that facet). I would then like to be able to tell, based on the crossed facet ID: 1) which is the neighbouring cell across that facet and 2) transform the coordinates of the point on that facet to the local (reference) coordinates of the "next cell" across that facet.

@pbrubeck pbrubeck Jul 30, 2026

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.

I see. Often it helps to follow the existing patterns established in Firedrake. In this case the main pattern to consider is code generation. When we generate code to assemble a form, we don't precompute/store in the mesh the geometric information (Jacobians/affine mappings). We compute everything from the mesh coordinates withtin the generated C kernels (avoiding python loops over the cells).

It seems to me that the right thing is to generate a facet kernel. A facet kernel iterates over pairs of cells sharing a facet, this solves 1). You can then access the coordinates of both cells and generate code from ufl.FacetJacobian(mesh), this solves 2).

Generation of bespoke code is quite painful, but we are here to help you. I think by sticking to the established patterns one can get a very general particle code that works on high-order meshes of any cell type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I completely understand your point. As this idea of writing kernels to generate code is pretty new to me, could you point out where I could look for these code patterns for example in existing kernels that achieve some sort of similar operation (e.g., the form assembly you mentioned)? It will help me get started on this task!

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.

3 participants