Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
80304b7
add equality to cells
indiamai Apr 1, 2025
d881c4a
refactor
indiamai Apr 1, 2025
e49c703
first steps to making a fuse element from a flattened tensor prod
indiamai Apr 1, 2025
2fb6b88
first level of generated hasse diagram
indiamai Apr 3, 2025
e6341b9
fix recursion issue
indiamai Apr 7, 2025
208d64a
lint
indiamai Apr 7, 2025
3f06f03
refactor work on poly spaces and trying to figure out how to make qua…
indiamai Apr 7, 2025
01ac541
fix contains in polynomial sets
indiamai Apr 10, 2025
78e0861
working on cell
indiamai Apr 14, 2025
e9560f9
Ensure all numeric reps are 0..n over the whole group
indiamai Apr 23, 2025
37b6124
comparision to existing
indiamai Apr 24, 2025
d2d2d4b
tidy up
indiamai Mar 10, 2026
06a50d0
lint
indiamai Mar 10, 2026
52fa438
small fixes
indiamai Mar 10, 2026
0e197b4
modifications to polynomial kernel to take into account value shape
indiamai Mar 11, 2026
87dfe97
fix mistake in components
indiamai Mar 11, 2026
ea8265b
add jacobian
indiamai Mar 11, 2026
6ad49d3
ensure vector kernel and scalar poly kernel behave the same
indiamai Mar 11, 2026
3eaf4c7
bug fix
indiamai Mar 11, 2026
fdf00cc
use ufl sobolev spaces
indiamai Mar 11, 2026
f7f2709
first stab at addition
indiamai Mar 11, 2026
860f11e
fix bubble - addition working
indiamai Mar 12, 2026
d196320
fix tests
indiamai Mar 12, 2026
ec273f6
working towards symmetric tensor products
indiamai Mar 19, 2026
f3627a0
add groups to non flattened tensor product cell
indiamai Mar 20, 2026
8f3719a
flattening or not
indiamai Mar 23, 2026
6e4f40f
work on combining matrices
indiamai Apr 20, 2026
f91d745
fix choice of form degree for fuse elements, work on making hdiv elem…
indiamai Apr 21, 2026
9c107c0
add higher order tests, turn matrix use off
indiamai May 4, 2026
493d2b5
Merge branch 'main' into indiamai/full_symmetry
indiamai May 4, 2026
55ac2f9
adapt tensor product creation to follow fuse directions, include in m…
indiamai May 4, 2026
5748785
lint
indiamai May 4, 2026
7d03fb5
fix merge issues
indiamai May 5, 2026
c9c9bd6
making matrices for symmetric tps - successfully broke cg3
indiamai May 14, 2026
4b85868
pin ubuntu here too
indiamai May 14, 2026
0bcf3f6
initial incomplete stab at enriched tensor products
indiamai May 15, 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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
arch: [default]
runs-on: [self-hosted, Linux]
container:
image: ubuntu:latest
image: ubuntu:25.10
env:
OMPI_ALLOW_RUN_AS_ROOT: 1
OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ tests:
@echo " Running all tests"
@FIREDRAKE_USE_FUSE=1 python3 -m coverage run -p -m pytest -rx test

mini_tests:
@FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_2d_examples_docs.py
@FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_convert_to_fiat.py::test_1d
@FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_orientations.py::test_surface_vec_rt
@FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_convert_to_fiat.py::test_projection_convergence_3d\[construct_tet_ned-N1curl-1-0.8\]

coverage:
@python3 -m coverage combine
@python3 -m coverage report -m
Expand Down
5 changes: 3 additions & 2 deletions fuse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex

from fuse.cells import Point, Edge, polygon, line, make_tetrahedron, constructCellComplex, TensorProductPoint
from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, diff_C3, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, get_cyc_group, get_sym_group
from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, VectorKernel, BarycentricPolynomialKernel, PolynomialKernel, ComponentKernel
from fuse.triples import ElementTriple, DOFGenerator, immerse
from fuse.traces import TrH1, TrGrad, TrHess, TrHCurl, TrHDiv
from fuse.tensor_products import tensor_product
from fuse.tensor_products import tensor_product, symmetric_tensor_product

from fuse.spaces.element_sobolev_spaces import CellH1, CellL2, CellHDiv, CellHCurl, CellH2
from fuse.spaces.polynomial_spaces import P0, P1, P2, P3, Q2, PolynomialSpace
Expand Down
217 changes: 196 additions & 21 deletions fuse/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@
raise ValueError("Dimension {} not supported".format(d))


