Sum factorisation on simplices#5263
Conversation
Extends tsfc/fem.py's translate_coefficient and translate_argument to
take a sum-factorized (Duffy/lattice) tabulation path for simplicial DG
Legendre elements under dx(scheme="collapsed"), targeting O(p^{d+1})
flops for the matrix-free residual instead of the dense O(p^{2d}).
The lattice multiindex from Legendre.duffy_evaluation is gathered
against coefficients and scattered back to the flat dof index through
FIAT's existing Morton dof numbering (new morton_forward_table /
morton_inverse_table in FIAT.expansions), entirely inside fem.py:
element.index_shape and argument_multiindices stay flat, so no
driver.py or kernel_interface changes are needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pbrubeck
left a comment
There was a problem hiding this comment.
TSFC codegen diff should be as tight as possible. Add the new code in a separate finat submodule.
| """Whether the sum-factorized (Duffy/lattice) coefficient contraction | ||
| applies. | ||
|
|
||
| This holds exactly when `element` is a simplicial DG element whose | ||
| nodal basis coincides with the Dubiner expansion set (any | ||
| `finat.duffy.DuffyElement`), evaluation points come from a collapsed | ||
| tensor-product quadrature rule (requested via | ||
| ``dx(scheme="collapsed")``), and the integral is over the cell | ||
| interior. In that case `DuffyElement.duffy_contraction` contracts a | ||
| `Coefficient` against the element in O(p^d) space/time using the | ||
| lattice multi-index, whereas the standard dense contraction | ||
| materializes all O(p^d) basis functions at all O(p^d) points before | ||
| contracting. | ||
|
|
||
| The argument (basis evaluation) side needs no such dispatch: it is | ||
| handled transparently by `DuffyElement.basis_evaluation`, reached | ||
| through the usual `~.PointSetContext.basis_evaluation` call. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| element : finat.finiteelementbase.FiniteElementBase | ||
| The element being tabulated. | ||
| ctx : ContextBase | ||
| The translation context. | ||
|
|
||
| Returns | ||
| ------- | ||
| bool | ||
| Whether to use `DuffyElement.duffy_contraction` in place of the | ||
| standard dense contraction. | ||
| """ |
There was a problem hiding this comment.
One-line docstring
…rred Legendre's dof order is now lattice-lexicographic (permuted in FIAT), which simplified finat/duffy.py's index arithmetic but did not eliminate the VariableIndex gather/scatter: get_indices() must stay a flat index because it can't distinguish a cell-interior kernel from a facet-integral kernel, and facet tabulation always uses the dense, flat-(ndof,) FIAT path. Fully eliminating the gather/scatter would need a bespoke jagged gem.FlexiblyIndexed view in kernel_interface/ common.py's prepare_arguments/prepare_coefficient, shared code every Firedrake kernel depends on -- deliberately deferred as a separate, higher-risk follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Temporarily pins firedrake-fiat to pbrubeck/simplex-sum-factor so CI exercises this branch's paired FIAT changes (dof-order permutation, gem.Delta fix). Revert to @main once the FIAT PR merges. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| u = TrialFunction(V) | ||
| v = TestFunction(V) | ||
| w = Function(V) | ||
| w.dat.data[:] = np.random.default_rng(0).random(w.dat.data.shape) |
There was a problem hiding this comment.
use rg = RandomGenerator(PCG64(seed=0))
translate_coefficient no longer dispatches on isinstance(element, DuffyElement): finat.duffy.DuffyElement.basis_evaluation now returns an already flat-dof-indexed tabulation, so the generic dense contraction path applies uniformly and recovers the same sum-factorized complexity without any special-casing. This also makes the Duffy fast path compose with Vector/TensorElement, which previously fell through to the slow path since TensorFiniteElement is never itself a DuffyElement instance. test_duffy_scatter_and_contract updated to contract generically instead of calling the now-removed duffy_contraction. test_collapsed_quadrature_sum_factorisation now compares against dx(scheme="canonical") instead of the default scheme: same collapsed Gauss-Jacobi points/weights as dx(scheme="collapsed"), but tabulated via the dense FIAT path, isolating the comparison to the sum-factorized tabulation itself rather than to a difference in quadrature rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
||
| @pytest.mark.parametrize("element_name", ["Legendre", "IntegratedLegendre"]) | ||
| @pytest.mark.parametrize("cellname,degree", [("triangle", 3), ("tetrahedron", 2)]) | ||
| def test_duffy_scatter_and_contract(cellname, degree, element_name): |
rckirby
left a comment
There was a problem hiding this comment.
This just checks that the code gives a correct answer. Do we have a way of checking whether the algorithm has the right complexity?
This PR adds both correctness and complexity tests. Complexity in flops is not enough, I also had to enforce tests on temporaries |
I see those tests, was thinking about FIAT. We should also test out Bernstein in the one-element benchmarks -- it doesn't have the indirection internally that modified C^0 expansions have but can be directly (after Duffy) sum-factored. |
7cfae19 to
6e30e23
Compare
Depends on firedrakeproject/fiat#262.
Summary
gem.JaggedIndexrepresents simplex lattice indices with bounds depending on parent indices. TSFC lowers these to parameterized Loopy domains.gem.FlattenedTensorexposes a jagged lattice through a flat DoF index while retaining its tensor-product structure.gem.SparseMatrixrepresents the sparse C0_basis recombination.DuffyElement.duffy_evaluation()constructs sum-factorized Legendre and IntegratedLegendre tabulations;get_sparse_coeffs()provides DG ordering or CG recombination data.gem.optimise now:
One-element benchmarks
FLOPs use the exact generated jagged loop domains. Times are median assembly times after warm-up.
AI assistance: OpenAI Codex was used for code analysis, implementation support, testing, and benchmarking.