Skip to content

Computing barycentric coordinates in Tensor Product Cells#246

Draft
achanbour wants to merge 9 commits into
mainfrom
achanbour/bary-coords-fiat
Draft

Computing barycentric coordinates in Tensor Product Cells#246
achanbour wants to merge 9 commits into
mainfrom
achanbour/bary-coords-fiat

Conversation

@achanbour
Copy link
Copy Markdown

This is the first part of PR #245 concerning changes to FIAT only. In particular, I have extended FIAT to enable the computation of barycentric coordinates on tensor product cells.

The main distinction lies in how barycentric coordinates are represented in simplicies vs tensor product cells. For simplicies, there is one barycentric coordinate per vertex so the default ordering of these coordinates follows the vertex ordering. For tensor product cells, there is one barycentric coordinate per facet so the natural ordering is that of facets.

First, to enable facet ordering in simplicies, I have added a facet ordering permutation in SimplicialComplex.compute_barycentric_coordinates. Then, since barycentric coordinates in tensor product cells are computed recursively on each factor, having facet ordering on the simplicies that form each factor ensure that we obtain facet ordering on the tensor-product cell as a whole.

@pbrubeck
Copy link
Copy Markdown

pbrubeck commented May 8, 2026

Looks like you are including commits form #240. Are they needed?

@achanbour achanbour force-pushed the achanbour/bary-coords-fiat branch from 448c540 to d71fa37 Compare May 12, 2026 17:38
achanbour added 9 commits May 12, 2026 18:45
…cells

minor fixes

modified compute_barycentric_coords to consistently handle input points of different sizes

extended the tensor product barycentric coordinates computation function to handle nested tensor-product cells + added tests

removed line enforcing points to be numpy arrays to ensure code generation works properly for symbolic points

added conversion to numpy array only in axis_barycentric_coords

generalised numpy operations in compute_barycentric_coordinates methods to work on different array shapes

changed compute_barycentric_coordinates method to work on input GEM expressions representing point sets

extended gem matmul to convert numpy arrays to Literals and deal separately with singleton contractions

changes made to gem and FIAT to make barycentric coordinate computations work on GEM tensor expressions

extended gem's slicing syntax and simplified the code in compute_axis_barycentric_coordinates in tp cells

tidied up and added comments

recent changes post merging

compute barycentric coords symbolically in simplicies and hypercubes (to be tested)

modified unit tests for computing bary coords on points passed as numpy arrays

latest changes + gem tests

implemented handler in gem.evaluate for FlexiblyIndexed nodes

added a new ListIndex type to GEM for indexing GEM tensors using an index array

final changes

fixed formatting
changed ListIndex to be a subclass of Index
… sub-entities of simplicies + added notes + modified test checking facet ordering of bary coords on tp cells

modified the index substitution logic to correctly handle ListIndex

changed index substitution optim
isation
@achanbour achanbour force-pushed the achanbour/bary-coords-fiat branch from d71fa37 to a3d51a1 Compare May 12, 2026 17:49
@achanbour
Copy link
Copy Markdown
Author

Looks like you are including commits form #240. Are they needed?

No. I had accidentally merged some irrelevant changes into the old branch from which this PR was branched off. I've now reviewed the changes and removed unnecessary commits.

Comment thread FIAT/reference_element.py
Comment on lines +629 to 631
subcomplex = global_indices

if entity_dim != sd:
Copy link
Copy Markdown

@pbrubeck pbrubeck May 13, 2026

Choose a reason for hiding this comment

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

Suggested change
subcomplex = global_indices
if entity_dim != sd:
if entity_dim == sd:
subcomplex = global_indices
else:

Comment thread FIAT/reference_element.py
cell_id = self.connectivity[(entity_dim, sd)][entity_id][0]

# restrict indices to the chosen sub-cell
indices = [i for i, v in enumerate(top[sd][cell_id]) if v in subcomplex]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
indices = [i for i, v in enumerate(top[sd][cell_id]) if v in subcomplex]
indices = [i for i, v in enumerate(top[sd][cell_id]) if v in global_indices]

Comment thread FIAT/reference_element.py
facet_ids = self.connectivity[(entity_dim, entity_dim-1)][entity_id]
facets = [top[entity_dim-1][f] for f in facet_ids] # facet as a tuple of vertex indices that form it

perm = [facets.index(tuple(sorted(set(global_indices) - {v}))) for v in global_indices]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we need to permute indices with the inverse of perm. facets.index returns a facet index, but indices refers to vertices.

Comment thread FIAT/reference_element.py
Comment on lines +650 to +651
cell_verts = self.get_vertices_of_subcomplex(subcomplex) # get vertex coordinates of the sub-cell
ref_verts = numpy.eye(sd + 1) # get vertex coordinates of the standard reference cell
Copy link
Copy Markdown

@pbrubeck pbrubeck May 13, 2026

Choose a reason for hiding this comment

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

To keep a cleaner diff, it is better to not add extra comments on unmodified lines

Suggested change
cell_verts = self.get_vertices_of_subcomplex(subcomplex) # get vertex coordinates of the sub-cell
ref_verts = numpy.eye(sd + 1) # get vertex coordinates of the standard reference cell
# Get vertex coordinates of the physical and reference sub-cells
cell_verts = self.get_vertices_of_subcomplex(subcomplex)
ref_verts = numpy.eye(sd + 1)

