Generalising compute barycentric coordinates#245
Conversation
…ts of different sizes
…ion to handle nested tensor-product cells + added tests
…ation works properly for symbolic points
…ds to work on different array shapes
…xpressions representing point sets
…rately with singleton contractions
…ons work on GEM tensor expressions
…_barycentric_coordinates in tp cells
| if not isinstance(key, tuple): | ||
| key = (key,) | ||
|
|
||
| # Expand ellipsis -> fill in remaining dimensions with slice(None) |
There was a problem hiding this comment.
I don't quite understand this behaviour. What if I have something like tensor[::2, ..., 3]? I wonder if we should only allow ... if there are no other indices provided.
There was a problem hiding this comment.
Currently, there's no support for mixed indexing involving slices and arrays so something like tensor[..., [1,2,3]] isn't supported.
Lines 110 to 114 in a5b8ab0
The reason for that is that indexing by ... (or by slice) is handled by view() which expresses a reshape while indexing with [1,2,3] is handled via ListIndex which expresses a gather-like operation. I'm not sure if there's a Node in GEM that allows to compose these operations.
There was a problem hiding this comment.
It would be nice to unify the code paths for __getitem__ and gem.view. I would definitely expect something like
tensor_2d[1::3, [2, 3, 4]]
to work
| if has_slice and has_array: | ||
| raise NotImplementedError("Mixed slice and array indexing is not supported.") | ||
|
|
||
| # Slice indexing -> delegate to view() |
There was a problem hiding this comment.
I wonder if the implementation of view should live in here
There was a problem hiding this comment.
then everything is together. We could even deprecate gem.view
There was a problem hiding this comment.
Why not have everything in gem.view? In this way we can keep the dunder methods in gem short and concise.
There was a problem hiding this comment.
I don't mind that. I just want to unify the code paths somewhat.
| if len(points) == 0: | ||
|
|
||
| if isinstance(points, numpy.ndarray) and len(points) == 0: |
There was a problem hiding this comment.
We should not start assuming that points may only be a numpy.ndarray. In FIAT most of the time points are treated as tuples, since those are immutable. We try to use numpy for our computations as much as possible, while relying on the casting done by most numpy functions.
pbrubeck
left a comment
There was a problem hiding this comment.
I suggest splitting this into two separate PRs. First to get the numerics right for barycentric coordinates on tensor product cells, and the second one getting the GEM side of things right.
| assert isinstance(index, IndexBase) | ||
| if isinstance(index, Index): | ||
| index.set_extent(extent) | ||
| elif isinstance(index, ListIndex): |
There was a problem hiding this comment.
There might be an issue here. Indexed inherits from Scalar. I think we want Indexed(Node, ListIndex) to evaluate to a tensor, so we might need a new class, maybe something similar to ComponentTensor.
There was a problem hiding this comment.
If ListIndex is a subclass of Index (my most recent change ) then the extent of this free index would be len(index.index_array) where index_array is the integer index array stored in ListIndex.
Lines 726 to 736 in 6bbddc5
The evaluation into a tensor happens in __getitem__:
Lines 125 to 131 in 6bbddc5
Would this be correct?
There was a problem hiding this comment.
I think I understand it the logic better now, ListIndex is like a normal Index, but taking values from a list, so I think it is fine to use it with Indexed.
There was a problem hiding this comment.
Speaking to @connorjward, he advised to create a Index inside ListIndex to range over the values from the index_array list rather than having it implicit as here so essentially:
-
Option 1 (current):
tensor[list_index]istensor[list_index.index_array[list_index]]wherelist_indexis a free index ranging over0,1..., len(index_array) -
Option 2:
tensor[list_index]istensor[list_index.index_array[list_index.free_index]]
In option 2, the inheritance between ListIndex and Index as its parent class may cause issues so a solution could bee to have a separate parent class for both.
There was a problem hiding this comment.
Both options I think are equivalent but option 2 is clearer.
| return Result(val[idx], all_fids) | ||
|
|
||
|
|
||
| @_evaluate.register(gem.FlexiblyIndexed) |
There was a problem hiding this comment.
I've opened #175, which is very similar but didn't merge it because I didn't add tests
| return unflattening_map | ||
|
|
||
|
|
||
| def compute_facet_permutation(unflattening_map, product): |
There was a problem hiding this comment.
What are the possible permutations for Prisms and Cubes? Don't they end up in a trivial permutation?
There was a problem hiding this comment.
What do you mean by trivial permutation?
There was a problem hiding this comment.
I think he means an identity permutation. I would recommend that you add some sort of check to catch those somewhere.
There was a problem hiding this comment.
It's identity for simplicies. As written compute_facet_permutation only works for Hypercubes since we only have unflattening_map there. For general TP cells like prisms, my assumptions is that something similar to an unflattening map should be implemented first before generalising compute_facet_permutation.
There was a problem hiding this comment.
Thanks for adding the comment with the quad example. I see the issue now. The problem is that the UFCInterval does not order the facets in the same way UFCTriangle and UFCTetrahedron do. UFCInterval has the vertices as the facets, and vertices are always ordered in vertex order, but facets are ordered by the vertex they exclude, so you get an inconsistency for 1D when the facets are also the vertices.
I think the implementation of compute_barycentric_coordinates on UFCInterval should apply the permutation [1, 0]. Then this gives us the right ordering for UFCQuadrilateral.
There was a problem hiding this comment.
The expected ordering for compute_barycentric_coordinates on simplices is not facet-based, but always vertex-based. If you would like a facet-based ordering, then only the interval should be permuted and tensor products would get the facet-based ordering for free.
There was a problem hiding this comment.
Thanks Pablo. It is indeed very confusing but with your help I am able to see better. I'll try to re-iterate the reasoning here for the sake of clarity.
The expected ordering for
compute_barycentric_coordinateson simplices is not facet-based, but always vertex-based.
- This is consistent with the fact that
$\lambda_i(P_j) = \delta_{ij}$ i.e., the i-th barycentric coordinate is 1 on vertex i and 0 on all other vertices. Hence it is 0 on the facet excluding vertex i. - Since in
UFCTriangleandUFCTetrahedronorder facets by the vertex they exclude we get that facet i excludes vertex i, i.e.,$\lambda_i = 0$ on facet i. - Since in
UFCIntervalthe vertices are the facets, we have that facet 1 contains vertex 1 and facet 2 contains vertex 2. This translates to$\lambda_1$ vanishing on facet 2 and$\lambda_2$ vanishing on facet 1 (reversed).
only the interval should be permuted and tensor products would get the facet-based ordering for free.
- Since
compute_factor_barycentric_coordinatesrecurses into callingcompute_barycentric_coordinateson simplices forming the factors of the tensor-product cell, we just need to ensure that the facet-ordering is preserved on 1D intervals as the facet ordering already holds in all other simplices likeUFCTriangleandUFCTetrahedron.
|
@connorjward I changed the definition of Lines 795 to 797 in 82d51a9 and similarly those is There's one behaviour that I noted though when testing it through the compilation pipeline. Suppose Lines 125 to 131 in 82d51a9 currently produces. Now if I then do Lines 95 to 103 in 82d51a9 I think returning a |
I think you need to set self.free_indices = (self.free_index,)If you grep |
Tried this and it doesn't change anything to the behaviour I described above. Just to clarify: Lines 826 to 833 in 82d51a9 which explains why |
|
Are you treating |
I don't think I do other than I have it sub-classed from Regarding my first sentence: I wonder if instead of |
…plicies and tensor product cells
|
@pbrubeck Thanks again for helping me work this out. I've now made the required changes such that the barycentric coordinates are now listed in facet order, consistently across all cells. I added/modified the tests in |
I have just added a test in I cannot think right now of a better way to test this since entities in general TP cells are not indexed by integers but by dimension tuples. For example in prisms which is a triangle x interval we have: The concept of "bary coord i vanishes on facet i" only makes sense for cells that have a flat integer facet numbering like |
The facet numbering that we use is lexicographical, first ordered by the tuple dimension, then by entity number: |
There was a problem hiding this comment.
I cannot make code suggestions at the moment. To be more general, the permutation we want might not always be indices[::-1]. Instead we need something along the lines of
if facet_ordering:
facet_ids = self.connectivity[(entity_dim, entity_dim-1)][entity_id]
facets = [top[entity_dim-1][f] for f in facet_ids]
perm = [facets.index(tuple(set(indices) - {v})) for v in indices]
indices = [indices[p] for p in perm]
And with this we could set facet_ordering=True for any dimension
Yeah this makes sense to me. I am not sure how this would extend to multi-cell simplicies as I haven't worked with these yet but I noticed one thing: |
… sub-entities of simplicies + added notes + modified test checking facet ordering of bary coords on tp cells
|
In latest commit, I have generalised the facet ordering permutation of barycentric coordinates in Since we allow computing barycentric coordinates on sub-entities, I think a missing piece here is to implement a sub-entity decomposition in the case of tensor product cells (i.e., express the sub-entity of a tensor product cell in terms of its factors) fiat/FIAT/reference_element.py Lines 1453 to 1456 in 8084e8a Following this, we may want to pass fiat/FIAT/reference_element.py Lines 1463 to 1467 in 8084e8a |
|
Thanks for the hard work you've put into this. I still think we need to split this into FIAT-only and GEM-only PRs. Would that be okay? |
This PR is based on the initial work done in #230. In particular, two sets of new features are introduced:
Currently, FIAT exposes a method for computing barycentric coordinates on simplicies only. The order in which these barycentric coordinates are listed follows the UFC ordering of facets in FIAT such that the following invariant holds: the
i-th barycentric coordinate vanishes on faceti. In order for this invariant to hold not only on simplicies but also on other cells we need: 1) a method to compute barycentric coordinates on these cells 2) a permutation map that reorders the barycentric coordinates in facet order. For 1), I have implemented a method inTensorProductCellthat computes barycentric coordinates on each factor and for 2) I have implemented a methodcompute_facet_permutationthat computes the required permutation of coordinates for cells of typeHypercube.In order to ensure the above methods run not only on input
pointsthat arenumpyarrays but also when these aregem.Node(e.g., runtime-known points), I have extended GEM's indexing interface to support indexing with a slice (needed when extracting coordinates along specific axes individually), an Ellipsis (e..g,points[...,s]) and (optionally) an array of integer indices mimickingnumpy's fancy indexing (A[perm]wherepermis an integer permutation array). The Ellipsis is expanded intoslice(None)which is handled bygem.viewwhich already handles slice indexing. In particular, this introduces aFlexiblyIndexednode in the outputComponentTensorfor which I have implemented a handler in gem's evaluator for the tests to work.To support the integer array indexing, I have implemented a new index type in GEM called
ListIndexwhich can be used alongsideIndex(free index) andVariableIndexwhen indexing a GEM tensor.ListIndexacts as lookup table, wrapping a numpy array containing the specific index values to index the tensor at alongside a free index hat drives iteration over those valuesIndexed(A, (ListIndex(arr),))means "for each position i inarr, accesstensor[arr[I]]", withListIndex.free_indexas the loop variable ranging overlen(arr). Indexing a GEM tensor witharrproduces aComponentTensor(Indexed(tensor, (ListIndex(arr),)), (ListIndex.free_index,))converting the free index back into a shape dimension, such that the result is rank-1 GEM tensor representing the permuted elements. The tests at GEM's evaluator level confirm thatListIndexworks as expected - though code generation fails since Loopy does not currently have support forListIndex.In the context of this PR's main theme, which is computing barycentric coordinates, whether we really need
ListIndexdepends on the return type ofcompute_axis_barycentric_coordinates- the flattened barycentric coordinates array to which the integer facet permutation is applied. If this is anumpyarray then all works fine since we can just apply the permutation throughnumpy. However, if we want to return a GEM node such asgem.ListTensorfor instance, and apply the permutation throughGEMthen we would needListIndex.