Conversation
This PR needs firedrakeproject/fiat#294 and the UFL work in FEniCS/ufl#511 and FEniCS/ufl#512, none of which has merged, so CI installs both branches over the pins in pyproject.toml. Drop this commit once they land; nothing else on the branch touches .github. Both installs sit inside the Install Firedrake step, ahead of the Firedrake install rather than after it: that step ends with firedrake-clean, which imports Firedrake, and Firedrake cannot be imported against the released dependencies. A step of its own after the install therefore never gets to run.
numpy.array_equal compares shapes before it compares values, so it returned False for every table that the pass gave it and folded nothing but a scalar zero. A table of zeros stayed a dense Literal. Dual evaluation between facet-restricted elements on a tensor product cell tabulates the interior basis functions of each direct-sum component at the boundary points of every other component. Those tables are exactly zero, so 30 of the 49 component pairs that a hexahedron produces contribute nothing. Folding the tables lets Indexed and Product drop those pairs, which returns the interpolation to the O(degree^(dim + 1)) cost that sum factorisation gives. The kernel of tests/tsfc/test_dual_evaluation.py::test_dual_argument_is_sum_factorised loses 72% of its flops at degree 16. Fold on the value of the table, and keep the dtype so that a table of integers does not become a floating point zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013tWTW5ErfhCgV63nXai8HN
EnrichedElement presented two decompositions of the same direct sum. basis_evaluation concatenated blocks of shape elem.index_shape over self.elements, giving (4,4,3), (24,4) for NCE3, while _dual_evaluation concatenated whatever the as_enriched rewriting returned, giving (4,4,3), (96,). A basis and its dual basis must be blocked alike, so nothing downstream could pair them up and contract them. Promote EnrichedElement's private _summands to a `summands` property on every element, and block basis_evaluation, point_evaluation, dual_basis and _dual_evaluation along it alike. as_enriched on a FlattenedDimensions dropped the wrapper and returned summands on the tensor product cell, which cannot tabulate against the entities of the quadrilateral or hexahedron they came from. Distribute the flattening over the sum instead, as the other wrappers already do. Add split_contraction, which carries the identity that a sum over a whole direct sum is the sum of the sums over its blocks. Unlike unconcatenate it needs no assignment variable to carry the concatenation index, because the sum itself is what the Concatenate splits against. split_group holds the part that the two now share. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KWajdMc5VFPbPuB1cupu9F
rckirby
left a comment
There was a problem hiding this comment.
A lot of the verbiage in commentary is vague/awkward. Please clarify.
Also, I see some new tests. Can we confirm that the feature being added or that was previously broken is now being tested?
| downstream; a concatenation over the contracted points could not be. | ||
| The summands do not share their points, so their evaluations stack | ||
| along the basis index while retaining their own point indices. | ||
| Concatenating over a free basis index is what |
There was a problem hiding this comment.
"Concatenating" is what "unconcatenate" does is confusing to me.
| summand may be a direct sum in turn. These are the elements that | ||
| evaluate their dual basis on their own points, and whose points make | ||
| up the union that :attr:`dual_basis` works against. | ||
| An element is brought out as a direct sum one level at a time. A summand |
There was a problem hiding this comment.
The fully-flattened decomposition of this element as a sum of non-EnrichedElements
|
|
||
| @cached_property | ||
| def summands(self): | ||
| """The direct summands whose bases stack into this element's basis. |
There was a problem hiding this comment.
Clearer: Return (E1, E2, ..., En) where E = E1 \oplus E2 \oplus ... \oplus En?
Is this the correct idea?
| def _unconcatenate(cache, pairs): | ||
| # Tail-call recursive core of unconcatenate. | ||
| # Assumes that input has already been sanitised. | ||
| # Only an index carried by an assignment variable can be split against it. |
There was a problem hiding this comment.
Can this be stated more clearly?
What happens if this fails? Is there a check?
| index, multiindices, mappings = split_group(cache, concat_group) | ||
|
|
||
| def cut(node): | ||
| """No need to rebuild expression of independent of the |
There was a problem hiding this comment.
Grammar? "expression of independent of the ..."
There was a problem hiding this comment.
this grammar mistake was carried over
| def split_contraction(expression, indices, cache=None): | ||
| """Splits a contraction along the :py:class:`Concatenate` nodes it sums over. | ||
|
|
||
| No assignment variable need carry the concatenation index here. The sum |
There was a problem hiding this comment.
This is akward wording. Clarify, the math below is helpful!
Interpolation into facet-restricted finite element spaces on tensor-product cells was not being sum-factorised correctly. Three separate problems contributed.
1. Nested direct sums were not fully expanded
An enriched element can contain another direct sum. This occurs, for example, in
MixedElement(EnrichedElement(...)). Different code paths used different decompositions of the element. Basis tabulation stopped at the immediate elements, while dual evaluation could expand nested sums. The basis and dual basis then had different blocks, so TSFC could not pair the corresponding pieces.The element now exposes one fully expanded list of direct-sum leaves. Basis tabulation, point evaluation, dual evaluation, and dual-basis construction all use that list and the same ordering. Wrappers such as
FlattenedDimensionsare distributed over the sum so each leaf remains on the correct cell.2. All-zero tables were not folded
_constant_fold_zero_literalusednumpy.array_equal(array, 0). For a non-scalar array, that compares the array shape with the scalar shape and returns false. Dense tables containing only zeros therefore survived optimisation.These tables are common for direct sums. Each component is zero at the points belonging to the other components. The pass now checks the values directly and preserves the literal dtype. Later simplification can then remove the zero
IndexedandProductexpressions.3. Direct-sum contractions were not split
In simple terms,
Concatenate(A, B)is one long vector made by puttingAandBnext to each other. If two such vectors are multiplied entry by entry and summed, matching blocks can be summed separately:There are no cross-terms because each position in the first vector meets only the same position in the second vector.
split_contractionapplies this identity to the direct-sum blocks produced by FInAT. It lets TSFC contract each block over its own points and recursively handles more than one summed index.EnrichedElement._dual_evaluationnow leaves the summand point indices free. TSFC can therefore applysplit_contractionand choose the correct contraction for each block.Validation
NCE x Qmixed element, whereNCEis a compositeEnrichedElement.AI tools used in preparing this change: OpenAI Codex and Claude Code (Opus 5).