Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
958e2a5
WIP: extract generic sum-factorisation infrastructure
pbrubeck Aug 2, 2026
f0453a4
WIP
pbrubeck Aug 2, 2026
1ecbee9
hoist_linear_index
pbrubeck Aug 3, 2026
6db0483
Remove superseded factorisation experiments
pbrubeck Aug 3, 2026
e073e11
Preserve contraction domains under distribution
pbrubeck Aug 3, 2026
67f63fc
Factor independent jagged iteration domains
pbrubeck Aug 6, 2026
29b748d
Avoid vacuous factorisation expansion
pbrubeck Aug 6, 2026
5028b1a
Bound factorisation candidate expansion
pbrubeck Aug 7, 2026
3612839
WIP: represent sparse basis maps as ragged contractions
pbrubeck Aug 7, 2026
4ddaa61
Remove superseded factorisation machinery
pbrubeck Aug 7, 2026
17dc6a1
Consolidate COFFEE factorisation search
pbrubeck Aug 7, 2026
4dd8246
Fix generic factorisation lint
pbrubeck Aug 7, 2026
f47759c
Respect ragged domains in GEM interpretation
pbrubeck Aug 7, 2026
ab864e2
Accept numeric mapped tabulations
pbrubeck Aug 7, 2026
9bc189a
Fix GEM docstring style
pbrubeck Aug 7, 2026
6ae4cd9
Bound polynomial factorisation search
pbrubeck Aug 13, 2026
43f1be4
Clarify factorisation documentation
pbrubeck Aug 13, 2026
0546911
Describe compact simplex lattice storage
pbrubeck Aug 13, 2026
b259306
Fix compact lattice docstring style
pbrubeck Aug 13, 2026
c78ab04
Share finite element linear maps in COFFEE
pbrubeck Aug 14, 2026
151c5b8
Leave simplex storage to simplex lowering
pbrubeck Aug 14, 2026
58a5d3b
Preserve finite element maps during factorisation
pbrubeck Aug 14, 2026
bd8a4c9
Expose sparse basis maps as ragged contractions
pbrubeck Aug 14, 2026
8999c15
Document contraction planning boundaries
pbrubeck Aug 14, 2026
43df8d1
Share contraction connectivity analysis
pbrubeck Aug 14, 2026
90a6b67
Schedule component tensor value loops explicitly
pbrubeck Aug 15, 2026
dd9e795
Fix FIAT docstring section spacing
pbrubeck Aug 15, 2026
0180216
Separate GEM contraction planning
pbrubeck Aug 15, 2026
5a96b11
Apply sparse basis maps as one rectangular contraction
pbrubeck Aug 15, 2026
ad1a982
Cut GEM code-generation cost for Johnson--Mercier
pbrubeck Aug 15, 2026
867be6e
Speed up GEM node construction and monomial accumulation
pbrubeck Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 97 additions & 30 deletions finat/physically_mapped.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
from abc import ABCMeta, abstractmethod
from collections.abc import Mapping
from collections.abc import Iterable, Mapping
from numbers import Number

import gem
import numpy

from finat.citations import cite


zero = gem.Zero()
one = gem.Literal(1.0)


def _as_basis_entry(value: object) -> gem.Node:
"""Normalize a basis-transformation matrix entry.

Parameters
----------
value
Scalar numerical or GEM matrix entry.

Returns
-------
gem.Node
Scalar GEM entry with numerical zero and one represented
symbolically.

"""
if isinstance(value, Number):
if value == 0:
return zero
if value == 1:
return one
return gem.as_gem(value)


class NeedsCoordinateMappingElement(metaclass=ABCMeta):
"""Abstract class for elements that require physical information
either to map or construct their basis functions."""
Expand All @@ -16,38 +44,81 @@ def dual_transformation(self, Q, coordinate_mapping=None):


class MappedTabulation(Mapping):
"""A lazy tabulation dict that applies the basis transformation only
on the requested derivatives.
"""Apply a sparse basis transformation to reference tabulations.