def line():
"""
Constructs the default 1D interval
"""
return Point(1, [Point(0), Point(0)], vertex_num=2)


def polygon(n):
"""
Constructs the 2D default cell with n sides/vertices
Expand Down Expand Up @@ -373,6 +380,7 @@
"""
verts = self.ordered_vertices()
v_coords = [self.get_node(v, return_coords=True) for v in verts]

n = len(verts)
max_group = SymmetricGroup(n)
edges = [edge.ordered_vertices() for edge in self.edges()]
Expand Down Expand Up @@ -502,6 +510,15 @@
min_ids = [min(dimension) for dimension in structure]
return min_ids

def local_id(self, node):
structure = [sorted(generation) for generation in nx.topological_generations(self.G)]
structure.reverse()
min_id = self.get_starter_ids()
for d in range(len(structure)):
if node.id in structure[d]:
return node.id - min_id[d]
raise ValueError("Node not found in cell")

def graph_dim(self):
if self.oriented:
dim = self.dimension + 1
Expand Down Expand Up @@ -648,7 +665,6 @@
self_levels = [generation for generation in nx.topological_generations(self.G)]
vertices = entity.ordered_vertices()
if self.dimension == 0:
# return [[]
raise ValueError("Dimension 0 entities cannot have Basis Vectors")
if self.oriented:
# ordered_vertices() handles the orientation so we want to drop the orientation node
Expand Down Expand Up @@ -822,7 +838,7 @@

return lambda *x: fold_reduce(attachments[0], *x)

def attachment_J(self, source, dst):
def attachment_J_det(self, source, dst):
attachment = self.attachment(source, dst)
symbol_names = ["x", "y", "z"]
symbols = []
Expand All @@ -831,7 +847,7 @@
for i in range(self.dim_of_node(dst)):
symbols += [sp.Symbol(symbol_names[i])]
J = sp.Matrix(attachment(*symbols)).jacobian(sp.Matrix(symbols))
return J
return np.sqrt(abs(float(sp.det(J.T * J))))

def quadrature(self, degree):
fiat_el = self.to_fiat()
Expand Down Expand Up @@ -903,6 +919,13 @@
def _from_dict(o_dict):
return Point(o_dict["dim"], o_dict["edges"], oriented=o_dict["oriented"], cell_id=o_dict["id"])

def equivalent(self, other):
if self.dimension != other.dimension:
return False
if set(self.ordered_vertex_coords()) != set(other.ordered_vertex_coords()):
return False
return self.get_topology() == other.get_topology()


