Skip to content

Preserve direct-sum point indices in dual evaluation - #294

Open
pbrubeck wants to merge 3 commits into
mainfrom
pbrubeck/fix/dual-enriched-again
Open

Preserve direct-sum point indices in dual evaluation#294
pbrubeck wants to merge 3 commits into
mainfrom
pbrubeck/fix/dual-enriched-again

Conversation

@pbrubeck

@pbrubeck pbrubeck commented Sep 10, 2026

Copy link
Copy Markdown

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_evaluation summed each summand over its own points before it returned, and so reported an empty point_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_zero tested a table for zeros with numpy.array_equal(node.array, 0). That function compares shapes before it compares values, so it returned False for 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 Q elements on a hexahedron now costs O(degree^(dim + 1)) again.

Changes

  • EnrichedElement._dual_evaluation leaves 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 that gem.unconcatenate.unconcatenate splits downstream, so the caller chooses how to contract each block.
  • _constant_fold_zero_literal folds on the value of the table rather than on a shape comparison, so a Literal of any shape that holds only zeros becomes a Zero. Indexed and Product already simplify Zero away, so the expressions those tables multiply disappear with them.
  • The fold keeps the literal's dtype, so a table of integers does not become a floating point zero. tsfc.fem builds its quadrature orientation permutation maps as uint literals and indexes them.

The fold stays in this deferred pass rather than moving into Literal.__new__. gem.optimise.select_expression needs 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.py in Firedrake still covers it.

Tests

  • test/finat/test_dual_basis.py checks that the summand point indices stay free.
  • test/gem/test_simplify.py checks that a Literal of 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 approaches degree^(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.
  • Firedrake's tests/tsfc (395 tests), this repository's test/gem and test/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).

@pbrubeck pbrubeck added the LLM used An LLM was used in the production of this PR label Sep 10, 2026
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.
pbrubeck and others added 2 commits September 11, 2026 23:40
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

LLM used An LLM was used in the production of this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant