Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -175,7 +175,7 @@ jobs:
with:
repository: firedrakeproject/fiat
path: fiat-repo
ref: refs/heads/indiamai/integrate_fuse
ref: refs/heads/indiamai/fuse_ufl_spaces

- name: Install checked out FIAT
run: |
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
3 changes: 2 additions & 1 deletion fuse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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
Expand Down
140 changes: 126 additions & 14 deletions fuse/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ def compute_scaled_verts(d, n):
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 @@ def compute_cell_group(self):
"""
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 @@ def get_starter_ids(self):
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 @@ def basis_vectors(self, return_coords=True, entity=None, order=False, norm=True)
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,6 +838,17 @@ def attachment(self, source, dst):

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

def attachment_J_det(self, source, dst):
attachment = self.attachment(source, dst)
symbol_names = ["x", "y", "z"]
symbols = []
if self.dim_of_node(dst) == 0:
return 1
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 np.sqrt(abs(float(sp.det(J.T * J))))

def attachment_J(self, source, dst):
attachment = self.attachment(source, dst)
symbol_names = ["x", "y", "z"]
Expand Down Expand Up @@ -903,6 +930,13 @@ def dict_id(self):
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 +960,12 @@ def __call__(self, *x):
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 @@ -958,11 +997,11 @@ def _from_dict(o_dict):

class TensorProductPoint():

def __init__(self, A, B, flat=False):
def __init__(self, A, B):
self.A = A
self.B = B
self.dimension = self.A.dimension + self.B.dimension
self.flat = flat
self.flat = False

def ordered_vertices(self):
return self.A.ordered_vertices() + self.B.ordered_vertices()
Expand All @@ -974,8 +1013,8 @@ def get_sub_entities(self):
self.A.get_sub_entities()
self.B.get_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):
return self.A.d_entities(d, get_class) + self.B.d_entities(d, get_class)
Expand All @@ -990,17 +1029,91 @@ def vertices(self, get_class=True, return_coords=False):
return verts

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)

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


class FlattenedPoint(Point, TensorProductPoint):

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

def construct_fuse_rep(self):
sub_cells = [self.A, self.B]
dims = (self.A.dimension, self.B.dimension)

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))]
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_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))
return edge_cls2

def flatten(self):
return self


class CellComplexToFiatSimplex(Simplex):
Expand Down Expand Up @@ -1190,9 +1303,8 @@ def constructCellComplex(name):
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
31 changes: 16 additions & 15 deletions fuse/dof.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,16 @@ def __call__(self, *args):
return self.pt

def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape):
if len(value_shape) == 0:
comps = [[tuple()] for pt in Qpts]
else:
comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts]

if isinstance(self.pt, int):
return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts]
return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), comps
if not immersed:
return Qpts, np.array([wt*np.matmul(self.pt, basis_change)for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts]
return Qpts, np.array([wt*immersed(np.matmul(self.pt, basis_change))for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts]
return Qpts, np.array([wt*np.matmul(self.pt, basis_change)for wt in Qwts]).astype(np.float64), comps
return Qpts, np.array([wt*immersed(np.matmul(self.pt, basis_change))for wt in Qwts]).astype(np.float64), comps

def _to_dict(self):
o_dict = {"pt": self.pt}
Expand Down Expand Up @@ -304,9 +309,7 @@ def __call__(self, *args):
if self.shape == 0:
res = sympy_to_numpy(self.fn, self.syms, args[:len(self.syms)])
else:
res = []
for i in range(self.shape):
res += [sympy_to_numpy(self.fn[i], self.syms, args[:len(self.syms)])]
res = [sympy_to_numpy(self.fn[i], self.syms, args[:len(self.syms)]) for i in range(self.shape)]
return res

def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape):
Expand Down Expand Up @@ -411,9 +414,9 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non
self.pairing = self.pairing.add_entity(cell)
if self.target_space is None:
self.target_space = space
if self.id is None and overall_id is not None:
if overall_id is not None:
self.id = overall_id
if self.sub_id is None and generator_id is not None:
if generator_id is not None:
self.sub_id = generator_id

def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()):
Expand Down Expand Up @@ -452,20 +455,18 @@ def immersed(pt):
pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension, value_shape)

if self.immersed:
# need to compute jacobian from attachment.
pts = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts])
# J_det = self.cell.attachment_J_det(self.cell.id, self.cell_defined_on.id)
J_det = 1
J_det = self.cell.attachment_J_det(self.cell.id, self.cell_defined_on.id)
if not np.allclose(J_det, 1):
raise ValueError("Jacobian Determinant is not 1 did you do something wrong")
# if self.pairing.orientation:
# immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.pairing.orientation))[0]
# else:
immersion = self.target_space.tabulate(pts, self.cell_defined_on)
if isinstance(self.target_space, TrH1):
new_wts = wts
new_wts = wts * J_det
else:
new_wts = np.outer(wts * J_det, immersion)
# shape is wrong for 2d face on tet
# if isinstance(self.kernel, BarycentricPolynomialKernel) and self.kernel.shape > 1:
# new_wts = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in new_wts])
else:
new_wts = wts
# pt dict is { pt: [(weight, component)]}
Expand Down
18 changes: 17 additions & 1 deletion fuse/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,15 @@ def compute_perm(self, base_val=None):
return val, val_list

def numeric_rep(self):
""" Uses a standard formula to number permutations in the group.
For the case where this doesn't automatically number from 0..n (ie the group is not the full symmetry group),
a mapping is constructed on group creation"""
identity = self.group.identity.perm.array_form
m_array = self.perm.array_form
return orientation_value(identity, m_array)
val = orientation_value(identity, m_array)
if self.group.group_rep_numbering is not None:
return self.group.group_rep_numbering[val]
return val

def __eq__(self, x):
assert isinstance(x, GroupMemberRep)
Expand Down Expand Up @@ -144,6 +150,11 @@ def __init__(self, perm_list, cell=None):
counter += 1
# self._members = sorted(self._members, key=lambda g: g.numeric_rep())

self.group_rep_numbering = None
numeric_reps = [m.numeric_rep() for m in self.members()]
if sorted(numeric_reps) != list(range(len(numeric_reps))):
self.group_rep_numbering = {a: b for a, b in zip(sorted(numeric_reps), list(range(len(numeric_reps))))}

def add_cell(self, cell):
return PermutationSetRepresentation(self.perm_list, cell=cell)

Expand Down Expand Up @@ -268,6 +279,11 @@ def __init__(self, base_group, cell=None):
self.identity = p_rep
counter += 1

self.group_rep_numbering = None
numeric_reps = [m.numeric_rep() for m in self.members()]
if sorted(numeric_reps) != list(range(len(numeric_reps))):
self.group_rep_numbering = {a: b for a, b in zip(sorted(numeric_reps), list(range(len(numeric_reps))))}

# this order produces simpler generator lists
# self.generators.reverse()

Expand Down
Loading
Loading