class Edge():
"""
Expand All @@ -926,7 +949,12 @@
if hasattr(self.attachment, '__iter__'):
res = []
for attach_comp in self.attachment:
res.append(sympy_to_numpy(attach_comp, syms, x))
if len(attach_comp.atoms(sp.Symbol)) <= len(x):
res.append(sympy_to_numpy(attach_comp, syms, x))
else:
res_val = attach_comp.subs({syms[i]: x[i] for i in range(len(x))})
res.append(res_val)

return tuple(res)
return sympy_to_numpy(self.attachment, syms, x)
return x
Expand Down Expand Up @@ -957,50 +985,198 @@


class TensorProductPoint():
id_iter = itertools.count()

def __init__(self, A, B, flat=False):
def __init__(self, A, B):
self.id = next(self.id_iter)
self.A = A
self.B = B
self.dimension = self.A.dimension + self.B.dimension
self.flat = flat
self.flat = False
self.fiat_elem = None
self.group = self.compute_cell_group()
self.entities = {}
for d in [(a_d, b_d) for a_d in range(self.A.dimension + 1) for b_d in range(self.B.dimension + 1)]:
if d == (self.A.dimension, self.B.dimension):
self.entities[d] = [self]
else:
self.entities[d] = [TensorProductPoint(e_a, e_b) for e_a in self.A.d_entities(d[0], True) for e_b in self.B.d_entities(d[1], True)]

def ordered_vertices(self):
return self.A.ordered_vertices() + self.B.ordered_vertices()

def component_orientations(self):
from fuse.utils import orientation_value
self.component_os_to_os = {}
for dim in self.to_fiat().get_topology():
self.component_os_to_os[dim] = {}
a_ent = self.A.d_entities(dim[0])[0]
b_ent = self.B.d_entities(dim[1])[0]
verts = [(v_a, v_b) for v_a in a_ent.vertices() for v_b in b_ent.vertices()]
ident = [i for i in range(len(verts))]
group = [(g_a, g_b) for g_a in a_ent.group.members() for g_b in b_ent.group.members()]
for g_a, g_b in group:
new_verts = [(v_a, v_b) for v_a in g_a.permute(a_ent.vertices()) for v_b in g_b.permute(b_ent.vertices())]
perm = [verts.index(v) for v in new_verts]
o_val = orientation_value(ident, perm)
if sum(dim) == self.dimension and self.group.group_rep_numbering is not None:
o_val = self.group.group_rep_numbering[o_val]
self.component_os_to_os[dim][(g_a.numeric_rep(), g_b.numeric_rep())] = o_val
return self.component_os_to_os

def compute_cell_group(self):
"""
Systematically work out the symmetry group of the tensor product cell.
"""
verts = self.vertices()
group = [(g_a, g_b) for g_a in self.A.group.members() for g_b in self.B.group.members()]
perms = []
for g_a, g_b in group:
new_verts = [(v_a, v_b) for v_a in g_a.permute(self.A.vertices()) for v_b in g_b.permute(self.B.vertices())]
perm = [verts.index(v) for v in new_verts]
perms += [fuse_groups.Permutation(perm)]

grp = fuse_groups.PermutationSetRepresentation(perms).add_cell(self)
return grp

def get_starter_ids(self):
# this doesn't actually make sense - remove when confirmed all changes to eliminate min ids from triple is done
a_starts = self.A.get_starter_ids()
b_starts = self.B.get_starter_ids()
ids = []
for a, b in zip(a_starts, b_starts):
ids += [max(a, b)]
return ids

def get_spatial_dimension(self):
return self.dimension

def get_sub_entities(self):
self.A.get_sub_entities()
self.B.get_sub_entities()
return self.to_fiat().sub_entities

def dimension(self):
return tuple(self.A.dimension, self.B.dimension)
def dim(self):
return (self.A.dimension, self.B.dimension)

def d_entities(self, d, get_class=True):
if isinstance(d, tuple):
if get_class:
return self.entities[d]
return [e.id for e in self.entities[d]]
raise NotImplementedError("not sure this is right")
return self.A.d_entities(d, get_class) + self.B.d_entities(d, get_class)

def vertices(self, get_class=True, return_coords=False):
# TODO maybe refactor with get_node
verts = self.d_entities(0, get_class)
if return_coords:
a_verts = self.A.vertices(return_coords=return_coords)
b_verts = self.B.vertices(return_coords=return_coords)
return [a + b for a in a_verts for b in b_verts]
return verts
return [(a, b) for a in self.A.vertices() for b in self.B.vertices()]

def to_ufl(self, name=None):
if self.flat:
return CellComplexToUFL(self, "quadrilateral")
return TensorProductCell(self.A.to_ufl(), self.B.to_ufl())

def to_fiat(self, name=None):
if self.flat:
return CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name))
return CellComplexToFiatTensorProduct(self, name)
if self.fiat_elem is None:
self.fiat_elem = CellComplexToFiatTensorProduct(self, name)
return self.fiat_elem

def flatten(self):
assert self.A.equivalent(self.B)
return FlattenedPoint(self.A, self.B)


class FlattenedPoint(Point, TensorProductPoint):
d_entities_by_total_d = Point.d_entities

def __init__(self, A, B):
self.A = A
self.B = B
self.dimension = self.A.dimension + self.B.dimension
self.flat = True
fuse_edges = self.construct_fuse_rep()
super().__init__(self.dimension, fuse_edges)

def to_ufl(self, name=None):
return CellComplexToUFL(self, "quadrilateral")

def to_fiat(self, name=None):
# TODO this should check if it actually is a hypercube
fiat = CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name))
return fiat

Check failure on line 1107 in fuse/cells.py

View workflow job for this annotation

GitHub Actions / Run linter

W293

fuse/cells.py:1107:1: W293 blank line contains whitespace
def d_entities(self, d, get_class=True):
if isinstance(d, tuple):
if not get_class:
return [p.id for p in self.all_subpoints[d]]
return self.all_subpoints[d]
return self.d_entities_by_total_d(d, get_class)


def construct_fuse_rep(self):

Check failure on line 1116 in fuse/cells.py

View workflow job for this annotation

GitHub Actions / Run linter

E303

fuse/cells.py:1116:5: E303 too many blank lines (2)
sub_cells = [self.A, self.B]
dims = (self.A.dimension, self.B.dimension)
if sum(dims) > 2:
raise NotImplementedError("Flattening 3D tensor products not yet implemented")

self.all_subpoints = {(a_d, b_d): [] for a_d in range(self.A.dimension + 1) for b_d in range(self.B.dimension + 1)}
points = {cell: {i: [] for i in range(max(dims) + 1)} for cell in sub_cells}
attachments = {cell: {i: [] for i in range(max(dims) + 1)} for cell in sub_cells}

for d in range(max(dims) + 1):
for cell in sub_cells:
if d <= cell.dimension:
sub_ent = cell.d_entities(d, get_class=True)
points[cell][d].extend(sub_ent)
for s in sub_ent:
attachments[cell][d].extend(s.connections)

prod_points = list(itertools.product(*[points[cell][0] for cell in sub_cells]))
# temp = prod_points[1]
# prod_points[1] = prod_points[2]
# prod_points[2] = temp
point_cls = [Point(0) for i in range(len(prod_points))]
self.all_subpoints[(0,0)] = point_cls

Check failure on line 1139 in fuse/cells.py

View workflow job for this annotation

GitHub Actions / Run linter

E231

fuse/cells.py:1139:30: E231 missing whitespace after ','
edges = []

# generate edges of tensor product result
for a in prod_points:
for b in prod_points:
# of all combinations of point, take those where at least one changes and at least one is the same
if any(a[i] == b[i] for i in range(len(a))) and any(a[i] != b[i] for i in range(len(sub_cells))):
# ensure if they change, that edge exists in the existing topology
if all([a[i] == b[i] or (sub_cells[i].local_id(a[i]), sub_cells[i].local_id(b[i])) in list(sub_cells[i]._topology[1].values()) for i in range(len(sub_cells))]):
edges.append((a, b))
# hasse level 1
edge_cls1 = {e: None for e in edges}
for i in range(len(sub_cells)):
for (a, b) in edges:
a_idx = prod_points.index(a)
b_idx = prod_points.index(b)
if a[i] != b[i]:
a_edge = [att for att in attachments[sub_cells[i]][1] if att.point == a[i]][0]
b_edge = [att for att in attachments[sub_cells[i]][1] if att.point == b[i]][0]
edge_cls1[(a, b)] = Point(1, [Edge(point_cls[a_idx], a_edge.attachment, a_edge.o),
Edge(point_cls[b_idx], b_edge.attachment, b_edge.o)])
edge_tuple_dim = tuple(int((sub_cells[i].local_id(a[i]), sub_cells[i].local_id(b[i])) in list(sub_cells[i]._topology[1].values())) for i in range(len(sub_cells)))
self.all_subpoints[edge_tuple_dim] += [edge_cls1[(a, b)]]
edge_cls2 = []
# hasse level 2
for i in range(len(sub_cells)):
for (a, b) in edges:
if a[i] == b[i]:
x = sp.Symbol("x")
a_edge = [att for att in attachments[sub_cells[i]][1] if att.point == a[i]][0]
if i == 0:
attach = (x,) + a_edge.attachment
else:
attach = a_edge.attachment + (x,)
edge_cls2.append(Edge(edge_cls1[(a, b)], attach, a_edge.o))
self.all_subpoints[(1, 1)] = [self]
return edge_cls2

def flatten(self):
return TensorProductPoint(self.A, self.B, True)
return self


class CellComplexToFiatSimplex(Simplex):
Expand Down Expand Up @@ -1190,9 +1366,8 @@
return polygon(3).to_ufl(name)
# return ufc_triangle().to_ufl(name)
elif name == "quadrilateral":
interval = Point(1, [Point(0), Point(0)], vertex_num=2)
return TensorProductPoint(interval, interval).flatten().to_ufl(name)
# return ufc_quad().to_ufl(name)
return TensorProductPoint(line(), line()).flatten().to_ufl(name)
# return firedrake_quad().to_ufl(name)
# return polygon(4).to_ufl(name)
elif name == "tetrahedron":
# return ufc_tetrahedron().to_ufl(name)
Expand Down
Loading
Loading