Parameters
----------
M : gem.ListTensor
Basis-transformation matrix.
ref_tabulation : Mapping
Reference tabulations indexed by derivative order.
indices : iterable of int, optional
Rows retained by an element restriction.

Notes
-----
The transformation is stored in compressed sparse row form. One
constant table holds the column of each nonzero, and a second holds its
value. Rows shorter than the longest are padded with zero values, which
contribute nothing to the sum.

Every row therefore contracts over the same number of entries. The
basis axis stays one loop, so the transformation reaches the quadrature
contraction as a linear map over the reference tabulation.

:arg M: a gem.ListTensor with the basis transformation matrix.
:arg ref_tabulation: a dict of tabulations on the reference cell.
:kwarg indices: an optional list of restriction indices on the basis functions.
"""
def __init__(self, M, ref_tabulation, indices=None):
self.M = M

def __init__(
self, M: gem.ListTensor, ref_tabulation: Mapping,
indices: Iterable[int] | None = None) -> None:
self.ref_tabulation = ref_tabulation
if indices is None:
indices = list(range(M.shape[0]))
self.indices = indices
# we expect M to be sparse with O(1) nonzeros per row
# for each row, get the column index of each nonzero entry
csr = [[j for j in range(M.shape[1]) if not isinstance(M.array[i, j], gem.Zero)]
for i in indices]
self.csr = csr
indices = range(M.shape[0])
self.indices = tuple(indices)

nonzero_rows = []
for source_row in self.indices:
row = []
for column in range(M.shape[1]):
value = _as_basis_entry(M.array[source_row, column])
if not isinstance(value, gem.Zero):
row.append((column, value))
nonzero_rows.append(row)
width = max((len(row) for row in nonzero_rows), default=0)
nrows = len(self.indices)
columns = numpy.zeros((nrows, width), dtype=gem.uint_type)
data = numpy.full((nrows, width), zero, dtype=object)
for index, row in enumerate(nonzero_rows):
columns[index, :len(row)] = tuple(column for column, _ in row)
data[index, :len(row)] = tuple(
gem.as_gem(value) for _, value in row)
self._width = width
self._columns = gem.Literal(columns, dtype=gem.uint_type)
self._values = gem.ListTensor(data)
self._tabulation_cache = {}

def matvec(self, table):
# basis recombination using hand-rolled sparse-dense matrix multiplication
ii = gem.indices(len(table.shape)-1)
phi = [gem.Indexed(table, (j, *ii)) for j in range(self.M.shape[1])]
# the sum approach is faster than calling numpy.dot or gem.IndexSum
exprs = [gem.ComponentTensor(gem.Sum(*(self.M.array[i, j] * phi[j] for j in js)), ii)
for i, js in zip(self.indices, self.csr)]
def matvec(self, table: gem.Node) -> gem.Node:
"""Transform one reference tabulation.

Parameters
----------
table
Reference tabulation with the basis axis first.

result = gem.ListTensor(exprs)
result, = gem.optimise.unroll_indexsum((result,), lambda index: True)
# result = gem.optimise.aggressive_unroll(self.M @ table)
return result
Returns
-------
gem.Node
Tabulation whose first axis is the transformed basis axis.

"""
tail = gem.indices(len(table.shape) - 1)
row = gem.Index(extent=len(self.indices))
entry = gem.Index(extent=self._width)
column = gem.VariableIndex(gem.Indexed(self._columns, (row, entry)))
basis = gem.Product(gem.Indexed(self._values, (row, entry)),
gem.Indexed(table, (column, *tail)))
mapped = gem.IndexSum(basis, (entry,))
return gem.ComponentTensor(mapped, (row, *tail))

def __getitem__(self, alpha):
try:
Expand Down Expand Up @@ -195,10 +266,6 @@ def physical_vertices(self):
(gdim, )."""


zero = gem.Zero()
one = gem.Literal(1.0)


def identity(*shape):
V = numpy.eye(*shape, dtype=object)
for multiindex in numpy.ndindex(V.shape):
Expand Down
Loading
Loading