Comment thread FIAT/reference_element.py
Comment on lines +1455 to +1465
result = []
for factor, s in zip(self.cells, point_slices):
factor_sd = factor.get_spatial_dimension()
if factor_sd == 1:
factor_bary_coords = factor.compute_barycentric_coordinates(points[..., s], entity, rescale, facet_ordering=True)
else:
# For higher dimensional simplicies: vertex ordering is already guaranteed.
# NOTE: But what if we want to compute barycentric coordinates on a sub-entity and still want facet ordering?
# Then we need to pass facet_ordering=True even when factor_sd > 1
factor_bary_coords = factor.compute_barycentric_coordinates(points[..., s], entity, rescale)
result.append(factor_bary_coords)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
result = []
for factor, s in zip(self.cells, point_slices):
factor_sd = factor.get_spatial_dimension()
if factor_sd == 1:
factor_bary_coords = factor.compute_barycentric_coordinates(points[..., s], entity, rescale, facet_ordering=True)
else:
# For higher dimensional simplicies: vertex ordering is already guaranteed.
# NOTE: But what if we want to compute barycentric coordinates on a sub-entity and still want facet ordering?
# Then we need to pass facet_ordering=True even when factor_sd > 1
factor_bary_coords = factor.compute_barycentric_coordinates(points[..., s], entity, rescale)
result.append(factor_bary_coords)
result = [factor.compute_barycentric_coordinates(points[..., s], entity, rescale, facet_ordering=True)
for factor, s in zip(self.cells, point_slices)]

Comment thread FIAT/reference_element.py
facets = [top[entity_dim-1][f] for f in facet_ids] # facet as a tuple of vertex indices that form it

perm = [facets.index(tuple(sorted(set(global_indices) - {v}))) for v in global_indices]
indices = [indices[p] for p in perm]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
indices = [indices[p] for p in perm]
indices = [indices[p] for p in numpy.argsort(perm)]

Comment thread FIAT/reference_element.py
return unflattening_map


def compute_facet_permutation(unflattening_map, product):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should get rid of this function if not used anymore

Comment thread FIAT/reference_element.py
def __le__(self, other):
return self.product <= other

def compute_barycentric_coordinates(self, points, entity=None, rescale=False):
Copy link
Copy Markdown

@pbrubeck pbrubeck May 13, 2026

Choose a reason for hiding this comment

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

Suggested change
def compute_barycentric_coordinates(self, points, entity=None, rescale=False):
def compute_barycentric_coordinates(self, points, entity=None, rescale=False, facet_ordering=True):

Comment thread FIAT/reference_element.py

Returns
-------
List of numpy.ndarray or GEM.ComponentTensor
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not quite true, so far we return a single numpy.ndarray

Comment thread FIAT/reference_element.py
# get a subcell containing the entity and the restriction indices of the entity
indices = slice(None)
subcomplex = top[entity_dim][entity_id]
# indices = slice(None)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
# indices = slice(None)
# get a subcell containing the entity and the restriction indices of the entity

(tetrahedron, [0.25, 0.25, 0.25]),
(quadrilateral, [0.25, 0.5]),
(hexahedron, [0.25, 0.5, 0.25]),])
def test_bary_coords_gem(cell, point):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test could pass at the moment, we just need to cast back and forth between gem and numpy

(hexahedron, [0.3, 0.4, 1.0]),])
def test_hypercube_bary_coords_are_in_facet_order(cell, point, epsilon=1e-12):
"""Test that the barycentric coordinates computed on hypercubes are listed in facet order."""
point = np.asarray(point)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we should support a single or multiple points as tuple or numpy.array

coords = gem.Variable('X', (sd,))
bindings = {coords: point}

bary_gem = cell.compute_barycentric_coordinates(coords)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
bary_gem = cell.compute_barycentric_coordinates(coords)
np_coords = tuple(coords[i] for i in range(sd))
bary_gem = gem.as_gem(cell.compute_barycentric_coordinates(np_coords))

Comment thread FIAT/reference_element.py
factor_bary_coords = factor.compute_barycentric_coordinates(points[..., s], entity, rescale)
result.append(factor_bary_coords)

flat_result = numpy.concatenate(result)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
flat_result = numpy.concatenate(result)
flat_result = numpy.concatenate(result, axis=-1)

Comment thread FIAT/reference_element.py
Comment on lines +1602 to +1604
if isinstance(points, numpy.ndarray) and len(points) == 0:
return points

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
if isinstance(points, numpy.ndarray) and len(points) == 0:
return points

Comment thread FIAT/reference_element.py
if isinstance(points, numpy.ndarray) and len(points) == 0:
return points

tp_bary_coords = self.product.compute_factor_barycentric_coordinates(points, entity, rescale)
Copy link
Copy Markdown

@pbrubeck pbrubeck May 13, 2026

Choose a reason for hiding this comment

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

Suggested change
tp_bary_coords = self.product.compute_factor_barycentric_coordinates(points, entity, rescale)
return self.product.compute_factor_barycentric_coordinates(points, entity, rescale)

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.

2 participants