Preserve direct-sum point indices in dual evaluation - #294
Open
pbrubeck wants to merge 3 commits into
Open
Conversation
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 10, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Interpolation into a facet-restricted element on a tensor product cell did not sum factorise. The dual basis of such an element is a direct sum over the entities of the cell, and two separate faults stopped TSFC from contracting each component of that sum on its own.
EnrichedElement._dual_evaluationsummed each summand over its own points before it returned, and so reported an emptypoint_indices. The caller could then only contract the whole concatenated evaluation at once, over every summand's points together, which is the contraction that sum factorisation has to split.gem.optimise.constant_fold_zerotested a table for zeros withnumpy.array_equal(node.array, 0). That function compares shapes before it compares values, so it returnedFalsefor every table the pass gave it and folded nothing but a scalar zero. The tabulations that vanish identically therefore survived as dense literals. They are common here: each direct-sum component tabulates its interior basis functions at the boundary points of the other components, and those tables are exactly zero. On a hexahedron 30 of the 49 pairs of components contribute nothing, and all 30 were compiled.Together the two faults cost a power of the degree. A degree-lowering interpolation between facet-restricted
Qelements on a hexahedron now costsO(degree^(dim + 1))again.Changes
EnrichedElement._dual_evaluationleaves each summand's point indices free and returns them, instead of contracting them itself. The evaluations stack along the basis index, which is the free index thatgem.unconcatenate.unconcatenatesplits downstream, so the caller chooses how to contract each block._constant_fold_zero_literalfolds on the value of the table rather than on a shape comparison, so aLiteralof any shape that holds only zeros becomes aZero.IndexedandProductalready simplifyZeroaway, so the expressions those tables multiply disappear with them.tsfc.fembuilds its quadrature orientation permutation maps asuintliterals and indexes them.The fold stays in this deferred pass rather than moving into
Literal.__new__.gem.optimise.select_expressionneeds every expression it is given to share a structural shape, and folding a literal eagerly breaks that for restricted elements on tensor product cells, which is the case at hand. Commit c8cbefa removed the eager fold for that reason and added this pass in its place;test/tsfc/test_tsfc_274.pyin Firedrake still covers it.Tests
test/finat/test_dual_basis.pychecks that the summand point indices stay free.test/gem/test_simplify.pychecks that aLiteralof any shape folds, that a table holding a nonzero entry does not, and that folding a zero table removes the product it appears in. The three shape cases and the product case fail on the previous predicate.tests/tsfc/test_dual_evaluation.py::test_dual_argument_is_sum_factorised[cell1-facet]in Firedrake passes again. Its kernel loses 72% of its flops at degree 16 (530974 → 149476), and the measured cost now approachesdegree^(dim + 1)from below, at 2.924, 2.977 and 2.990 over the degree pairs 4→8, 8→16 and 16→32, as the cases that already passed do.tests/tsfc(395 tests), this repository'stest/gemandtest/finat(403 tests), and Firedrake's interpolation regression tests (247 tests) all pass. Interpolation between facet-restricted spaces on an extruded hexahedral mesh reproduces a polynomial that both spaces hold, and satisfies<I^T c, u> == <c, I u>to machine precision, at degrees 3 to 6.AI was used to draft this change: the first commit with Codex, the second with Claude Code (Opus 5).