From c5d916dd40af512994f824ebd1b6e64ec9f9aae1 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 29 Sep 2025 14:34:34 +0100 Subject: [PATCH 01/98] add mcoked example for orientations --- test/test_orientations.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/test_orientations.py diff --git a/test/test_orientations.py b/test/test_orientations.py new file mode 100644 index 00000000..1cec51e7 --- /dev/null +++ b/test/test_orientations.py @@ -0,0 +1,23 @@ +import unittest.mock as mock +from firedrake import * +from fuse import * +from test_convert_to_fiat import create_cg1, helmholtz_solve + +def dummy_dof_perms(cls, *args, **kwargs): + # return -1s of right shape here + oriented_mats_by_entity, flat_by_entity = cls._initialise_entity_dicts(cls.generate()) + for key1, val1 in oriented_mats_by_entity.items(): + for key2, val2 in oriented_mats_by_entity[key1].items(): + for key3, val3 in oriented_mats_by_entity[key1][key2].items(): + oriented_mats_by_entity[key1][key2][key3] = -1 * np.identity(val3.shape[0]) + return oriented_mats_by_entity, False, None + +def test_orientation_application(): + deg = 1 + with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): + cell = polygon(3) + elem = create_cg1(cell) + mesh = UnitSquareMesh(4, 4) + + V = FunctionSpace(mesh, elem.to_ufl()) + res = helmholtz_solve(V, mesh) From fe85a47b047343dcaf7df45dd6c7e207c2c04d1c Mon Sep 17 00:00:00 2001 From: India Marsden Date: Fri, 3 Oct 2025 17:46:00 +0100 Subject: [PATCH 02/98] cg working - draft rt test --- test/test_orientations.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/test/test_orientations.py b/test/test_orientations.py index 1cec51e7..e4c8e3ed 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -1,7 +1,7 @@ import unittest.mock as mock from firedrake import * from fuse import * -from test_convert_to_fiat import create_cg1, helmholtz_solve +from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_rt def dummy_dof_perms(cls, *args, **kwargs): # return -1s of right shape here @@ -9,15 +9,43 @@ def dummy_dof_perms(cls, *args, **kwargs): for key1, val1 in oriented_mats_by_entity.items(): for key2, val2 in oriented_mats_by_entity[key1].items(): for key3, val3 in oriented_mats_by_entity[key1][key2].items(): - oriented_mats_by_entity[key1][key2][key3] = -1 * np.identity(val3.shape[0]) + oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) return oriented_mats_by_entity, False, None -def test_orientation_application(): +def test_orientation_application_mocked(): deg = 1 with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): cell = polygon(3) elem = create_cg1(cell) - mesh = UnitSquareMesh(4, 4) + mesh = UnitSquareMesh(1, 1) V = FunctionSpace(mesh, elem.to_ufl()) - res = helmholtz_solve(V, mesh) + u = TestFunction(V) + res1 = assemble(u * dx) + print(res1.dat.data) + + V = FunctionSpace(mesh, "CG", 1) + u = TestFunction(V) + res2 = assemble(u * dx) + print(res2.dat.data) + assert np.allclose(res1.dat.data, res2.dat.data) + +def test_orientation_application(): + deg = 1 + cell = polygon(3) + elem = construct_rt(cell) + mesh = UnitSquareMesh(1, 1) + ones = as_vector((1,1)) + + V = FunctionSpace(mesh, elem.to_ufl()) + u = TestFunction(V) + x = project(ones, V) + res1 = assemble(dot(u, x) * dx) + print(res1.dat.data) + + V = FunctionSpace(mesh, "RT", 1) + u = TestFunction(V) + x = project(ones, V) + res2 = assemble(dot(u, x) * dx) + print(res2.dat.data) + assert np.allclose(res1.dat.data, res2.dat.data) From 9aa1149dae3cf42b87c2d7452caf61665b5742c7 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 6 Oct 2025 13:19:28 +0100 Subject: [PATCH 03/98] add reversed matrices transforms --- fuse/cells.py | 2 ++ fuse/triples.py | 15 +++++++++++++++ test/test_orientations.py | 35 ++++++++++++++++++----------------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 14d4f4da..fb9694aa 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -13,6 +13,7 @@ from fuse.utils import sympy_to_numpy, fold_reduce, numpy_to_str_tuple, orientation_value from FIAT.reference_element import Simplex, TensorProductCell as FiatTensorProductCell, Hypercube from ufl.cell import Cell, TensorProductCell +from functools import cache class Arrow3D(FancyArrowPatch): @@ -469,6 +470,7 @@ def _subentity_traversal(self, sub_ents, min_ids): return sub_ents + @cache def get_starter_ids(self): structure = [sorted(generation) for generation in nx.topological_generations(self.G)] structure.reverse() diff --git a/fuse/triples.py b/fuse/triples.py index 14975f9f..473a305e 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -15,6 +15,7 @@ import warnings import numpy as np import scipy +from functools import cache class ElementTriple(): @@ -51,6 +52,7 @@ def __repr__(self): return "FuseTriple(%s, %s, (%s, %s, %s), %s)" % ( repr(self.DOFGenerator), repr(self.cell), repr(self.spaces[0]), repr(self.spaces[1]), repr(self.spaces[2]), "X") + @cache def generate(self): res = [] id_counter = 0 @@ -128,6 +130,7 @@ def to_fiat(self): self.matrices_by_entity = self.make_entity_dense_matrices(ref_el, entity_ids, nodes, poly_set) mat_perms, entity_perms, pure_perm = self.make_dof_perms(ref_el, entity_ids, nodes, poly_set) self.matrices = mat_perms + self.reverse_dof_perms() form_degree = 1 if self.spaces[0].set_shape else 0 # TODO: Change this when Dense case in Firedrake @@ -422,6 +425,18 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): return oriented_mats_by_entity, flat_by_entity, True return oriented_mats_by_entity, None, False + def reverse_dof_perms(self): + min_ids = self.cell.get_starter_ids() + reversed_mats = self.matrices.copy() + for dim in self.matrices.keys(): + ents = self.cell.d_entities(dim) + for e in ents: + e_id = e.id - min_ids[dim] + members = e.group.members() + for m in members: + reversed_mats[dim][e_id][m.numeric_rep()] = self.matrices[dim][e_id][(~m).numeric_rep()] + self.reversed_matrices = reversed_mats + def _to_dict(self): o_dict = {"cell": self.cell, "spaces": self.spaces, "dofs": self.DOFGenerator} return o_dict diff --git a/test/test_orientations.py b/test/test_orientations.py index e4c8e3ed..7d1fabab 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -1,7 +1,7 @@ import unittest.mock as mock from firedrake import * from fuse import * -from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_rt +from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_nd def dummy_dof_perms(cls, *args, **kwargs): # return -1s of right shape here @@ -9,7 +9,7 @@ def dummy_dof_perms(cls, *args, **kwargs): for key1, val1 in oriented_mats_by_entity.items(): for key2, val2 in oriented_mats_by_entity[key1].items(): for key3, val3 in oriented_mats_by_entity[key1][key2].items(): - oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) + oriented_mats_by_entity[key1][key2][key3] = -1 * np.identity(val3.shape[0]) return oriented_mats_by_entity, False, None def test_orientation_application_mocked(): @@ -32,20 +32,21 @@ def test_orientation_application_mocked(): def test_orientation_application(): deg = 1 - cell = polygon(3) - elem = construct_rt(cell) - mesh = UnitSquareMesh(1, 1) - ones = as_vector((1,1)) + with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): + cell = polygon(3) + elem = construct_nd(cell) + mesh = UnitSquareMesh(1, 1) + ones = as_vector((1,1)) - V = FunctionSpace(mesh, elem.to_ufl()) - u = TestFunction(V) - x = project(ones, V) - res1 = assemble(dot(u, x) * dx) - print(res1.dat.data) + V = FunctionSpace(mesh, elem.to_ufl()) + u = TestFunction(V) + x = project(ones, V) + #res1 = assemble(dot(u, x) * dx) + #print(res1.dat.data) - V = FunctionSpace(mesh, "RT", 1) - u = TestFunction(V) - x = project(ones, V) - res2 = assemble(dot(u, x) * dx) - print(res2.dat.data) - assert np.allclose(res1.dat.data, res2.dat.data) + V = FunctionSpace(mesh, "N1curl", 1) + u = TestFunction(V) + x = project(ones, V) + res2 = assemble(dot(u, x) * dx) + print(res2.dat.data) + #assert np.allclose(res1.dat.data, res2.dat.data) From 70c85a091397e1f4735aff6f1e556876a0d4c6ae Mon Sep 17 00:00:00 2001 From: India Marsden Date: Fri, 10 Oct 2025 16:38:39 +0100 Subject: [PATCH 04/98] oriented mat perms, nedelec 2nd order --- fuse/__init__.py | 2 +- fuse/dof.py | 63 ++++++-- fuse/spaces/polynomial_spaces.py | 7 +- fuse/triples.py | 21 ++- test/test_2d_examples_docs.py | 1 + test/test_convert_to_fiat.py | 18 ++- test/test_orientations.py | 244 ++++++++++++++++++++++++++++--- 7 files changed, 317 insertions(+), 39 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index 152f8156..c0ffec15 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,7 +1,7 @@ from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, tri_C3, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, get_cyc_group, get_sym_group -from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, PolynomialKernel +from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, 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 diff --git a/fuse/dof.py b/fuse/dof.py index fcca8b4a..52afcb5a 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -1,6 +1,6 @@ from FIAT.quadrature_schemes import create_quadrature from FIAT.quadrature import FacetQuadratureRule -from FIAT.functional import PointEvaluation, FrobeniusIntegralMoment +from FIAT.functional import PointEvaluation, IntegralMoment, FrobeniusIntegralMoment from fuse.utils import sympy_to_numpy import numpy as np import sympy as sp @@ -114,7 +114,7 @@ def permute(self, g): return res def convert_to_fiat(self, ref_el, dof, interpolant_degree): - total_deg = interpolant_degree + dof.kernel.degree() + total_deg = dof.kernel.degree(interpolant_degree) ent_id = self.entity.id - ref_el.fe_cell.get_starter_ids()[self.entity.dim()] # entity_ref = ref_el.construct_subelement(self.entity.dim()) entity = ref_el.construct_subelement(self.entity.dim(), ent_id, self.orientation) @@ -126,9 +126,15 @@ def convert_to_fiat(self, ref_el, dof, interpolant_degree): Jdet = Q.jacobian_determinant() # Jdet = pseudo_determinant(J) qpts, _ = Q.get_points(), Q.get_weights() - - f_at_qpts = dof.tabulate(qpts).T / Jdet - functional = FrobeniusIntegralMoment(ref_el, Q, f_at_qpts) + # (np.sqrt(2)) * + f_at_qpts = dof.tabulate(qpts).T / Jdet + comp = None + if hasattr(dof.kernel, "comp"): + # TODO Value shape should be a variable. + comp = dof.kernel.comp + functional = IntegralMoment(ref_el, Q, f_at_qpts, comp=comp, shp=(2,)) + else: + functional = FrobeniusIntegralMoment(ref_el, Q, f_at_qpts) return functional def __repr__(self): @@ -170,8 +176,8 @@ def __repr__(self): x = list(map(str, list(self.pt))) return ','.join(x) - def degree(self): - return 1 + def degree(self, interpolant_degree): + return interpolant_degree def permute(self, g): return PointKernel(g(self.pt)) @@ -180,8 +186,8 @@ def __call__(self, *args): return self.pt def tabulate(self, Qpts, attachment=None): - if attachment: - return np.array([attachment(*self.pt) for _ in Qpts]).astype(np.float64) + #if attachment: + # return np.array([attachment(*self.pt) for _ in Qpts]).astype(np.float64) return np.array([self.pt for _ in Qpts]).astype(np.float64) def _to_dict(self): @@ -207,10 +213,10 @@ def __init__(self, fn, symbols=[]): def __repr__(self): return str(self.fn) - def degree(self): + def degree(self, interpolant_degree): if len(self.fn.free_symbols) == 0: - return 1 - return self.fn.as_poly().total_degree() + return interpolant_degree + return self.fn.as_poly().total_degree() * interpolant_degree def permute(self, g): return self @@ -239,6 +245,39 @@ def dict_id(self): def _from_dict(obj_dict): return PolynomialKernel(obj_dict["fn"]) +class ComponentKernel(BaseKernel): + + def __init__(self, comp): + self.comp = comp + super(ComponentKernel, self).__init__() + + def __repr__(self): + return f"[{self.comp}]" + + def degree(self, interpolant_degree): + return interpolant_degree + + def permute(self, g): + return self + + def __call__(self, *args): + return args[self.comp] + + def tabulate(self, Qpts, attachment=None): + return np.array([1 for _ in Qpts]).astype(np.float64) + #return np.array([self(*pt) for pt in Qpts]).astype(np.float64) + + def _to_dict(self): + o_dict = {"comp": self.comp} + return o_dict + + def dict_id(self): + return "ComponentKernel" + + def _from_dict(obj_dict): + return ComponentKernel(obj_dict["comp"]) + + class DOF(): diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index bff2c957..c05844a1 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -204,8 +204,11 @@ def __mul__(self, x): __rmul__ = __mul__ def __add__(self, x): - return ConstructedPolynomialSpace(self.weights.extend([1]), - self.spaces.extend(x)) + w = self.weights.copy() + w.extend([1]) + s = self.spaces.copy() + s.extend([x]) + return ConstructedPolynomialSpace(w, s) def _to_dict(self): super_dict = super(ConstructedPolynomialSpace, self)._to_dict() diff --git a/fuse/triples.py b/fuse/triples.py index 473a305e..13c17bcd 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -1,4 +1,4 @@ -from fuse.cells import Point, TensorProductPoint +from fuse.cells import Point, TensorProductPoint, compare_topologies from fuse.spaces.element_sobolev_spaces import ElementSobolevSpace from fuse.dof import DeltaPairing, L2Pairing, FuseFunction, PointKernel from fuse.traces import Trace @@ -6,7 +6,7 @@ from fuse.utils import numpy_to_str_tuple from FIAT.dual_set import DualSet from FIAT.finite_element import CiarletElement -# from FIAT.reference_element import ufc_cell +from FIAT.reference_element import ufc_cell import matplotlib as mpl mpl.use('Agg') import matplotlib.pyplot as plt @@ -126,9 +126,9 @@ def to_fiat(self): entity_ids[dim][dofs[i].trace_entity.id - min_ids[dim]].append(counter) nodes.append(dofs[i].convert_to_fiat(ref_el, degree)) counter += 1 - # entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname()).get_topology(), self.cell.get_topology() self.matrices_by_entity = self.make_entity_dense_matrices(ref_el, entity_ids, nodes, poly_set) mat_perms, entity_perms, pure_perm = self.make_dof_perms(ref_el, entity_ids, nodes, poly_set) + mat_perms = self.orient_mat_perms(mat_perms) self.matrices = mat_perms self.reverse_dof_perms() form_degree = 1 if self.spaces[0].set_shape else 0 @@ -425,6 +425,21 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): return oriented_mats_by_entity, flat_by_entity, True return oriented_mats_by_entity, None, False + def orient_mat_perms(self, mat_perms): + min_ids = self.cell.get_starter_ids() + entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname()).get_topology(), self.cell.get_topology()) + num_ents = 0 + for dim in mat_perms.keys(): + ents = self.cell.d_entities(dim) + for e in ents: + e_id = e.id - min_ids[dim] + if entity_orientations[num_ents + e_id] != 0: + modifier = mat_perms[dim][e_id][entity_orientations[num_ents+e_id]] + for val, mat in mat_perms[dim][e_id].items(): + mat_perms[dim][e_id][val] = np.matmul(modifier, mat) + num_ents += len(ents) + return mat_perms + def reverse_dof_perms(self): min_ids = self.cell.get_starter_ids() reversed_mats = self.matrices.copy() diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 511d762d..de6be11b 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -165,6 +165,7 @@ def construct_nd(tri=None): y = sp.Symbol("y") # xs = [DOF(L2Pairing(), PointKernel(edge.basis_vectors()[0]))] + #xs = [DOF(L2Pairing(), PointKernel((np.sqrt(2),)))] xs = [DOF(L2Pairing(), PolynomialKernel((1,)))] dofs = DOFGenerator(xs, S1, S2) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index a08a74a0..e33f4f30 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -573,8 +573,7 @@ def project(U, mesh, func): (lambda cell: CR_n(cell, 3), "CR", 1), (create_cf, "CR", 1), # Don't think Crouzeix Falk in in Firedrake (construct_cg3, "CG", 3), - pytest.param(construct_nd, "N1curl", 1, marks=pytest.mark.xfail(reason='Dense Matrices needed')), - pytest.param(construct_rt, "RT", 1, marks=pytest.mark.xfail(reason='Dense Matrices needed'))]) + ]) def test_project(elem_gen, elem_code, deg): cell = polygon(3) elem = elem_gen(cell) @@ -587,6 +586,21 @@ def test_project(elem_gen, elem_code, deg): assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) +@pytest.mark.parametrize("elem_gen,elem_code,deg", [ + pytest.param(construct_nd, "N1curl", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 ND'), + pytest.param(construct_rt, "RT", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 RT')]) +def test_project_vec(elem_gen, elem_code, deg): + cell = polygon(3) + elem = elem_gen(cell) + mesh = UnitTriangleMesh() + + U = FunctionSpace(mesh, elem_code, deg) + assert np.allclose(project(U, mesh, as_vector((1,1))), 0, rtol=1e-5) + + U = FunctionSpace(mesh, elem.to_ufl()) + assert np.allclose(project(U, mesh, as_vector((1,1))), 0, rtol=1e-5) + + @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_dg1_tet, "DG", 1)]) def test_project_3d(elem_gen, elem_code, deg): cell = make_tetrahedron() diff --git a/test/test_orientations.py b/test/test_orientations.py index 7d1fabab..71a2362a 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -1,6 +1,7 @@ import unittest.mock as mock from firedrake import * from fuse import * +import sympy as sp from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_nd def dummy_dof_perms(cls, *args, **kwargs): @@ -9,7 +10,8 @@ def dummy_dof_perms(cls, *args, **kwargs): for key1, val1 in oriented_mats_by_entity.items(): for key2, val2 in oriented_mats_by_entity[key1].items(): for key3, val3 in oriented_mats_by_entity[key1][key2].items(): - oriented_mats_by_entity[key1][key2][key3] = -1 * np.identity(val3.shape[0]) + oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) + oriented_mats_by_entity[key1][key2][key3][0] = key1*100 + key2*10 + key3 return oriented_mats_by_entity, False, None def test_orientation_application_mocked(): @@ -21,32 +23,236 @@ def test_orientation_application_mocked(): V = FunctionSpace(mesh, elem.to_ufl()) u = TestFunction(V) - res1 = assemble(u * dx) - print(res1.dat.data) + + f1 = Function(V) + x, y = SpatialCoordinate(mesh) + f1.interpolate(x) + #res1 = assemble(u * dx) + #print(res1.dat.data) V = FunctionSpace(mesh, "CG", 1) u = TestFunction(V) - res2 = assemble(u * dx) - print(res2.dat.data) + f = Function(V) + x, y = SpatialCoordinate(mesh) + f.interpolate(x) + print(f"f1, {f1.dat.data}") + print(f"f, {f.dat.data}") + #res2 = assemble(u * dx) + #print(res2.dat.data) + breakpoint() assert np.allclose(res1.dat.data, res2.dat.data) def test_orientation_application(): deg = 1 - with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): - cell = polygon(3) - elem = construct_nd(cell) - mesh = UnitSquareMesh(1, 1) - ones = as_vector((1,1)) + cell = polygon(3) + elem = construct_nd(cell) + mesh = UnitSquareMesh(4, 4) + ones = as_vector((1,1)) + + V = FunctionSpace(mesh, elem.to_ufl()) + u = TestFunction(V) + res1 = assemble(dot(u, ones) * dx) + print(res1.dat.data) + + V = FunctionSpace(mesh, "N1curl", 2) + u = TestFunction(V) + res2 = assemble(dot(u, ones) * dx) + print(res2.dat.data) + assert np.allclose(res1.dat.data, res2.dat.data) + + + +def test_interpolation_const_fire(): + print() + mesh = UnitSquareMesh(1, 1) + ones = as_vector((1,0)) + V = FunctionSpace(mesh, "N1curl", 1) + u = TestFunction(V) + res2= assemble(interpolate(ones, V)) + print(res2.dat.data) + print([n.pt_dict for n in V.finat_element.fiat_equivalent.dual_basis()]) + +def test_interpolation_const_fuse(): + cell = polygon(3) + elem = construct_nd(cell) + print() + #with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): + mesh = UnitSquareMesh(1, 1) + ones = as_vector((1,0)) + V = FunctionSpace(mesh, elem.to_ufl()) + u = TestFunction(V) + res1= assemble(interpolate(ones, V)) + print(res1.dat.data) + print([n.pt_dict for n in V.finat_element.fiat_equivalent.dual_basis()]) + breakpoint() + + +def test_surface_const_fuse(): + cell = polygon(3) + elem = construct_nd(cell) + ones = as_vector((1,0)) + for n in range(1, 6): + mesh = UnitSquareMesh(n, n) V = FunctionSpace(mesh, elem.to_ufl()) - u = TestFunction(V) - x = project(ones, V) - #res1 = assemble(dot(u, x) * dx) - #print(res1.dat.data) + n = FacetNormal(mesh) + ones1 = interpolate(ones, V) + res1= assemble(dot(ones1, n) * ds) + + print(f"{n}: {res1}") + assert np.allclose(res1, 0) + +def test_surface_const_fire(): + cell = polygon(3) + elem = construct_nd(cell) + ones = as_vector((1,0)) + for n in range(1, 6): + mesh = UnitSquareMesh(n, n) + V = FunctionSpace(mesh, "N1curl", 1) - u = TestFunction(V) - x = project(ones, V) - res2 = assemble(dot(u, x) * dx) - print(res2.dat.data) - #assert np.allclose(res1.dat.data, res2.dat.data) + n = FacetNormal(mesh) + ones2 = interpolate(ones, V) + res1= assemble(dot(ones2, n) * ds) + + print(f"{n}: {res1}") + assert np.allclose(res1, 0) + + +def test_surface_y(): + import os + import subprocess + cell = polygon(3) + elem = construct_nd(cell) + ones = as_vector((1,0)) + + for n in range(2, 6): + + mesh = UnitSquareMesh(n, n) + x, y = SpatialCoordinate(mesh) + V = FunctionSpace(mesh, elem.to_ufl()) + normal = FacetNormal(mesh) + vec1 = interpolate(as_vector((y, 0)), V) + res1= assemble(dot(vec1, normal) * ds) + + #V = FunctionSpace(mesh, "N1curl", 1) + #normal = FacetNormal(mesh) + #vec2 = interpolate(as_vector((y, 0)), V) + #res1 = assemble(dot(vec2, normal) * ds) + print(f"{n}: {res1}") + + #print(f"{n}: 1: {res1} 2: {res2}") + assert np.allclose(1/n, res2) + + +def test_interpolate_vs_project(V): + mesh = V.mesh() + dim = mesh.geometric_dimension() + if dim == 2: + x, y = SpatialCoordinate(mesh) + elif dim == 3: + x, y, z = SpatialCoordinate(mesh) + + shape = V.value_shape + if dim == 2: + if len(shape) == 0: + expression = x + y + elif len(shape) == 1: + expression = as_vector([x, y]) + elif len(shape) == 2: + expression = as_tensor(([x, y], [x, y])) + elif dim == 3: + if len(shape) == 0: + expression = x + y + z + elif len(shape) == 1: + expression = as_vector([x, y, z]) + elif len(shape) == 2: + expression = as_tensor(([x, y, z], [x, y, z], [x, y, z])) + + f = assemble(interpolate(expression, V)) + expect = project(expression, V) + print(f.dat.data) + print(expect.dat.data) + assert np.allclose(f.dat.data, expect.dat.data, atol=1e-06) + +def construct_nd2(tri=None): + if tri is None: + tri = polygon(3) + deg = 2 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + #xs = [DOF(L2Pairing(), PointKernel((-np.sqrt(2),)))] + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x+1), symbols=[x])), + DOF(L2Pairing(), PolynomialKernel((1/2)*(1-x), symbols=[x]))] + + + dofs = DOFGenerator(xs, S1, S2) + int_ned = ElementTriple(edge, (P1, CellHCurl, C0), dofs) + + xs = [DOF(L2Pairing(), ComponentKernel((0,))), + DOF(L2Pairing(), ComponentKernel((1,)))] + center_dofs = DOFGenerator(xs, S1, S3) + xs = [immerse(tri, int_ned, TrHCurl)] + tri_dofs = DOFGenerator(xs, C3, S1) + + vec_Pk = PolynomialSpace(deg - 1, set_shape=True) + Pk = PolynomialSpace(deg - 1) + M = sp.Matrix([[y, -x]]) + nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M + + + ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) + return ned + + +def test_n1(): + cell = polygon(3) + #elem = construct_nd2(cell) + mesh = UnitSquareMesh(1,1) + + #print("fuse") + #V = FunctionSpace(mesh, elem.to_ufl()) + #test_interpolate_vs_project(V) + + print("firedrake") + V = FunctionSpace(mesh, "N1curl", 2) + test_interpolate_vs_project(V) + +def test_create_fiat_nd(): + cell = polygon(3) + nd = construct_nd2(cell) + ref_el = cell.to_fiat() + sd = ref_el.get_spatial_dimension() + deg = 2 + + from FIAT.nedelec import Nedelec + fiat_elem = Nedelec(ref_el, deg) + print("fiat") + for f in fiat_elem.dual_basis(): + print(f.pt_dict) + + print("fuse") + for d in nd.generate(): + print(d.convert_to_fiat(ref_el, deg).pt_dict) + print(d) + breakpoint() + my_elem = nd.to_fiat() + + + Q = create_quadrature(ref_el, 2*(deg+1)) + Qpts, _ = Q.get_points(), Q.get_weights() + + fiat_vals = fiat_elem.tabulate(0, Qpts) + my_vals = my_elem.tabulate(0, Qpts) + + fiat_vals = flatten(fiat_vals[(0,) * sd]) + my_vals = flatten(my_vals[(0,) * sd]) + + (x, res, _, _) = np.linalg.lstsq(fiat_vals.T, my_vals.T) + x1 = np.linalg.inv(x) + assert np.allclose(np.linalg.norm(my_vals.T - fiat_vals.T @ x), 0) + assert np.allclose(np.linalg.norm(fiat_vals.T - my_vals.T @ x1), 0) + assert np.allclose(res, 0) + From 1d408948d5dcbd019c3d29b36c0be5bfdb7e6747 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 14 Oct 2025 12:03:04 +0100 Subject: [PATCH 05/98] tidy up tests --- test/test_convert_to_fiat.py | 5 +- test/test_orientations.py | 163 ++++++++++++++--------------------- 2 files changed, 68 insertions(+), 100 deletions(-) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index e33f4f30..128a53e1 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -586,9 +586,8 @@ def test_project(elem_gen, elem_code, deg): assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) -@pytest.mark.parametrize("elem_gen,elem_code,deg", [ - pytest.param(construct_nd, "N1curl", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 ND'), - pytest.param(construct_rt, "RT", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 RT')]) +@pytest.mark.parametrize("elem_gen,elem_code,deg", [pytest.param(construct_nd, "N1curl", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 ND')), + pytest.param(construct_rt, "RT", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 RT'))]) def test_project_vec(elem_gen, elem_code, deg): cell = polygon(3) elem = elem_gen(cell) diff --git a/test/test_orientations.py b/test/test_orientations.py index 71a2362a..de81546e 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -2,7 +2,8 @@ from firedrake import * from fuse import * import sympy as sp -from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_nd +from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_nd, construct_rt +import os def dummy_dof_perms(cls, *args, **kwargs): # return -1s of right shape here @@ -10,139 +11,107 @@ def dummy_dof_perms(cls, *args, **kwargs): for key1, val1 in oriented_mats_by_entity.items(): for key2, val2 in oriented_mats_by_entity[key1].items(): for key3, val3 in oriented_mats_by_entity[key1][key2].items(): - oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) - oriented_mats_by_entity[key1][key2][key3][0] = key1*100 + key2*10 + key3 + if key1 == 2: + oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) + #oriented_mats_by_entity[key1][key2][key3][0] = key1*100 + key2*10 + key3 + if key3 == 5: + + oriented_mats_by_entity[key1][key2][key3] = 100 * np.identity(val3.shape[0]) return oriented_mats_by_entity, False, None -def test_orientation_application_mocked(): - deg = 1 - with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): - cell = polygon(3) - elem = create_cg1(cell) - mesh = UnitSquareMesh(1, 1) - V = FunctionSpace(mesh, elem.to_ufl()) - u = TestFunction(V) - - f1 = Function(V) - x, y = SpatialCoordinate(mesh) - f1.interpolate(x) - #res1 = assemble(u * dx) - #print(res1.dat.data) - - V = FunctionSpace(mesh, "CG", 1) - u = TestFunction(V) - f = Function(V) - x, y = SpatialCoordinate(mesh) - f.interpolate(x) - print(f"f1, {f1.dat.data}") - print(f"f, {f.dat.data}") - #res2 = assemble(u * dx) - #print(res2.dat.data) - breakpoint() - assert np.allclose(res1.dat.data, res2.dat.data) - -def test_orientation_application(): - deg = 1 - cell = polygon(3) - elem = construct_nd(cell) - mesh = UnitSquareMesh(4, 4) - ones = as_vector((1,1)) - - V = FunctionSpace(mesh, elem.to_ufl()) - u = TestFunction(V) - res1 = assemble(dot(u, ones) * dx) - print(res1.dat.data) - - V = FunctionSpace(mesh, "N1curl", 2) - u = TestFunction(V) - res2 = assemble(dot(u, ones) * dx) - print(res2.dat.data) - assert np.allclose(res1.dat.data, res2.dat.data) - - - -def test_interpolation_const_fire(): - print() - mesh = UnitSquareMesh(1, 1) - ones = as_vector((1,0)) - V = FunctionSpace(mesh, "N1curl", 1) - u = TestFunction(V) - res2= assemble(interpolate(ones, V)) - print(res2.dat.data) - print([n.pt_dict for n in V.finat_element.fiat_equivalent.dual_basis()]) - -def test_interpolation_const_fuse(): +def test_interpolation_values(): cell = polygon(3) elem = construct_nd(cell) print() #with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): - mesh = UnitSquareMesh(1, 1) - ones = as_vector((1,0)) - V = FunctionSpace(mesh, elem.to_ufl()) + mesh = UnitSquareMesh(3, 3) + ones = as_vector((0,1)) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + else: + V = FunctionSpace(mesh, "N1curl", 1) + + print(V.cell_node_list) u = TestFunction(V) res1= assemble(interpolate(ones, V)) - print(res1.dat.data) - print([n.pt_dict for n in V.finat_element.fiat_equivalent.dual_basis()]) - breakpoint() + for i in range(len(res1.dat.data)): + print(f"{i}: {res1.dat.data[i]}") -def test_surface_const_fuse(): +def test_surface_const_nd(): cell = polygon(3) elem = construct_nd(cell) - ones = as_vector((1,0)) + ones = as_vector((0,1)) for n in range(1, 6): mesh = UnitSquareMesh(n, n) - V = FunctionSpace(mesh, elem.to_ufl()) - n = FacetNormal(mesh) + + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + else: + V = FunctionSpace(mesh, "N1curl", 1) + normal = FacetNormal(mesh) ones1 = interpolate(ones, V) - res1= assemble(dot(ones1, n) * ds) + res1= assemble(dot(ones1, normal) * ds) print(f"{n}: {res1}") assert np.allclose(res1, 0) -def test_surface_const_fire(): + +def test_surface_const_rt(): cell = polygon(3) - elem = construct_nd(cell) + elem = construct_rt(cell) ones = as_vector((1,0)) for n in range(1, 6): mesh = UnitSquareMesh(n, n) - - V = FunctionSpace(mesh, "N1curl", 1) - n = FacetNormal(mesh) - ones2 = interpolate(ones, V) - res1= assemble(dot(ones2, n) * ds) + V = FunctionSpace(mesh, elem.to_ufl()) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + else: + V = FunctionSpace(mesh, "RT", 1) + normal = FacetNormal(mesh) + ones1 = interpolate(ones, V) + res1= assemble(dot(ones1, normal) * ds) print(f"{n}: {res1}") assert np.allclose(res1, 0) -def test_surface_y(): - import os - import subprocess +def test_surface_vec(): cell = polygon(3) - elem = construct_nd(cell) - ones = as_vector((1,0)) + rt_elem = construct_rt(cell) + nd_elem = construct_nd(cell) - for n in range(2, 6): + for n in range(1, 6): mesh = UnitSquareMesh(n, n) x, y = SpatialCoordinate(mesh) - V = FunctionSpace(mesh, elem.to_ufl()) normal = FacetNormal(mesh) - vec1 = interpolate(as_vector((y, 0)), V) - res1= assemble(dot(vec1, normal) * ds) - - #V = FunctionSpace(mesh, "N1curl", 1) - #normal = FacetNormal(mesh) - #vec2 = interpolate(as_vector((y, 0)), V) - #res1 = assemble(dot(vec2, normal) * ds) - print(f"{n}: {res1}") + test_vec = as_vector((-y,x)) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, rt_elem.to_ufl()) + vec1 = interpolate(test_vec, V) + res1 = assemble(dot(vec1, normal) * ds) + else: + V = FunctionSpace(mesh, "RT", 1) + vec2 = interpolate(test_vec, V) + res1 = assemble(dot(vec2, normal) * ds) + print(f"div {n}: {res1}") - #print(f"{n}: 1: {res1} 2: {res2}") - assert np.allclose(1/n, res2) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, nd_elem.to_ufl()) + vec1 = interpolate(test_vec, V) + res2 = assemble(dot(vec1, normal) * ds) + else: + V = FunctionSpace(mesh, "N1curl", 1) + vec1 = interpolate(test_vec, V) + res2 = assemble(dot(vec1, normal) * ds) + print(f"curl {n}: {res2}") + + assert np.allclose(0, res1) + assert np.allclose(0, res2) def test_interpolate_vs_project(V): @@ -207,7 +176,7 @@ def construct_nd2(tri=None): return ned -def test_n1(): +def test_degree2_interpolation(): cell = polygon(3) #elem = construct_nd2(cell) mesh = UnitSquareMesh(1,1) From 3ef20105d0b3a1eb79de82efa41295f9cab775a7 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 16 Oct 2025 15:55:25 +0100 Subject: [PATCH 06/98] tidy up mat perms --- fuse/groups.py | 9 +++++++++ fuse/triples.py | 48 +++++++++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/fuse/groups.py b/fuse/groups.py index 0ec63ef0..a08d96e0 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -165,6 +165,14 @@ def get_member(self, perm): return m raise ValueError("Permutation not a member of group") + + def get_member_by_val(self, val): + for m in self.members(): + if m.numeric_rep() == val: + return m + + raise ValueError("Value does not represent a group member") + def compute_num_reps(self, base_val=0): """ Compute the numerical represention of each member as compared to the identity. Where the numerical rep is: @@ -277,6 +285,7 @@ def get_member(self, perm): if m.perm == perm: return m raise ValueError("Permutation not a member of group") + # def compute_reps(self, g, path, remaining_members): # # breadth first search to find generator representations of all members diff --git a/fuse/triples.py b/fuse/triples.py index 13c17bcd..3d4e3f72 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -108,16 +108,14 @@ def to_fiat(self): top = ref_el.get_topology() min_ids = self.cell.get_starter_ids() poly_set = self.spaces[0].to_ON_polynomial_set(ref_el) + #from FIAT.nedelec import Nedelec + #poly_set = Nedelec(ref_el, 2).poly_set for dim in sorted(top): entity_ids[dim] = {i: [] for i in top[dim]} entity_perms[dim] = {} entities = [(dim, entity) for dim in sorted(top) for entity in sorted(top[dim])] - # if sort_entities: - # # sort the entities by support vertex ids - # support = [sorted(top[dim][entity]) for dim, entity in entities] - # entities = [entity for verts, entity in sorted(zip(support, entities))] counter = 0 for entity in entities: dim = entity[0] @@ -128,9 +126,10 @@ def to_fiat(self): counter += 1 self.matrices_by_entity = self.make_entity_dense_matrices(ref_el, entity_ids, nodes, poly_set) mat_perms, entity_perms, pure_perm = self.make_dof_perms(ref_el, entity_ids, nodes, poly_set) - mat_perms = self.orient_mat_perms(mat_perms) - self.matrices = mat_perms - self.reverse_dof_perms() + if not pure_perm: + self.matrices = mat_perms + self.reverse_dof_perms() + self.orient_mat_perms() form_degree = 1 if self.spaces[0].set_shape else 0 # TODO: Change this when Dense case in Firedrake @@ -390,6 +389,11 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() else: # TODO what if an orientation is not in G1 + #warnings.warn("FUSE: should probably be using equivalent members of g in g1 for this") + #sub_mat = g.matrix_form() + #oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + + #raise NotImplementedError(f"Orientation {g} is not in group {dof_gen_class[dim].g1.members()}") pass if len(dof_gen_class.keys()) == 2 and dim == self.cell.dim(): @@ -420,36 +424,46 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): for val, mat in oriented_mats_overall.items(): cell_dofs = entity_ids[dim][0] flat_by_entity[dim][e_id][val] = perm_matrix_to_perm_array(mat[np.ix_(cell_dofs, cell_dofs)]) - if pure_perm and sub_pure_perm: return oriented_mats_by_entity, flat_by_entity, True return oriented_mats_by_entity, None, False - def orient_mat_perms(self, mat_perms): + def orient_mat_perms(self): min_ids = self.cell.get_starter_ids() entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname()).get_topology(), self.cell.get_topology()) num_ents = 0 - for dim in mat_perms.keys(): + for dim in self.matrices.keys(): ents = self.cell.d_entities(dim) for e in ents: e_id = e.id - min_ids[dim] - if entity_orientations[num_ents + e_id] != 0: - modifier = mat_perms[dim][e_id][entity_orientations[num_ents+e_id]] - for val, mat in mat_perms[dim][e_id].items(): - mat_perms[dim][e_id][val] = np.matmul(modifier, mat) + members = e.group.members() + if entity_orientations[num_ents + e_id] != 0 and dim < self.cell.dim(): + modifier = self.matrices[dim][e_id][entity_orientations[num_ents+e_id]] + reverse_modifier_val = (~e.group.get_member_by_val(entity_orientations[num_ents+e_id])).numeric_rep() + reverse_modifier = self.matrices[dim][e_id][reverse_modifier_val] + perms_copy = self.matrices[dim][e_id].copy() + for val, mat in self.matrices[dim][e_id].items(): + perms_copy[val] = np.matmul(modifier, mat) + self.matrices[dim][e_id] = perms_copy + perms_copy = self.reversed_matrices[dim][e_id].copy() + for val, mat in self.reversed_matrices[dim][e_id].items(): + perms_copy[val] = np.matmul(reverse_modifier, mat) + self.reversed_matrices[dim][e_id] = perms_copy num_ents += len(ents) - return mat_perms def reverse_dof_perms(self): min_ids = self.cell.get_starter_ids() - reversed_mats = self.matrices.copy() + reversed_mats = {} for dim in self.matrices.keys(): + reversed_mats[dim] = {} ents = self.cell.d_entities(dim) for e in ents: e_id = e.id - min_ids[dim] + perms_copy = self.matrices[dim][e_id].copy() members = e.group.members() for m in members: - reversed_mats[dim][e_id][m.numeric_rep()] = self.matrices[dim][e_id][(~m).numeric_rep()] + perms_copy[m.numeric_rep()] = self.matrices[dim][e_id][(~m).numeric_rep()] + reversed_mats[dim][e_id] = perms_copy self.reversed_matrices = reversed_mats def _to_dict(self): From a93e1f4da2b668e0864f3ef69047707e29c61c61 Mon Sep 17 00:00:00 2001 From: India Marsden <37078108+indiamai@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:09:29 +0000 Subject: [PATCH 07/98] Fix 2nd order nedelec (#41) * issues with poly degree and adding parameterisation kernels * debugging * light refactor * work on parameterisation kernel * working on dof permutations * Make to fiat follow the definition closer (#40) kernels evaluated on immersed entity cells provide quadrature kernels modify weights and components points are then immersed if needed * work on orientation of dofs - restructuring, still needs a big clean up * reassess transformation of kernel, generating basis pair through reflection * fix ordering issue with generation of center dofs * test for two forms * add 3d polynomial example * some tidying up, fiddling with tests * Generation of permutations matrices for firedrake (#42) --- .github/workflows/setup_repos.sh | 18 +- .github/workflows/test.yml | 16 +- Makefile | 12 +- conftest.py | 4 +- docs/source/conf.py | 9 +- fuse/__init__.py | 3 +- fuse/cells.py | 49 +++- fuse/dof.py | 225 +++++++++------ fuse/groups.py | 30 +- fuse/spaces/polynomial_spaces.py | 6 +- fuse/traces.py | 71 ++--- fuse/triples.py | 156 +++++----- fuse/utils.py | 17 +- test/test_2d_examples_docs.py | 33 +-- test/test_3d_examples_docs.py | 68 +++-- test/test_convert_to_fiat.py | 36 ++- test/test_dofs.py | 88 +++++- test/test_orientations.py | 473 ++++++++++++++++++++++--------- test/test_polynomial_space.py | 37 +++ test/test_tensor_prod.py | 1 + 20 files changed, 889 insertions(+), 463 deletions(-) diff --git a/.github/workflows/setup_repos.sh b/.github/workflows/setup_repos.sh index f986e1fe..ea5d1fe5 100644 --- a/.github/workflows/setup_repos.sh +++ b/.github/workflows/setup_repos.sh @@ -16,12 +16,12 @@ git checkout indiamai/integrate_fuse git status python3 -m pip install --break-system-packages -e . -/usr/bin/git config --global --add safe.directory ~ -cd ~ -git clone https://github.com/firedrakeproject/ufl.git -/usr/bin/git config --global --add safe.directory ~/ufl -cd ufl -git fetch -git checkout indiamai/integrate-fuse -git status -python3 -m pip install --break-system-packages -e . \ No newline at end of file +#/usr/bin/git config --global --add safe.directory ~ +#cd ~ +#git clone https://github.com/firedrakeproject/ufl.git +#/usr/bin/git config --global --add safe.directory ~/ufl +#cd ufl +#git fetch +#git checkout indiamai/integrate-fuse +#git status +#python3 -m pip install --break-system-packages -e . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a265da9b..88b3d0f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,8 @@ jobs: # Run on the Github hosted runner runs-on: ubuntu-latest container: - image: firedrakeproject/firedrake-vanilla-default:latest + #image: firedrakeproject/firedrake-vanilla-default:latest + image: firedrakeproject/firedrake-vanilla-default:dev-main # Steps represent a sequence of tasks that will be executed as # part of the jobs steps: @@ -44,17 +45,6 @@ jobs: git checkout indiamai/integrate_fuse git status python3 -m pip install --break-system-packages -e . - - name: Checkout correct UFL branch - run: | - /usr/bin/git config --global --add safe.directory ~ - cd ~ - git clone https://github.com/firedrakeproject/ufl.git - /usr/bin/git config --global --add safe.directory ~/ufl - cd ufl - git fetch - git checkout indiamai/integrate-fuse - git status - python3 -m pip install --break-system-packages -e . - name: Run tests run: | pip list @@ -104,4 +94,4 @@ jobs: message: ${{ env.total }}% minColorRange: 50 maxColorRange: 90 - valColorRange: ${{ env.total }} \ No newline at end of file + valColorRange: ${{ env.total }} diff --git a/Makefile b/Makefile index 969fd028..734a8d34 100644 --- a/Makefile +++ b/Makefile @@ -19,12 +19,12 @@ lint: test_examples: @echo " Running examples" - @python3 -m pytest test/test_2d_examples_docs.py - @python3 -m pytest test/test_3d_examples_docs.py + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_2d_examples_docs.py + @FIREDRAKE_USE_FUSE=1 python3 -m pytest test/test_3d_examples_docs.py tests: @echo " Running all tests" - @python3 -m coverage run -p -m pytest -rx test + @FIREDRAKE_USE_FUSE=1 python3 -m coverage run -p -m pytest -rx test coverage: @python3 -m coverage combine @@ -34,13 +34,13 @@ coverage: test_cells: @echo " Running all cell comparison tests" @firedrake-clean - @python3 -m pytest -rPx --run-cleared test/test_cells.py::test_ref_els[expect0] + @FIREDRAKE_USE_FUSE=1 python3 -m pytest -rPx --run-cleared test/test_cells.py::test_ref_els[expect0] @firedrake-clean - @python3 -m pytest -rPx --run-cleared test/test_cells.py::test_ref_els[expect1] + @FIREDRAKE_USE_FUSE=1 python3 -m pytest -rPx --run-cleared test/test_cells.py::test_ref_els[expect1] clean: @(cd docs/ && make clean) prepush: lint tests @rm .coverage.* - make clean docs \ No newline at end of file + make clean docs diff --git a/conftest.py b/conftest.py index c8c4e009..acf59ca6 100644 --- a/conftest.py +++ b/conftest.py @@ -1,9 +1,7 @@ -import pytest - def pytest_addoption(parser): parser.addoption( "--run-cleared", action="store_true", default=False, help="Run tests that require a cleared cache", - ) \ No newline at end of file + ) diff --git a/docs/source/conf.py b/docs/source/conf.py index 154171e8..a8781a85 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -87,8 +87,11 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # html_css_files = ["additional.css"] + + def setup(app): - app.add_css_file('additional.css') + app.add_css_file('additional.css') + html_theme_options = { 'navbar_links': [ @@ -109,11 +112,11 @@ def setup(app): 'globaltoc_depth': 2, } -html_sidebars = {'api/*': ['localtoc.html'], +html_sidebars = {'api/*': ['localtoc.html'], 'api': ['localtoc.html'], 'about': [], 'install': [], 'index': ['localtoc.html'], 'manual': ['localtoc.html'], 'manual/*': ['localtoc.html'], - '_generated/*': ['custom-sidebar.html'], } \ No newline at end of file + '_generated/*': ['custom-sidebar.html'], } diff --git a/fuse/__init__.py b/fuse/__init__.py index c0ffec15..605f3cf9 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,7 +1,6 @@ - from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, tri_C3, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, get_cyc_group, get_sym_group -from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, PolynomialKernel, ComponentKernel +from fuse.dof import DeltaPairing, DOF, L2Pairing, FuseFunction, PointKernel, VectorKernel, 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 diff --git a/fuse/cells.py b/fuse/cells.py index fb9694aa..6ac51360 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -12,6 +12,7 @@ from sympy.combinatorics.named_groups import SymmetricGroup from fuse.utils import sympy_to_numpy, fold_reduce, numpy_to_str_tuple, orientation_value from FIAT.reference_element import Simplex, TensorProductCell as FiatTensorProductCell, Hypercube +from FIAT.quadrature_schemes import create_quadrature from ufl.cell import Cell, TensorProductCell from functools import cache @@ -94,6 +95,7 @@ def compute_scaled_verts(d, n): :param: n: number of vertices """ if d == 2: + source = np.array([-np.sqrt(3)/2, -1/2]) source = np.array([0, 1]) rot_coords = [source for i in range(0, n)] @@ -544,7 +546,7 @@ def get_node(self, node, return_coords=False): if return_coords: top_level_node = self.d_entities_ids(self.graph_dim())[0] if self.dimension == 0: - return [()] + return () return self.attachment(top_level_node, node)() return self.G.nodes.data("point_class")[node] @@ -623,7 +625,11 @@ def basis_vectors(self, return_coords=True, entity=None): if self.dimension == 0: # return [[] raise ValueError("Dimension 0 entities cannot have Basis Vectors") - top_level_node = self_levels[0][0] + if self.oriented: + # ordered_vertices() handles the orientation so we want to drop the orientation node + top_level_node = self_levels[1][0] + else: + top_level_node = self_levels[0][0] v_0 = vertices[0] if return_coords: v_0_coords = self.attachment(top_level_node, v_0)() @@ -656,6 +662,27 @@ def to_tikz(self, show=True, scale=3): return "\n".join(tikz_commands) return tikz_commands + def generate_facet_parameterisation(self, facet_num): + raise NotImplementedError("Facet Parameterisation can be expressed using polynomials") + # facet = self.d_entities(self.dimension - 1)[facet_num] + facet = self.get_node(facet_num) + facet_dim = facet.dimension + if facet_dim != self.dimension - 1: + raise ValueError(f"Supplied node {facet_num} is not a facet") + if facet_dim > 1: + raise NotImplementedError("Facet parameterisation is not implemented for dimensions greater than 1") + verts = facet.vertices() + v_coords = np.array([self.get_node(v.id, return_coords=True) for v in verts]) + stacked = np.c_[np.ones((self.dimension,)), v_coords[:, 0].reshape(self.dimension, 1)] + b = np.array([0, 1]) + coeffs = np.linalg.solve(stacked, b) + symbol_names = ["x", "y", "z"] + symbols = [1] + [sp.Symbol(symbol_names[d]) for d in range(facet_dim)] + res = 0 + for d in range(facet_dim + 1): + res += coeffs[d] * symbols[d] + return res, symbols[1:] + def plot(self, show=True, plain=False, ax=None, filename=None): """ for now into 2 dimensional space """ if self.dimension == 3: @@ -767,6 +794,12 @@ def attachment(self, source, dst): return lambda *x: fold_reduce(attachments[0], *x) + def quadrature(self, degree): + fiat_el = self.to_fiat() + Q = create_quadrature(fiat_el, degree) + pts, wts = Q.get_points(), Q.get_weights() + return pts, wts + def cell_attachment(self, dst): if not isinstance(dst, int): raise ValueError @@ -775,6 +808,10 @@ def cell_attachment(self, dst): def orient(self, o): """ Orientation node is always labelled with -1 """ + if o is None: + return self + if self.oriented: + o = self.oriented * o oriented_point = copy.deepcopy(self) top_level_node = oriented_point.d_entities_ids( oriented_point.dimension)[0] @@ -838,10 +875,7 @@ def __call__(self, *x): if hasattr(self.attachment, '__iter__'): res = [] for attach_comp in self.attachment: - if len(attach_comp.atoms(sp.Symbol)) == len(x): - res.append(sympy_to_numpy(attach_comp, syms, x)) - else: - res.append(attach_comp.subs({syms[i]: x[i] for i in range(len(x))})) + res.append(sympy_to_numpy(attach_comp, syms, x)) return tuple(res) return sympy_to_numpy(self.attachment, syms, x) return x @@ -885,7 +919,6 @@ def get_spatial_dimension(self): def get_sub_entities(self): self.A.get_sub_entities() self.B.get_sub_entities() - breakpoint() def dimension(self): return tuple(self.A.dimension, self.B.dimension) @@ -1076,7 +1109,7 @@ def __init__(self, cell, name=None): super(CellComplexToUFL, self).__init__(name) def to_fiat(self): - return self.cell_complex.to_fiat(name=self.cellname()) + return self.cell_complex.to_fiat(name=self.cellname) def __repr__(self): return super(CellComplexToUFL, self).__repr__() diff --git a/fuse/dof.py b/fuse/dof.py index 52afcb5a..4f00948f 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -1,6 +1,4 @@ -from FIAT.quadrature_schemes import create_quadrature -from FIAT.quadrature import FacetQuadratureRule -from FIAT.functional import PointEvaluation, IntegralMoment, FrobeniusIntegralMoment +from FIAT.functional import Functional from fuse.utils import sympy_to_numpy import numpy as np import sympy as sp @@ -35,11 +33,8 @@ def __call__(self, kernel, v, cell): assert isinstance(kernel, PointKernel) return v(*kernel.pt) - def convert_to_fiat(self, ref_el, dof, interpolant_deg): - pt = dof.eval(FuseFunction(lambda *x: x)) - # pt1 = dof.tabulate([[1]]) - return PointEvaluation(ref_el, pt) - # return PointEvaluation(ref_el, tuple(pt1[0])) + def tabulate(self): + return 1 def add_entity(self, entity): res = DeltaPairing() @@ -51,7 +46,10 @@ def add_entity(self, entity): def permute(self, g): res = DeltaPairing() if self.entity: - res.entity = self.entity.orient(g) + res.entity = self.entity + # res.entity = self.entity.orient(g) + if self.orientation is not None: + g = g * self.orientation res.orientation = g return res @@ -73,28 +71,16 @@ def __init__(self): super(L2Pairing, self).__init__() def __call__(self, kernel, v, cell): - # TODO get degree of v - # if cell == self.entity: - # ref_el = self.entity.to_fiat() - # # print("evaluating", kernel, v, "on", self.entity) - # Q = create_quadrature(self.entity.to_fiat(), 5) - # # need quadrature here too - therefore need the information from the triple. - # else: - # ref_el = cell.to_fiat() - # ent_id = self.entity.id - ref_el.fe_cell.get_starter_ids()[self.entity.dim()] - # entity_ref = ref_el.construct_subelement(self.entity.dim()) - # entity = ref_el.construct_subelement(self.entity.dim(), ent_id, self.orientation) - # Q_ref = create_quadrature(entity, 5) - # Q = FacetQuadratureRule(ref_el, self.entity.dim(), ent_id, Q_ref, self.orientation) - Q = create_quadrature(self.entity.to_fiat(), 5) - - def kernel_dot(x): - return np.dot(kernel(*x), v(*x)) - - return Q.integrate(kernel_dot) + Qpts, Qwts = self.entity.quadrature(5) + return sum([wt*np.dot(kernel(*pt), v(*pt)) for pt, wt in zip(Qpts, Qwts)]) def tabulate(self): - pass + bvs = np.array(self.entity.basis_vectors()) + if self.orientation: + new_bvs = np.array(self.entity.orient(self.orientation).basis_vectors()) + basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) + return basis_change + return np.eye(bvs.shape[0]) def add_entity(self, entity): res = L2Pairing() @@ -109,35 +95,15 @@ def permute(self, g): res = L2Pairing() if self.entity: - res.entity = self.entity.orient(g) + res.entity = self.entity + if self.orientation is not None: + g = g * self.orientation res.orientation = g return res - def convert_to_fiat(self, ref_el, dof, interpolant_degree): - total_deg = dof.kernel.degree(interpolant_degree) - ent_id = self.entity.id - ref_el.fe_cell.get_starter_ids()[self.entity.dim()] - # entity_ref = ref_el.construct_subelement(self.entity.dim()) - entity = ref_el.construct_subelement(self.entity.dim(), ent_id, self.orientation) - Q_ref = create_quadrature(entity, total_deg) - # pts_ref, wts_ref = Q_ref.get_points(), Q_ref.get_weights() - - # pts, wts, J = map_quadrature(pts_ref, wts_ref, Q_ref.ref_el, entity_ref, jacobian=True) - Q = FacetQuadratureRule(ref_el, self.entity.dim(), ent_id, Q_ref, self.orientation) - Jdet = Q.jacobian_determinant() - # Jdet = pseudo_determinant(J) - qpts, _ = Q.get_points(), Q.get_weights() - # (np.sqrt(2)) * - f_at_qpts = dof.tabulate(qpts).T / Jdet - comp = None - if hasattr(dof.kernel, "comp"): - # TODO Value shape should be a variable. - comp = dof.kernel.comp - functional = IntegralMoment(ref_el, Q, f_at_qpts, comp=comp, shp=(2,)) - else: - functional = FrobeniusIntegralMoment(ref_el, Q, f_at_qpts) - return functional - def __repr__(self): + if self.orientation is not None: + return "integral_{}({})({{kernel}} * {{fn}}) dx) ".format(str(self.orientation), str(self.entity)) return "integral_{}({{kernel}} * {{fn}}) dx) ".format(str(self.entity)) def dict_id(self): @@ -152,8 +118,13 @@ def _from_dict(obj_dict): class BaseKernel(): def __init__(self): + self.cell = None + self.entity = None self.attachment = False + def add_context(self, cell, entity): + return self + def permute(self, g): raise NotImplementedError("This method should be implemented by the subclass") @@ -185,10 +156,8 @@ def permute(self, g): def __call__(self, *args): return self.pt - def tabulate(self, Qpts, attachment=None): - #if attachment: - # return np.array([attachment(*self.pt) for _ in Qpts]).astype(np.float64) - return np.array([self.pt for _ in Qpts]).astype(np.float64) + def evaluate(self, Qpts, Qwts, basis_change, immersed): + return np.array([self.pt for _ in Qpts]).astype(np.float64), np.ones_like(Qwts), [[tuple()] for pt in Qpts] def _to_dict(self): o_dict = {"pt": self.pt} @@ -201,12 +170,49 @@ def _from_dict(obj_dict): return PointKernel(tuple(obj_dict["pt"])) +class VectorKernel(BaseKernel): + + def __init__(self, x, g=None): + self.pt = x + self.g = g + super(VectorKernel, self).__init__() + + def __repr__(self): + x = list(map(str, list(self.pt))) + return ','.join(x) + + def degree(self, interpolant_degree): + return interpolant_degree + + def permute(self, g): + return VectorKernel(self.pt, g) + + def __call__(self, *args): + return self.pt + + def evaluate(self, Qpts, Qwts, basis_change, immersed): + if immersed: + return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] + return Qpts, np.array([wt*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] + + def _to_dict(self): + o_dict = {"pt": self.pt} + return o_dict + + def dict_id(self): + return "VectorKernel" + + def _from_dict(obj_dict): + return VectorKernel(tuple(obj_dict["pt"])) + + class PolynomialKernel(BaseKernel): - def __init__(self, fn, symbols=[]): + def __init__(self, fn, g=None, symbols=[]): if len(symbols) != 0 and not sp.sympify(fn).as_poly(): raise ValueError("Function argument must be able to be interpreted as a sympy polynomial") self.fn = sp.sympify(fn) + self.g = g self.syms = symbols super(PolynomialKernel, self).__init__() @@ -215,25 +221,25 @@ def __repr__(self): def degree(self, interpolant_degree): if len(self.fn.free_symbols) == 0: - return interpolant_degree + return interpolant_degree return self.fn.as_poly().total_degree() * interpolant_degree def permute(self, g): - return self # new_fn = self.fn.subs({self.syms[i]: g(self.syms)[i] for i in range(len(self.syms))}) - # return PolynomialKernel(new_fn, symbols=self.syms) + new_fn = self.fn + return PolynomialKernel(new_fn, g=g, symbols=self.syms) def __call__(self, *args): res = sympy_to_numpy(self.fn, self.syms, args[:len(self.syms)]) - if not hasattr(res, '__iter__'): - return [res] + # if not hasattr(res, '__iter__'): + # return [res] + # if self.g: + # print(self.g, self.g(res)) + # return self.g(res) return res - def tabulate(self, Qpts, attachment=None): - # TODO do we need to attach qpts - # if attachment: - # return np.array([self(*attachment(*pt)) for pt in Qpts]).astype(np.float64) - return np.array([self(*pt) for pt in Qpts]).astype(np.float64) + def evaluate(self, Qpts, Qwts, basis_change, immersed): + return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] def _to_dict(self): o_dict = {"fn": self.fn} @@ -245,6 +251,7 @@ def dict_id(self): def _from_dict(obj_dict): return PolynomialKernel(obj_dict["fn"]) + class ComponentKernel(BaseKernel): def __init__(self, comp): @@ -252,7 +259,7 @@ def __init__(self, comp): super(ComponentKernel, self).__init__() def __repr__(self): - return f"[{self.comp}]" + return f"[{self.comp}]" def degree(self, interpolant_degree): return interpolant_degree @@ -261,11 +268,12 @@ def permute(self, g): return self def __call__(self, *args): - return args[self.comp] + return tuple(args[i] if i in self.comp else 0 for i in range(len(args))) +# return tuple(args[c] for c in self.comp) - def tabulate(self, Qpts, attachment=None): - return np.array([1 for _ in Qpts]).astype(np.float64) - #return np.array([self(*pt) for pt in Qpts]).astype(np.float64) + def evaluate(self, Qpts, Qwts, basis_change, immersed): + return Qpts, Qwts, [[self.comp] for pt in Qpts] + # return Qpts, np.array([self(*pt) for pt in Qpts]).astype(np.float64) def _to_dict(self): o_dict = {"comp": self.comp} @@ -278,20 +286,20 @@ def _from_dict(obj_dict): return ComponentKernel(obj_dict["comp"]) - class DOF(): - def __init__(self, pairing, kernel, entity=None, attachment=None, target_space=None, g=None, immersed=False, generation=None, sub_id=None, cell=None): + def __init__(self, pairing, kernel, entity=None, attachment=None, target_space=None, g=None, immersed=False, generation=None, sub_id=None, cell=None, entity_o=False): self.pairing = pairing self.kernel = kernel self.immersed = immersed - self.trace_entity = entity + self.cell_defined_on = entity self.attachment = attachment self.target_space = target_space self.g = g self.id = None self.sub_id = sub_id self.cell = cell + self.entity_o = entity_o if generation is None: self.generation = {} @@ -300,9 +308,9 @@ def __init__(self, pairing, kernel, entity=None, attachment=None, target_space=N if entity is not None: self.pairing = self.pairing.add_entity(entity) - def __call__(self, g): + def __call__(self, g, entity_o=False): new_generation = self.generation.copy() - return DOF(self.pairing.permute(g), self.kernel.permute(g), self.trace_entity, self.attachment, self.target_space, g, self.immersed, new_generation, self.sub_id, self.cell) + return DOF(self.pairing.permute(g), self.kernel.permute(g), self.cell_defined_on, self.attachment, self.target_space, g, self.immersed, new_generation, self.sub_id, self.cell, entity_o) def eval(self, fn, pullback=True): return self.pairing(self.kernel, fn, self.cell) @@ -314,8 +322,9 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non # For some of these, we only want to store the first instance of each self.generation[cell.dim()] = dof_gen self.cell = cell - if self.trace_entity is None: + if self.cell_defined_on is None: self.trace_entity = cell + self.cell_defined_on = cell self.pairing = self.pairing.add_entity(cell) if self.target_space is None: self.target_space = space @@ -324,8 +333,34 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non if self.sub_id is None and generator_id is not None: self.sub_id = generator_id - def convert_to_fiat(self, ref_el, interpolant_degree): - return self.pairing.convert_to_fiat(ref_el, self, interpolant_degree) + def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): + # TODO deriv dict needs implementing (currently {}) + return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree), {}, str(self)) + + def to_quadrature(self, arg_degree): + Qpts, Qwts = self.cell_defined_on.quadrature(arg_degree) + Qwts = Qwts.reshape(Qwts.shape + (1,)) + dim = self.cell_defined_on.get_spatial_dimension() + if dim > 0 and self.pairing.orientation: + bvs = np.array(self.cell_defined_on.basis_vectors()) + new_bvs = np.array(self.cell_defined_on.orient(self.pairing.orientation).basis_vectors()) + basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) + else: + basis_change = np.eye(dim) + pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed) + + if self.immersed: + # need to compute jacobian from attachment. + pts = [self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts] + immersion = self.target_space.tabulate(wts, self.pairing.entity) + # Special case - force evaluation on different orientation of entity for construction of matrix transforms + if self.entity_o: + immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) + wts = np.outer(wts, immersion) + + # pt dict is { pt: (weight, component)} + pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, wts, comps)} + return pt_dict def __repr__(self, fn="v"): return str(self.pairing).format(fn=fn, kernel=self.kernel) @@ -348,34 +383,36 @@ def _from_dict(obj_dict): class ImmersedDOF(DOF): # probably need to add a convert to fiat method here to capture derivatives from immersion - def __init__(self, pairing, kernel, entity=None, attachment=None, target_space=None, g=None, triple=None, generation=None, sub_id=None, cell=None): + def __init__(self, pairing, kernel, entity=None, attachment=None, target_space=None, g=None, triple=None, generation=None, sub_id=None, cell=None, entity_o=False): self.immersed = True self.triple = triple - super(ImmersedDOF, self).__init__(pairing, kernel, entity=entity, attachment=attachment, target_space=target_space, g=g, immersed=True, generation=generation, sub_id=sub_id, cell=cell) + super(ImmersedDOF, self).__init__(pairing, kernel, entity=entity, attachment=attachment, target_space=target_space, g=g, immersed=True, generation=generation, sub_id=sub_id, cell=cell, entity_o=entity_o) def eval(self, fn, pullback=True): attached_fn = fn.attach(self.attachment) if pullback: - attached_fn = self.target_space(attached_fn, self.trace_entity, self.g) + attached_fn = self.target_space(attached_fn, self.cell_defined_on) return self.pairing(self.kernel, attached_fn, self.cell) def tabulate(self, Qpts): - immersion = self.target_space.tabulate(Qpts, self.trace_entity, self.g) - res = self.kernel.tabulate(Qpts, self.attachment) + # modify this to take reference space q pts + immersion = self.target_space.tabulate(Qpts, self.pairing.entity) + res, _ = self.kernel.tabulate(Qpts, self.attachment) return immersion*res - def __call__(self, g): - index_trace = self.cell.d_entities_ids(self.trace_entity.dim()).index(self.trace_entity.id) - permuted_e, permuted_g = self.cell.permute_entities(g, self.trace_entity.dim())[index_trace] - new_trace_entity = self.cell.get_node(permuted_e).orient(permuted_g) + def __call__(self, g, entity_o=False): + index_trace = self.cell.d_entities_ids(self.cell_defined_on.dim()).index(self.cell_defined_on.id) + permuted_e, permuted_g = self.cell.permute_entities(g, self.cell_defined_on.dim())[index_trace] + new_cell_defined_on = self.cell.get_node(permuted_e) + new_attach = lambda *x: g(self.attachment(*x)) - return ImmersedDOF(self.pairing.permute(permuted_g), self.kernel.permute(permuted_g), new_trace_entity, - new_attach, self.target_space, g, self.triple, self.generation, self.sub_id, self.cell) + return ImmersedDOF(self.pairing.permute(permuted_g), self.kernel.permute(permuted_g), new_cell_defined_on, + new_attach, self.target_space, g, self.triple, self.generation, self.sub_id, self.cell, entity_o) def __repr__(self): - fn = "tr_{1}_{0}(v)".format(str(self.trace_entity), str(self.target_space)) + fn = "tr_{1}_{0}(v)".format(str(self.cell_defined_on), str(self.target_space)) return super(ImmersedDOF, self).__repr__(fn) def immerse(self, entity, attachment, trace, g): diff --git a/fuse/groups.py b/fuse/groups.py index a08d96e0..32298de3 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -9,12 +9,12 @@ def perm_matrix_to_perm_array(p_mat): summed = np.sum(p_mat, axis=0) - if np.all(summed == np.zeros_like(summed)): + if np.allclose(summed, np.zeros_like(summed)): return list(np.zeros_like(summed)) - assert np.all(summed == np.ones_like(summed)) + assert np.allclose(summed, np.ones_like(summed)) res = [] for row in p_mat: - indices = list(row).index(1) + indices = list(np.isclose(row, 1)).index(True) res += [indices] return res @@ -78,7 +78,11 @@ def __hash__(self): def __mul__(self, x): assert isinstance(x, GroupMemberRep) - return self.group.get_member(self.perm * x.perm) + max_size = max(self.perm.size, x.perm.size) + larger_group = self.group if len(self.group.members()) >= len(x.group.members()) else x.group + x_perm = Permutation(x.perm, size=max_size) + self_perm = Permutation(self.perm, size=max_size) + return larger_group.get_member(self_perm * x_perm) def __invert__(self): return self.group.get_member(~self.perm) @@ -165,12 +169,11 @@ def get_member(self, perm): return m raise ValueError("Permutation not a member of group") - def get_member_by_val(self, val): for m in self.members(): if m.numeric_rep() == val: return m - + raise ValueError("Value does not represent a group member") def compute_num_reps(self, base_val=0): @@ -218,6 +221,8 @@ def __init__(self, base_group, cell=None): if cell is not None: self.cell = cell vertices = cell.vertices(return_coords=True) + verts = cell.ordered_vertices() + vertices = [cell.get_node(v, return_coords=True) for v in verts] self._members = [] counter = 0 @@ -280,12 +285,13 @@ def transform_between_perms(self, perm1, perm2): assert perm2 in member_perms return ~self.get_member(Permutation(perm1)) * self.get_member(Permutation(perm2)) - def get_member(self, perm): - for m in self.members(): - if m.perm == perm: - return m - raise ValueError("Permutation not a member of group") - + # def get_member(self, perm): + # if not isinstance(perm, Permutation): + # perm = Permutation.from_sequence(perm) + # for m in self.members(): + # if m.perm == perm: + # return m + # raise ValueError("Permutation not a member of group") # def compute_reps(self, g, path, remaining_members): # # breadth first search to find generator representations of all members diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index c05844a1..422cf053 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -148,8 +148,10 @@ def __init__(self, weights, spaces): self.weights = weights self.spaces = spaces - maxdegree = max([space.maxdegree for space in spaces]) - mindegree = min([space.mindegree for space in spaces]) + weight_degrees = [0 if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)) else max_deg_sp_mat(w) for w in self.weights] + + maxdegree = max([space.maxdegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) + mindegree = min([space.mindegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) vec = any([s.set_shape for s in spaces]) super(ConstructedPolynomialSpace, self).__init__(maxdegree, -1, mindegree, set_shape=vec) diff --git a/fuse/traces.py b/fuse/traces.py index 2f77606b..a699eaae 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -9,13 +9,13 @@ class Trace(): def __init__(self, cell): self.domain = cell - def __call__(self, trace_entity, g): + def __call__(self, trace_entity): raise NotImplementedError("Trace uninstanitated") - def plot(self, ax, coord, trace_entity, g, **kwargs): + def plot(self, ax, coord, trace_entity, **kwargs): raise NotImplementedError("Trace uninstanitated") - def tabulate(self, Qpts, trace_entity, g): + def tabulate(self, Qwts, trace_entity): raise NotImplementedError("Tabulation uninstantiated") def _to_dict(self): @@ -45,17 +45,17 @@ class TrH1(Trace): def __init__(self, cell): super(TrH1, self).__init__(cell) - def __call__(self, v, trace_entity, g): + def __call__(self, v, trace_entity): return v - def plot(self, ax, coord, trace_entity, g, **kwargs): + def plot(self, ax, coord, trace_entity, **kwargs): ax.scatter(*coord, **kwargs) - def to_tikz(self, coord, trace_entity, g, scale, color="black"): + def to_tikz(self, coord, trace_entity, scale, color="black"): return f"\\filldraw[{color}] {numpy_to_str_tuple(coord, scale)} circle (2pt) node[anchor = south] {{}};" - def tabulate(self, Qpts, trace_entity, g): - return np.ones_like(Qpts) + def tabulate(self, Qwts, trace_entity): + return Qwts def __repr__(self): return "H1" @@ -66,24 +66,25 @@ class TrHDiv(Trace): def __init__(self, cell): super(TrHDiv, self).__init__(cell) - def __call__(self, v, trace_entity, g): + def __call__(self, v, trace_entity): def apply(*x): - result = np.dot(self.tabulate(None, trace_entity, g), np.array(v(*x)).squeeze()) + result = np.dot(self.tabulate(None, trace_entity), np.array(v(*x)).squeeze()) if isinstance(result, np.float64): # todo: might always be a float return (result,) return tuple(result) return apply - def plot(self, ax, coord, trace_entity, g, **kwargs): + def plot(self, ax, coord, trace_entity, **kwargs): # plot dofs of the type associated with this space - vec = self.tabulate([], trace_entity, g).squeeze() + vec = self.tabulate([], trace_entity).squeeze() ax.quiver(*coord, *vec, **kwargs) - def tabulate(self, Qpts, trace_entity, g): - entityBasis = np.array(trace_entity.basis_vectors()) + def tabulate(self, Qwts, trace_entity): + # entityBasis = np.array(trace_entity.basis_vectors()) cellEntityBasis = np.array(self.domain.basis_vectors(entity=trace_entity)) - basis = np.matmul(entityBasis, cellEntityBasis) + # basis = np.matmul(entityBasis, cellEntityBasis) + basis = cellEntityBasis if trace_entity.dimension == 1: result = np.matmul(basis, np.array([[0, -1], [1, 0]])) @@ -94,8 +95,8 @@ def tabulate(self, Qpts, trace_entity, g): return result - def to_tikz(self, coord, trace_entity, g, scale, color="black"): - vec = self.tabulate([], trace_entity, g).squeeze() + def to_tikz(self, coord, trace_entity, scale, color="black"): + vec = self.tabulate([], trace_entity).squeeze() end_point = [coord[i] + 0.25*vec[i] for i in range(len(coord))] arw = "-{Stealth[length=3mm, width=2mm]}" return f"\\draw[thick, {color}, {arw}] {numpy_to_str_tuple(coord, scale)} -- {numpy_to_str_tuple(end_point, scale)};" @@ -109,26 +110,26 @@ class TrHCurl(Trace): def __init__(self, cell): super(TrHCurl, self).__init__(cell) - def __call__(self, v, trace_entity, g): + def __call__(self, v, trace_entity): def apply(*x): - result = np.dot(self.tabulate(None, trace_entity, g), np.array(v(*x)).squeeze()) + result = np.dot(self.tabulate(None, trace_entity), np.array(v(*x)).squeeze()) if isinstance(result, np.float64): return (result,) return tuple(result) return apply - def tabulate(self, Qpts, trace_entity, g): - tangent = np.array(trace_entity.basis_vectors()) + def tabulate(self, Qwts, trace_entity): + # tangent = trace_entity.basis_vectors() subEntityBasis = np.array(self.domain.basis_vectors(entity=trace_entity)) - result = np.matmul(tangent, subEntityBasis) - return result + # result = np.matmul(tangent, subEntityBasis) + return subEntityBasis - def plot(self, ax, coord, trace_entity, g, **kwargs): - vec = self.tabulate([], trace_entity, g).squeeze() + def plot(self, ax, coord, trace_entity, **kwargs): + vec = self.tabulate([], trace_entity).squeeze() ax.quiver(*coord, *vec, **kwargs) - def to_tikz(self, coord, trace_entity, g, scale, color="black"): - vec = self.tabulate([], trace_entity, g).squeeze() + def to_tikz(self, coord, trace_entity, scale, color="black"): + vec = self.tabulate([], trace_entity).squeeze() end_point = [coord[i] + 0.25*vec[i] for i in range(len(coord))] arw = "-{Stealth[length=3mm, width=2mm]}" return f"\\draw[thick, {color}, {arw}] {numpy_to_str_tuple(coord, scale)} -- {numpy_to_str_tuple(end_point, scale)};" @@ -142,8 +143,10 @@ class TrGrad(Trace): def __init__(self, cell): super(TrGrad, self).__init__(cell) - def __call__(self, v, trace_entity, g): + def __call__(self, v, trace_entity): # Compute grad v and then dot with tangent rotated according to the group member + raise NotImplementedError("Gradient immersions are under development") + g = None tangent = np.array(g(np.array(self.domain.basis_vectors())[0])) def apply(*x): @@ -159,11 +162,11 @@ def apply(*x): return tuple(result) return apply - def plot(self, ax, coord, trace_entity, g, **kwargs): + def plot(self, ax, coord, trace_entity, **kwargs): circle1 = plt.Circle(coord, 0.075, fill=False, **kwargs) ax.add_patch(circle1) - def to_tikz(self, coord, trace_entity, g, scale, color="black"): + def to_tikz(self, coord, trace_entity, scale, color="black"): return f"\\draw[{color}] {numpy_to_str_tuple(coord, scale)} circle (4pt) node[anchor = south] {{}};" def __repr__(self): @@ -175,7 +178,9 @@ class TrHess(Trace): def __init__(self, cell): super(TrHess, self).__init__(cell) - def __call__(self, v, trace_entity, g): + def __call__(self, v, trace_entity): + raise NotImplementedError("Hessian trace needs reviewing") + g = None b0, b1 = self.domain.basis_vectors() tangent0 = np.array(g(b0)) tangent1 = np.array(g(b1)) @@ -192,11 +197,11 @@ def apply(*x): return tuple(result) return apply - def plot(self, ax, coord, trace_entity, g, **kwargs): + def plot(self, ax, coord, trace_entity, **kwargs): circle1 = plt.Circle(coord, 0.15, fill=False, **kwargs) ax.add_patch(circle1) - def to_tikz(self, coord, trace_entity, g, scale, color="black"): + def to_tikz(self, coord, trace_entity, scale, color="black"): return f"\\draw[{color}] {numpy_to_str_tuple(coord, scale)} circle (6pt) node[anchor = south] {{}};" def __repr__(self): diff --git a/fuse/triples.py b/fuse/triples.py index 3d4e3f72..a4557a86 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -48,6 +48,37 @@ def __init__(self, cell, spaces, dof_gen): self.DOFGenerator = dof_gen self.flat = False + def setup_ids_and_nodes(self): + dofs = self.generate() + degree = self.spaces[0].degree() + value_shape = self.get_value_shape() + top = self.ref_el.get_topology() + min_ids = self.cell.get_starter_ids() + entity_ids = {} + nodes = [] + + for dim in sorted(top): + entity_ids[dim] = {i: [] for i in top[dim]} + + entities = [(dim, entity) for dim in sorted(top) for entity in sorted(top[dim])] + counter = 0 + for entity in entities: + dim = entity[0] + for i in range(len(dofs)): + if entity[1] == dofs[i].cell_defined_on.id - min_ids[dim]: + entity_ids[dim][dofs[i].cell_defined_on.id - min_ids[dim]].append(counter) + nodes.append(dofs[i].convert_to_fiat(self.ref_el, degree, value_shape)) + counter += 1 + return entity_ids, nodes + + def setup_matrices(self): + self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + matrices, _, _ = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + reversed_matrices = self.reverse_dof_perms(matrices) + self.apply_matrices = True + self.entity_perms = False + return matrices, reversed_matrices + def __repr__(self): return "FuseTriple(%s, %s, (%s, %s, %s), %s)" % ( repr(self.DOFGenerator), repr(self.cell), repr(self.spaces[0]), repr(self.spaces[1]), repr(self.spaces[2]), "X") @@ -77,16 +108,16 @@ def degree(self): def get_dof_info(self, dof, tikz=True): colours = {False: {0: "b", 1: "r", 2: "g", 3: "b"}, True: {0: "blue", 1: "red", 2: "green", 3: "black"}} - if dof.trace_entity.dimension == 0: - center = self.cell.cell_attachment(dof.trace_entity.id)() - elif dof.trace_entity.dimension == 1: - center = self.cell.cell_attachment(dof.trace_entity.id)(0) - elif dof.trace_entity.dimension == 2: - center = self.cell.cell_attachment(dof.trace_entity.id)(0, 0) + if dof.cell_defined_on.dimension == 0: + center = self.cell.cell_attachment(dof.cell_defined_on.id)() + elif dof.cell_defined_on.dimension == 1: + center = self.cell.cell_attachment(dof.cell_defined_on.id)(0) + elif dof.cell_defined_on.dimension == 2: + center = self.cell.cell_attachment(dof.cell_defined_on.id)(0, 0) else: center = list(sum(np.array(self.cell.vertices(return_coords=True)))) - return center, colours[tikz][dof.trace_entity.dimension] + return center, colours[tikz][dof.cell_defined_on.dimension] def get_value_shape(self): # TODO Shape should be specificed somewhere else probably @@ -96,48 +127,21 @@ def get_value_shape(self): return () def to_ufl(self): + # set up for eventual conversion to FIAT + self.ref_el = self.cell.to_fiat() + self.poly_set = self.spaces[0].to_ON_polynomial_set(self.ref_el) + self.entity_ids, self.nodes = self.setup_ids_and_nodes() + self.matrices, self.reversed_matrices = self.setup_matrices() return FuseElement(self) def to_fiat(self): - ref_el = self.cell.to_fiat() - dofs = self.generate() - degree = self.spaces[0].degree() - entity_ids = {} - entity_perms = {} - nodes = [] - top = ref_el.get_topology() - min_ids = self.cell.get_starter_ids() - poly_set = self.spaces[0].to_ON_polynomial_set(ref_el) - #from FIAT.nedelec import Nedelec - #poly_set = Nedelec(ref_el, 2).poly_set - - for dim in sorted(top): - entity_ids[dim] = {i: [] for i in top[dim]} - entity_perms[dim] = {} - - entities = [(dim, entity) for dim in sorted(top) for entity in sorted(top[dim])] - counter = 0 - for entity in entities: - dim = entity[0] - for i in range(len(dofs)): - if entity[1] == dofs[i].trace_entity.id - min_ids[dim]: - entity_ids[dim][dofs[i].trace_entity.id - min_ids[dim]].append(counter) - nodes.append(dofs[i].convert_to_fiat(ref_el, degree)) - counter += 1 - self.matrices_by_entity = self.make_entity_dense_matrices(ref_el, entity_ids, nodes, poly_set) - mat_perms, entity_perms, pure_perm = self.make_dof_perms(ref_el, entity_ids, nodes, poly_set) - if not pure_perm: - self.matrices = mat_perms - self.reverse_dof_perms() - self.orient_mat_perms() + # call this to ensure set up is complete + self.to_ufl() form_degree = 1 if self.spaces[0].set_shape else 0 + degree = self.spaces[0].degree() - # TODO: Change this when Dense case in Firedrake - if entity_perms is not None: - dual = DualSet(nodes, ref_el, entity_ids, entity_perms) - else: - dual = DualSet(nodes, ref_el, entity_ids) - return CiarletElement(poly_set, dual, degree, form_degree) + dual = DualSet(self.nodes, self.ref_el, self.entity_ids) + return CiarletElement(self.poly_set, dual, degree, form_degree) def to_tikz(self, show=True, scale=3): """Generates tikz code for the element diagram @@ -156,12 +160,12 @@ def to_tikz(self, show=True, scale=3): if isinstance(dof.pairing, DeltaPairing): coord = dof.eval(identity, pullback=False) if isinstance(dof.target_space, Trace): - tikz_commands += [dof.target_space.to_tikz(coord, dof.trace_entity, dof.g, scale, color)] + tikz_commands += [dof.target_space.to_tikz(coord, dof.cell_defined_on, scale, color)] else: tikz_commands += [f"\\filldraw[{color}] {numpy_to_str_tuple(coord, scale)} circle (2pt) node[anchor = south] {{}};"] elif isinstance(dof.pairing, L2Pairing): coord = center - tikz_commands += [dof.target_space.to_tikz(coord, dof.trace_entity, dof.g, scale, color)] + tikz_commands += [dof.target_space.to_tikz(coord, dof.cell_defined_on, scale, color)] if show: tikz_commands += ['\\end{tikzpicture}'] return "\n".join(tikz_commands) @@ -189,7 +193,7 @@ def plot(self, filename="temp.png"): if len(coord) == 1: coord = (coord[0], 0) if isinstance(dof.target_space, Trace): - dof.target_space.plot(ax, coord, dof.trace_entity, dof.g, color=color) + dof.target_space.plot(ax, coord, dof.cell_defined_on, color=color) else: ax.scatter(*coord, color=color) ax.text(*coord, dof.id) @@ -211,12 +215,12 @@ def plot(self, filename="temp.png"): if isinstance(dof.pairing, DeltaPairing): coord = dof.eval(identity, pullback=False) if isinstance(dof.target_space, Trace): - dof.target_space.plot(ax, coord, dof.trace_entity, dof.g, color=color) + dof.target_space.plot(ax, coord, dof.cell_defined_on, color=color) else: ax.scatter(*coord, color=color) elif isinstance(dof.pairing, L2Pairing): coord = center - dof.target_space.plot(ax, center, dof.trace_entity, dof.g, color=color, length=0.2) + dof.target_space.plot(ax, center, dof.cell_defined_on, color=color, length=0.2) ax.text(*coord, dof.id) plt.axis('off') ax.get_xaxis().set_visible(False) @@ -250,7 +254,7 @@ def compute_dense_matrix(self, ref_el, entity_ids, nodes, poly_set): def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): degree = self.spaces[0].degree() min_ids = self.cell.get_starter_ids() - nodes = [d.convert_to_fiat(ref_el, degree) for d in self.generate()] + nodes = [d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] sub_ents = [] res_dict = {} for d in range(0, self.cell.dim() + 1): @@ -260,7 +264,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): dim = e.dim() e_id = e.id - min_ids[dim] res_dict[dim][e_id] = {} - dof_ids = [d.id for d in self.generate() if d.trace_entity == e] + dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] res_dict[dim][e_id][0] = np.eye(len(dof_ids)) original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) @@ -273,9 +277,10 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): elif g.perm.is_Identity: res_dict[dim][e_id][val] = np.eye(len(dof_ids)) else: - new_nodes = [d(g).convert_to_fiat(ref_el, degree) if d.trace_entity == e else d.convert_to_fiat(ref_el, degree) for d in self.generate()] + new_nodes = [d(g, entity_o=permuted_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) res_dict[dim][e_id][val] = np.matmul(transformed_basis, original_V.T)[np.ix_(dof_ids, dof_ids)] + return res_dict def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): @@ -291,7 +296,7 @@ def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): if g.perm.is_Identity: res_dict[dim][e_id][val] = np.eye(len(nodes)) else: - new_nodes = [d(g).convert_to_fiat(ref_el, degree) for d in self.generate()] + new_nodes = [d(g).convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) res_dict[dim][e_id][val] = np.matmul(transformed_basis, original_V.T) return res_dict @@ -307,8 +312,8 @@ def _entity_associations(self, dofs): # construct mapping of entities to the dof generators and the dofs they generate for d in dofs: - sub_dim = d.trace_entity.dim() - sub_dict = entity_associations[sub_dim][d.trace_entity.id - min_ids[sub_dim]] + sub_dim = d.cell_defined_on.dim() + sub_dict = entity_associations[sub_dim][d.cell_defined_on.id - min_ids[sub_dim]] for dim in set([sub_dim, cell_dim]): dof_gen = str(d.generation[dim]) @@ -353,9 +358,10 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): dofs = self.generate() min_ids = self.cell.get_starter_ids() entity_associations, pure_perm, sub_pure_perm = self._entity_associations(dofs) - if pure_perm is False: - # TODO think about where this call goes - return self.make_overall_dense_matrices(ref_el, entity_ids, nodes, poly_set), None, pure_perm + # if pure_perm is False: + # #TODO think about where this call goes + # return self.matrices_by_entity, None, pure_perm + # return self.make_overall_dense_matrices(ref_el, entity_ids, nodes, poly_set), None, pure_perm oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(dofs) # for each entity, look up generation on that entity and permute the @@ -374,8 +380,9 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): # (dof_gen, ent_dofs) total_ent_dof_ids += [ed.id for ed in ent_dofs if ed.id not in total_ent_dof_ids] dof_gen_class = ent_dofs[0].generation - if not len(dof_gen_class[dim].g2.members()) == 1: + if not len(dof_gen_class[dim].g2.members()) == 1 and dim == min(dof_gen_class.keys()): # if DOFs on entity are not perms, get the matrix + # only get this if they are defined on the current dimension oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) @@ -389,11 +396,10 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() else: # TODO what if an orientation is not in G1 - #warnings.warn("FUSE: should probably be using equivalent members of g in g1 for this") - #sub_mat = g.matrix_form() - #oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - - #raise NotImplementedError(f"Orientation {g} is not in group {dof_gen_class[dim].g1.members()}") + warnings.warn("FUSE: orientation case not covered") + # sub_mat = g.matrix_form() + # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + # raise NotImplementedError(f"Orientation {g} is not in group {dof_gen_class[dim].g1.members()}") pass if len(dof_gen_class.keys()) == 2 and dim == self.cell.dim(): @@ -421,14 +427,15 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): # remove immersed DOFs from flat form oriented_mats_overall = oriented_mats_by_entity[dim][0] - for val, mat in oriented_mats_overall.items(): - cell_dofs = entity_ids[dim][0] - flat_by_entity[dim][e_id][val] = perm_matrix_to_perm_array(mat[np.ix_(cell_dofs, cell_dofs)]) if pure_perm and sub_pure_perm: + for val, mat in oriented_mats_overall.items(): + cell_dofs = entity_ids[dim][0] + flat_by_entity[dim][e_id][val] = perm_matrix_to_perm_array(mat[np.ix_(cell_dofs, cell_dofs)]) return oriented_mats_by_entity, flat_by_entity, True return oriented_mats_by_entity, None, False def orient_mat_perms(self): + raise NotImplementedError("This should not be necessary") min_ids = self.cell.get_starter_ids() entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname()).get_topology(), self.cell.get_topology()) num_ents = 0 @@ -436,8 +443,7 @@ def orient_mat_perms(self): ents = self.cell.d_entities(dim) for e in ents: e_id = e.id - min_ids[dim] - members = e.group.members() - if entity_orientations[num_ents + e_id] != 0 and dim < self.cell.dim(): + if entity_orientations[num_ents + e_id] != 0: modifier = self.matrices[dim][e_id][entity_orientations[num_ents+e_id]] reverse_modifier_val = (~e.group.get_member_by_val(entity_orientations[num_ents+e_id])).numeric_rep() reverse_modifier = self.matrices[dim][e_id][reverse_modifier_val] @@ -451,20 +457,20 @@ def orient_mat_perms(self): self.reversed_matrices[dim][e_id] = perms_copy num_ents += len(ents) - def reverse_dof_perms(self): + def reverse_dof_perms(self, matrices): min_ids = self.cell.get_starter_ids() reversed_mats = {} - for dim in self.matrices.keys(): + for dim in matrices.keys(): reversed_mats[dim] = {} ents = self.cell.d_entities(dim) for e in ents: e_id = e.id - min_ids[dim] - perms_copy = self.matrices[dim][e_id].copy() + perms_copy = matrices[dim][e_id].copy() members = e.group.members() for m in members: - perms_copy[m.numeric_rep()] = self.matrices[dim][e_id][(~m).numeric_rep()] + perms_copy[m.numeric_rep()] = matrices[dim][e_id][(~m).numeric_rep()] reversed_mats[dim][e_id] = perms_copy - self.reversed_matrices = reversed_mats + return reversed_mats def _to_dict(self): o_dict = {"cell": self.cell, "spaces": self.spaces, "dofs": self.DOFGenerator} @@ -531,7 +537,7 @@ def make_entity_ids(self): entity_ids[dim] = {i: [] for i in top[dim]} for i in range(len(dofs)): - entity = dofs[i].trace_entity + entity = dofs[i].cell_defined_on dim = entity.dim() entity_ids[dim][entity.id - min_ids[dim]].append(i) return entity_ids diff --git a/fuse/utils.py b/fuse/utils.py index 63d1e485..fcfa3d9e 100644 --- a/fuse/utils.py +++ b/fuse/utils.py @@ -17,7 +17,8 @@ def fold_reduce(func_list, *prev): def sympy_to_numpy(array, symbols, values): """ - Convert a sympy array to a numpy array. + TODO: rename this function + Evaluate symbols at values, then convert to numpy if all have been replaced :param: array: sympy array :param: symbols: array of symbols contained in the sympy exprs @@ -27,13 +28,17 @@ def sympy_to_numpy(array, symbols, values): is greater than 1 dimension to remove extra dimensions """ substituted = array.subs({symbols[i]: values[i] for i in range(len(values))}) - nparray = np.array(substituted).astype(np.float64) - if len(nparray.shape) > 1: - return nparray.squeeze() + if len(array.atoms(sp.Symbol)) == len(values) and all(not isinstance(v, sp.Expr) for v in values): + nparray = np.array(substituted).astype(np.float64) - if len(nparray.shape) == 0: - return nparray.item() + if len(nparray.shape) > 1: + return nparray.squeeze() + + if len(nparray.shape) == 0: + return nparray.item() + else: + nparray = substituted return nparray diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index de6be11b..a6af1e96 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -165,8 +165,8 @@ def construct_nd(tri=None): y = sp.Symbol("y") # xs = [DOF(L2Pairing(), PointKernel(edge.basis_vectors()[0]))] - #xs = [DOF(L2Pairing(), PointKernel((np.sqrt(2),)))] - xs = [DOF(L2Pairing(), PolynomialKernel((1,)))] + # xs = [DOF(L2Pairing(), PointKernel((1,)))] + xs = [DOF(L2Pairing(), VectorKernel((1,)))] dofs = DOFGenerator(xs, S1, S2) int_ned = ElementTriple(edge, (P1, CellHCurl, C0), dofs) @@ -216,7 +216,7 @@ def construct_rt(tri=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), PolynomialKernel(1))] + xs = [DOF(L2Pairing(), VectorKernel((1,)))] dofs = DOFGenerator(xs, S1, S2) int_rt = ElementTriple(edge, (vec_Pd, CellHDiv, C0), dofs) @@ -245,6 +245,7 @@ def test_rt_example(): for dof in rt.generate(): assert [np.allclose(1, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 1 assert [np.allclose(0, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 2 + rt.to_fiat() def construct_hermite(): @@ -270,19 +271,19 @@ def construct_hermite(): [v_dofs, v_derv_dofs, v_derv2_dofs, i_dofs]) return her - -def test_hermite_example(): - her = construct_hermite() - - # TODO improve this test - x = sp.Symbol("x") - y = sp.Symbol("y") - phi_0 = FuseFunction(x**2 + 3*y**3 + 4*x*y, symbols=(x, y)) - ls = her.generate() - print("num dofs ", her.num_dofs()) - for dof in ls: - print(dof) - print("dof eval", dof.eval(phi_0)) +# draft of hermite test, immersions need work +# def test_hermite_example(): +# her = construct_hermite() +# +# # TODO improve this test +# x = sp.Symbol("x") +# y = sp.Symbol("y") +# phi_0 = FuseFunction(x**2 + 3*y**3 + 4*x*y, symbols=(x, y)) +# ls = her.generate() +# print("num dofs ", her.num_dofs()) +# for dof in ls: +# print(dof) +# print("dof eval", dof.eval(phi_0)) def test_square_cg(): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 9123a5a3..10333b96 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -1,4 +1,6 @@ from fuse import * +import pytest +from sympy.combinatorics import Permutation import sympy as sp import numpy as np @@ -93,7 +95,7 @@ def construct_tet_rt(cell=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), PolynomialKernel((1, 1, 1)))] + xs = [DOF(L2Pairing(), PolynomialKernel(1))] dofs = DOFGenerator(xs, S1, S3) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) @@ -106,6 +108,38 @@ def construct_tet_rt(cell=None): return rt1 +def construct_tet_ned(cell=None): + deg = 1 + tri = make_tetrahedron() + edge = tri.edges()[0] + + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + + # [test_tet_ned 0] + xs = [DOF(L2Pairing(), PolynomialKernel(1))] + dofs = DOFGenerator(xs, S1, S2) + + edges = ElementTriple(edge, (vec_Pd, CellHCurl, L2), dofs) + xs = [immerse(tri, edges, TrHCurl)] + tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), + Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), + Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) + edge_dofs = DOFGenerator(xs, tet_edges, S1) + # [test_tet_ned 1] + + return ElementTriple(tri, (nd_space, CellHCurl, L2), [edge_dofs]) + + def plot_tet_rt(): rt = construct_tet_rt() rt.plot() @@ -118,34 +152,20 @@ def test_tet_rt(): # TODO make this a proper test for dof in ls: print(dof) + rt1.to_fiat() -def construct_tet_ned(): +@pytest.mark.xfail(reason='DOFs not forming full rank matrix') +def test_tet_nd(): tetra = make_tetrahedron() - edge = tetra.edges()[0] - # [test_tet_ned 0] - xs = [DOF(L2Pairing(), PolynomialKernel(1))] - dofs = DOFGenerator(xs, S1, S2) - int_ned = ElementTriple(edge, (P1, CellHCurl, "C0"), dofs) - - im_xs = [immerse(tetra, int_ned, TrHCurl)] - edge = DOFGenerator(im_xs, tet_edges, Z4) - - ned = ElementTriple(tetra, (P1, CellHCurl, "C0"), - [edge]) - # [test_tet_ned 1] - - return ned + nd1 = construct_tet_ned(tetra) + ls = nd1.generate() + # TODO make this a proper test + for dof in ls: + print(dof) + nd1.to_fiat() def plot_tet_ned(): ned = construct_tet_ned() ned.plot() - - -def test_tet_ned(): - ned = construct_tet_ned() - ls = ned.generate() - # TODO make this a proper test - for dof in ls: - print(dof) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 128a53e1..99fb1cf8 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -8,7 +8,6 @@ from test_3d_examples_docs import construct_tet_rt from test_polynomial_space import flatten from element_examples import CR_n -from firedrake.__future__ import interpolate def create_dg0(cell): @@ -161,7 +160,7 @@ def create_cg2_tri(cell): vert_dg0 = create_dg0(cell.vertices()[0]) xs = [immerse(cell, vert_dg0, TrH1)] - edge_dg0 = ElementTriple(cell.edges(get_class=True)[0], (Pk, CellL2, C0), DOFGenerator([DOF(DeltaPairing(), PointKernel((0,)))], S1, S1)) + edge_dg0 = ElementTriple(cell.edges(get_class=True)[0], (PolynomialSpace(0), CellL2, C0), DOFGenerator([DOF(DeltaPairing(), PointKernel((0,)))], S1, S1)) edge_xs = [immerse(cell, edge_dg0, TrH1)] cg = ElementTriple(cell, (Pk, CellL2, C0), [DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1), @@ -352,7 +351,6 @@ def test_entity_perms(elem_gen, cell): elem.to_fiat() dim = cell.get_spatial_dimension() - for i in elem.matrices[dim][0].keys(): print(elem.matrices[dim][0][i]) @@ -377,7 +375,7 @@ def test_immersed_entity_perms(elem_gen, cell, expected): def test_1d(elem_gen, elem_code, deg): cell = Point(1, [Point(0), Point(0)], vertex_num=2) elem = elem_gen(cell) - scale_range = range(3, 6) + scale_range = range(1, 6) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] @@ -386,11 +384,11 @@ def test_1d(elem_gen, elem_code, deg): V = FunctionSpace(mesh, elem_code, deg) res1 = helmholtz_solve(V, mesh) - diff2[i-4] = res1 + diff2[i-min(scale_range)] = res1 V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = helmholtz_solve(V2, mesh) - diff[i-4] = res2 + diff[i-min(scale_range)] = res2 assert np.allclose(res1, res2) print("firedrake l2 error norms:", diff2) @@ -414,11 +412,11 @@ def test_helmholtz_2d(elem_gen, elem_code, deg, conv_rate): V = FunctionSpace(mesh, elem_code, deg) res1 = helmholtz_solve(V, mesh) - diff2[i-3] = res1 + diff2[i-min(scale_range)] = res1 V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = helmholtz_solve(V2, mesh) - diff[i-3] = res2 + diff[i-min(scale_range)] = res2 assert np.allclose(res1, res2) print("firedrake l2 error norms:", diff2) @@ -439,7 +437,7 @@ def test_helmholtz_2d(elem_gen, elem_code, deg, conv_rate): def test_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) - scale_range = range(2, 4) + scale_range = range(3, 6) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] for i in scale_range: @@ -447,11 +445,11 @@ def test_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): V = FunctionSpace(mesh, elem_code, deg) res1 = helmholtz_solve(V, mesh) - diff2[i - 2] = res1 + diff2[i - min(scale_range)] = res1 V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = helmholtz_solve(V2, mesh) - diff[i - 2] = res2 + diff[i - min(scale_range)] = res2 assert np.allclose(res1, res2) print("firedrake l2 error norms:", diff2) @@ -470,7 +468,7 @@ def test_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): def helmholtz_solve(V, mesh): # Define variational problem - dim = mesh.ufl_cell().topological_dimension() + dim = mesh.ufl_cell().topological_dimension u = TrialFunction(V) v = TestFunction(V) f = Function(V) @@ -480,6 +478,7 @@ def helmholtz_solve(V, mesh): f.interpolate((1+8*pi*pi)*cos(x[0]*pi*2)) expect.interpolate(cos(x[0]*pi*2)) elif dim == 2: + # f.interpolate(x[0]*10 + x[1]) f.interpolate((1+8*pi*pi)*cos(x[0]*pi*2)*cos(x[1]*pi*2)) expect.interpolate(cos(x[0]*pi*2)*cos(x[1]*pi*2)) elif dim == 3: @@ -491,12 +490,19 @@ def helmholtz_solve(V, mesh): a = (inner(grad(u), grad(v)) + inner(u, v)) * dx L = inner(f, v) * dx - # l_a = assemble(L) # elem = V.finat_element.fiat_equivalent # W = VectorFunctionSpace(mesh, V.ufl_element()) # X = assemble(interpolate(mesh.coordinates, W)) # print(X.dat.data) + np.set_printoptions(precision=4, suppress=True) + print() # print(assemble(a).M.values) + l_a = assemble(L) + print(l_a.dat.data[0:2]) + print(V.cell_node_list[0:2]) + # print(mesh.entity_orientations) + + # breakpoint() # Compute solution sol = Function(V) @@ -594,10 +600,10 @@ def test_project_vec(elem_gen, elem_code, deg): mesh = UnitTriangleMesh() U = FunctionSpace(mesh, elem_code, deg) - assert np.allclose(project(U, mesh, as_vector((1,1))), 0, rtol=1e-5) + assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) U = FunctionSpace(mesh, elem.to_ufl()) - assert np.allclose(project(U, mesh, as_vector((1,1))), 0, rtol=1e-5) + assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_dg1_tet, "DG", 1)]) diff --git a/test/test_dofs.py b/test/test_dofs.py index 5b56f430..068c6dfc 100644 --- a/test/test_dofs.py +++ b/test/test_dofs.py @@ -1,5 +1,7 @@ from fuse import * from test_convert_to_fiat import create_cg1, create_dg1, construct_cg3, construct_rt, construct_nd +from test_orientations import construct_nd2 + import sympy as sp import numpy as np @@ -75,12 +77,33 @@ def test_permute_nd(): # print(dof) for g in nd.cell.group.members(): - print("g:", g.numeric_rep()) + print("g:", g, g.numeric_rep()) for dof in nd.generate(): print(dof(g).convert_to_fiat(cell.to_fiat(), 0).pt_dict) print(dof, "->", dof(g), "eval, ", dof(g).eval(func)) +def test_permute_nd2(): + cell = polygon(3) + + nd = construct_nd2(cell) + x = sp.Symbol("x") + y = sp.Symbol("y") + func = FuseFunction(sp.Matrix([x, -1/3 + 2*y]), symbols=(x, y)) + + # for dof in nd.generate(): + # print(dof) + + for g in nd.cell.group.members(): + print("g:", g.numeric_rep()) + if g.numeric_rep() == 2: + for i, dof in enumerate(nd.generate()): + if 0 < i < 2: + print(dof(g).convert_to_fiat(cell.to_fiat(), 2, (2,)).pt_dict) + print(dof, "->", dof(g), "eval, ", dof(g).eval(func)) + breakpoint() + + def test_permute_nd_old(): cell = polygon(3) @@ -138,3 +161,66 @@ def test_permute_nodes(): print([n.pt_dict for n in nodes]) for g in cg1.cell.group.members(): print(g, [d(g).convert_to_fiat(cell.to_fiat(), degree).pt_dict for d in cg1.generate()]) + + +# def test_edge_parametrisation(): +# tri = polygon(3) +# for i in tri.d_entities(1): +# print(tri.generate_facet_parameterisation(i.id)) +# from fuse.dof import ParameterisationKernel +# +# deg = 2 +# edge = tri.edges()[0] +# x = sp.Symbol("x") +# y = sp.Symbol("y") +# +# xs = [DOF(L2Pairing(), ParameterisationKernel())] +# +# dofs = DOFGenerator(xs, S2, S2) +# int_ned1 = ElementTriple(edge, (P1, CellHCurl, C0), dofs) +# +# xs = [DOF(L2Pairing(), ComponentKernel((0,))), +# DOF(L2Pairing(), ComponentKernel((1,)))] +# center_dofs = DOFGenerator(xs, S1, S3) +# xs = [immerse(tri, int_ned1, TrHCurl)] +# tri_dofs = DOFGenerator(xs, C3, S1) +# +# vec_Pk = PolynomialSpace(deg - 1, set_shape=True) +# Pk = PolynomialSpace(deg - 1) +# M = sp.Matrix([[y, -x]]) +# nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M +# +# ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) +# for n in ned.generate(): +# print(n) +# +# from test_orientations import construct_nd2_for_fiat +# +# ned_fiat = construct_nd2_for_fiat(tri) +# +# print("fiat") +# for n in ned_fiat.generate(): +# print(n) + + +def test_generate_quadrature(): + cell = polygon(3) + degree = 2 + # elem = create_dg1(cell) + # elem = create_cg1(cell) + # elem = construct_nd(cell) + elem = construct_nd2(cell) + # elem = construct_nd2_for_fiat(cell) + from FIAT.nedelec import Nedelec + fiat_elem = Nedelec(cell.to_fiat(), degree) + # from FIAT.lagrange import Lagrange + # fiat_elem = Lagrange(cell.to_fiat(), degree) + degree = elem.spaces[0].degree() + print(degree) + for d in fiat_elem.dual_basis(): + print("fiat", d.pt_dict) + print() + for d in elem.generate(): + print("fuse", d.to_quadrature(degree)) + + elem.to_fiat() diff --git a/test/test_orientations.py b/test/test_orientations.py index de81546e..5f888b0f 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -1,68 +1,141 @@ -import unittest.mock as mock +import pytest from firedrake import * from fuse import * +import numpy as np import sympy as sp -from test_convert_to_fiat import create_cg1, helmholtz_solve, construct_nd, construct_rt +from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt +from test_convert_to_fiat import create_cg1, create_cg2_tri, create_dg1 import os -def dummy_dof_perms(cls, *args, **kwargs): - # return -1s of right shape here - oriented_mats_by_entity, flat_by_entity = cls._initialise_entity_dicts(cls.generate()) - for key1, val1 in oriented_mats_by_entity.items(): - for key2, val2 in oriented_mats_by_entity[key1].items(): - for key3, val3 in oriented_mats_by_entity[key1][key2].items(): - if key1 == 2: - oriented_mats_by_entity[key1][key2][key3] = 1 * np.identity(val3.shape[0]) - #oriented_mats_by_entity[key1][key2][key3][0] = key1*100 + key2*10 + key3 - if key3 == 5: - - oriented_mats_by_entity[key1][key2][key3] = 100 * np.identity(val3.shape[0]) - return oriented_mats_by_entity, False, None - - -def test_interpolation_values(): - cell = polygon(3) - elem = construct_nd(cell) - print() - #with mock.patch.object(ElementTriple, 'make_dof_perms', new=dummy_dof_perms): - mesh = UnitSquareMesh(3, 3) - ones = as_vector((0,1)) - if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): - V = FunctionSpace(mesh, elem.to_ufl()) - else: - V = FunctionSpace(mesh, "N1curl", 1) +np.set_printoptions(precision=4, suppress=True) + + +def construct_nd2(tri=None): + if tri is None: + tri = polygon(3) + deg = 2 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + + dofs = DOFGenerator(xs, S2, S2) + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) + v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + + center_dofs = DOFGenerator(xs, S2, S3) + xs = [immerse(tri, int_ned1, TrHCurl)] + tri_dofs = DOFGenerator(xs, C3, S1) + + vec_Pk = PolynomialSpace(deg - 1, set_shape=True) + Pk = PolynomialSpace(deg - 1) + M = sp.Matrix([[y, -x]]) + nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M + + ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) + return ned + + +def construct_rt2(tri=None): + if tri is None: + tri = polygon(3) + edge = tri.d_entities(1, get_class=True)[0] + deg = 2 + x = sp.Symbol("x") + y = sp.Symbol("y") + vecP1 = PolynomialSpace(1, set_shape=True) + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(1 + x), symbols=(x,)))] + dofs = DOFGenerator(xs, S2, S2) + int_rt2 = ElementTriple(edge, (vecP1, CellHDiv, C0), dofs) + + xs = [immerse(tri, int_rt2, TrHDiv)] + tri_dofs = DOFGenerator(xs, C3, S1) + + v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) + v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) + i_xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + i_dofs = DOFGenerator(i_xs, S2, S3) + + vec_Pk = PolynomialSpace(deg - 1, set_shape=True) + Pk = PolynomialSpace(deg - 1) + M = sp.Matrix([[x, y]]) + rt_space = vec_Pk + (Pk.restrict(deg-2, deg-1))*M + rt2 = ElementTriple(tri, (rt_space, CellHDiv, C0), [tri_dofs, i_dofs]) + return rt2 + + +def construct_nd2_for_fiat(tri=None): + if tri is None: + tri = polygon(3) + deg = 2 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + xs = [DOF(L2Pairing(), PolynomialKernel(np.sqrt(2))), + DOF(L2Pairing(), PolynomialKernel((3*x*np.sqrt(2)/np.sqrt(3)), symbols=[x]))] + + dofs = DOFGenerator(xs, S1, S2) + int_ned1 = ElementTriple(edge, (P1, CellHCurl, C0), dofs) + + xs = [DOF(L2Pairing(), PolynomialKernel(np.sqrt(2))), + DOF(L2Pairing(), PolynomialKernel((-np.sqrt(2)/np.sqrt(3))*(6*x + 3), symbols=[x]))] + dofs = DOFGenerator(xs, S1, S2) + int_ned2 = ElementTriple(tri.edges()[0], (P1, CellHCurl, C0), dofs) + + xs = [DOF(L2Pairing(), PolynomialKernel(np.sqrt(2))), + DOF(L2Pairing(), PolynomialKernel((-np.sqrt(2)/np.sqrt(3))*(6*x - 3), symbols=[x]))] + dofs = DOFGenerator(xs, S1, S2) + int_ned3 = ElementTriple(tri.edges()[0], (P1, CellHCurl, C0), dofs) + + xs = [DOF(L2Pairing(), ComponentKernel((0,))), + DOF(L2Pairing(), ComponentKernel((1,)))] + center_dofs = DOFGenerator(xs, S1, S3) + xs = [immerse(tri, int_ned2, TrHCurl, node=0)] + xs += [immerse(tri, int_ned1, TrHCurl, node=1)] + xs += [immerse(tri, int_ned3, TrHCurl, node=2)] + tri_dofs = DOFGenerator(xs, S1, S1) + + vec_Pk = PolynomialSpace(deg - 1, set_shape=True) + Pk = PolynomialSpace(deg - 1) + M = sp.Matrix([[y, -x]]) + nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M - print(V.cell_node_list) - u = TestFunction(V) - res1= assemble(interpolate(ones, V)) - for i in range(len(res1.dat.data)): - print(f"{i}: {res1.dat.data[i]}") + ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) + return ned -def test_surface_const_nd(): +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_nd, "N1curl", 1), + (construct_nd2, "N1curl", 2)]) +def test_surface_const_nd(elem_gen, elem_code, deg): cell = polygon(3) - elem = construct_nd(cell) - ones = as_vector((0,1)) + elem = elem_gen(cell) + ones = as_vector((0, 1)) - for n in range(1, 6): + for n in range(2, 6): mesh = UnitSquareMesh(n, n) - if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, elem.to_ufl()) else: - V = FunctionSpace(mesh, "N1curl", 1) + V = FunctionSpace(mesh, elem_code, deg) normal = FacetNormal(mesh) ones1 = interpolate(ones, V) - res1= assemble(dot(ones1, normal) * ds) - + res1 = assemble(dot(ones1, normal) * ds) + print(f"{n}: {res1}") assert np.allclose(res1, 0) -def test_surface_const_rt(): +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_rt, "RT", 1), + (construct_rt2, "RT", 2)]) +def test_surface_const_rt(elem_gen, elem_code, deg): cell = polygon(3) - elem = construct_rt(cell) - ones = as_vector((1,0)) + elem = elem_gen(cell) + ones = as_vector((1, 0)) for n in range(1, 6): mesh = UnitSquareMesh(n, n) @@ -70,53 +143,61 @@ def test_surface_const_rt(): if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, elem.to_ufl()) else: - V = FunctionSpace(mesh, "RT", 1) + V = FunctionSpace(mesh, "RT", deg) normal = FacetNormal(mesh) ones1 = interpolate(ones, V) - res1= assemble(dot(ones1, normal) * ds) - + res1 = assemble(dot(ones1, normal) * ds) + print(f"{n}: {res1}") assert np.allclose(res1, 0) -def test_surface_vec(): +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_rt, "RT", 1), + (construct_rt2, "RT", 2)]) +def test_surface_vec_rt(elem_gen, elem_code, deg): cell = polygon(3) - rt_elem = construct_rt(cell) - nd_elem = construct_nd(cell) - - for n in range(1, 6): + rt_elem = elem_gen(cell) + for n in range(2, 6): mesh = UnitSquareMesh(n, n) x, y = SpatialCoordinate(mesh) normal = FacetNormal(mesh) - test_vec = as_vector((-y,x)) + test_vec = as_vector((-y, x)) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, rt_elem.to_ufl()) - vec1 = interpolate(test_vec, V) - res1 = assemble(dot(vec1, normal) * ds) else: V = FunctionSpace(mesh, "RT", 1) - vec2 = interpolate(test_vec, V) - res1 = assemble(dot(vec2, normal) * ds) - print(f"div {n}: {res1}") - + vec = interpolate(test_vec, V) + res = assemble(dot(vec, normal) * ds) + print(f"div {n}: {res}") + assert np.allclose(0, res) + + +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_nd, "N1curl", 1), + (construct_nd2, "N1curl", 2)]) +def test_surface_vec_nd(elem_gen, elem_code, deg): + cell = polygon(3) + nd_elem = elem_gen(cell) + + for n in range(2, 6): + + mesh = UnitSquareMesh(n, n) + x, y = SpatialCoordinate(mesh) + normal = FacetNormal(mesh) + test_vec = as_vector((-y, x)) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, nd_elem.to_ufl()) - vec1 = interpolate(test_vec, V) - res2 = assemble(dot(vec1, normal) * ds) else: - V = FunctionSpace(mesh, "N1curl", 1) - vec1 = interpolate(test_vec, V) - res2 = assemble(dot(vec1, normal) * ds) - print(f"curl {n}: {res2}") + V = FunctionSpace(mesh, elem_code, deg) + vec = interpolate(test_vec, V) + res = assemble(dot(vec, normal) * ds) + print(f"curl {n}: {res}") + assert np.allclose(0, res) - assert np.allclose(0, res1) - assert np.allclose(0, res2) - -def test_interpolate_vs_project(V): +def get_expression(V): mesh = V.mesh() - dim = mesh.geometric_dimension() + dim = mesh.geometric_dimension if dim == 2: x, y = SpatialCoordinate(mesh) elif dim == 3: @@ -125,103 +206,213 @@ def test_interpolate_vs_project(V): shape = V.value_shape if dim == 2: if len(shape) == 0: - expression = x + y + exact = Function(FunctionSpace(mesh, 'CG', 5)) + expression = x elif len(shape) == 1: + exact = Function(VectorFunctionSpace(mesh, 'CG', 5)) expression = as_vector([x, y]) - elif len(shape) == 2: - expression = as_tensor(([x, y], [x, y])) elif dim == 3: if len(shape) == 0: + exact = Function(FunctionSpace(mesh, 'CG', 5)) expression = x + y + z elif len(shape) == 1: + exact = Function(FunctionSpace(mesh, 'CG', 5)) expression = as_vector([x, y, z]) - elif len(shape) == 2: - expression = as_tensor(([x, y, z], [x, y, z], [x, y, z])) + return expression, exact + +def interpolate_vs_project(V, expression, exact): f = assemble(interpolate(expression, V)) expect = project(expression, V) - print(f.dat.data) - print(expect.dat.data) - assert np.allclose(f.dat.data, expect.dat.data, atol=1e-06) + exact.interpolate(expression) + return sqrt(assemble(inner((expect - exact), (expect - exact)) * dx)), sqrt(assemble(inner((f - exact), (f - exact)) * dx)) -def construct_nd2(tri=None): - if tri is None: - tri = polygon(3) - deg = 2 - edge = tri.edges()[0] - x = sp.Symbol("x") - y = sp.Symbol("y") - - #xs = [DOF(L2Pairing(), PointKernel((-np.sqrt(2),)))] - xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x+1), symbols=[x])), - DOF(L2Pairing(), PolynomialKernel((1/2)*(1-x), symbols=[x]))] +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_cg3, "CG", 3, 3.8)]) +def test_convergence(elem_gen, elem_code, deg, conv_rate): + cell = polygon(3) + elem = elem_gen(cell) + scale_range = range(3, 6) + diff_proj = [0 for i in scale_range] + diff_proj1 = [0 for i in scale_range] + diff_inte = [0 for i in scale_range] + diff_inte1 = [0 for i in scale_range] + for n in scale_range: + mesh = UnitSquareMesh(2**n, 2**n) + + V = FunctionSpace(mesh, elem_code, deg) + x, y = SpatialCoordinate(mesh) + expr = cos(x*pi*2)*sin(y*pi*2) + _, exact = get_expression(V) + diff_proj[n-min(scale_range)], diff_inte[n-min(scale_range)] = interpolate_vs_project(V, expr, exact) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + x, y = SpatialCoordinate(mesh) + expr = cos(x*pi*2)*sin(y*pi*2) + _, exact = get_expression(V) + diff_proj1[n-min(scale_range)], diff_inte1[n-min(scale_range)] = interpolate_vs_project(V, expr, exact) + + print("projection l2 error norms:", diff_proj) + diff_proj = np.array(diff_proj) + conv1 = np.log2(diff_proj[:-1] / diff_proj[1:]) + print("convergence order:", conv1) + # assert all([c > conv_rate for c in conv1]) + print("interpolation l2 error norms:", diff_inte) + diff_inte = np.array(diff_inte) + conv1 = np.log2(diff_inte[:-1] / diff_inte[1:]) + print("convergence order:", conv1) - dofs = DOFGenerator(xs, S1, S2) - int_ned = ElementTriple(edge, (P1, CellHCurl, C0), dofs) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + print("interpolation l2 error norms:", diff_inte1) + diff_inte1 = np.array(diff_inte1) + conv2 = np.log2(diff_inte1[:-1] / diff_inte1[1:]) + print("convergence order:", conv2) + assert all([c > conv_rate for c in conv2]) + assert all([c > conv_rate for c in conv1]) + + +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_nd, "N1curl", 1, 0.8), + (construct_rt2, "RT", 2, 1.8), + (construct_nd2, "N1curl", 2, 1.8)]) +def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): + cell = polygon(3) + elem = elem_gen(cell) + scale_range = range(3, 6) + diff_proj = [0 for i in scale_range] + diff_inte = [0 for i in scale_range] + for n in scale_range: + mesh = UnitSquareMesh(2**n, 2**n) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + else: + V = FunctionSpace(mesh, elem_code, deg) + x, y = SpatialCoordinate(mesh) + expr = cos(x*pi*2)*sin(y*pi*2) + expr = as_vector([expr, expr]) + _, exact = get_expression(V) + diff_proj[n-min(scale_range)], diff_inte[n-min(scale_range)] = interpolate_vs_project(V, expr, exact) + + print("projection l2 error norms:", diff_proj) + diff_proj = np.array(diff_proj) + conv1 = np.log2(diff_proj[:-1] / diff_proj[1:]) + print("convergence order:", conv1) + print("interpolation l2 error norms:", diff_inte) + diff_inte = np.array(diff_inte) + conv2 = np.log2(diff_inte[:-1] / diff_inte[1:]) + print("convergence order:", conv1) + + assert all([c > conv_rate for c in conv1]) + assert all([c > conv_rate for c in conv2]) + + +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), + (create_cg1, "CG", 1), + (create_dg1, "DG", 1), + (construct_cg3, "CG", 3), + (construct_rt2, "RT", 2), + (construct_nd2, "N1curl", 2)]) +def test_interpolation(elem_gen, elem_code, deg): + cell = polygon(3) + elem = elem_gen(cell) + mesh = UnitSquareMesh(1, 1) - xs = [DOF(L2Pairing(), ComponentKernel((0,))), - DOF(L2Pairing(), ComponentKernel((1,)))] - center_dofs = DOFGenerator(xs, S1, S3) - xs = [immerse(tri, int_ned, TrHCurl)] - tri_dofs = DOFGenerator(xs, C3, S1) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V = FunctionSpace(mesh, elem.to_ufl()) + else: + V = FunctionSpace(mesh, elem_code, deg) - vec_Pk = PolynomialSpace(deg - 1, set_shape=True) - Pk = PolynomialSpace(deg - 1) - M = sp.Matrix([[y, -x]]) - nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M + expression, _ = get_expression(V) + expect = project(expression, V) + f = assemble(interpolate(expression, V)) + assert np.allclose(f.dat.data, expect.dat.data) + expect = project(expression, V) + v = TestFunction(V) + u = TrialFunction(V) - ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) - return ned + a = inner(u, v) * dx + L = inner(expect, v) * dx + solution = Function(V) + solve(a == L, solution) -def test_degree2_interpolation(): - cell = polygon(3) - #elem = construct_nd2(cell) - mesh = UnitSquareMesh(1,1) + assert norm(assemble(expect - solution)) < 1e-15 - #print("fuse") - #V = FunctionSpace(mesh, elem.to_ufl()) - #test_interpolate_vs_project(V) - print("firedrake") - V = FunctionSpace(mesh, "N1curl", 2) - test_interpolate_vs_project(V) +@pytest.mark.xfail(reason="issues with tets") +def test_projection_convergence(elem_gen, elem_code, deg, conv_rate): + cell = make_tetrahedron() + elem = elem_gen(cell) + function = lambda x: cos((3/4)*pi*x[0]) -def test_create_fiat_nd(): + scale_range = range(1, 6) + diff = [0 for i in scale_range] + for i in scale_range: + mesh = UnitSquareMesh(2 ** i, 2 ** i) + x, y = SpatialCoordinate(mesh) + expr = cos(x*pi*2)*sin(y*pi*2) + expr = as_vector([expr, expr]) + exact = Function(VectorFunctionSpace(mesh, 'CG', 5)) + exact.interpolate(expr) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V2 = FunctionSpace(mesh, elem.to_ufl()) + res2 = project(function(x), V2) + diff[i - 1] = res2 + else: + V = FunctionSpace(mesh, elem_code, deg) + # res = project(function(x), V) + res = project(expr, V, solver_parameters={'ksp_type': 'preonly', 'pc_type': 'lu'}) + diff[i - 1] = sqrt(assemble(inner((res - exact), (res - exact)) * dx)) + + # assert np.allclose(res1, res2) + + print("l2 error norms:", diff) + diff = np.array(diff) + conv1 = np.log2(diff[:-1] / diff[1:]) + print("convergence order:", conv1) + + # print("fuse l2 error norms:", diff) + # diff = np.array(diff) + # conv2 = np.log2(diff[:-1] / diff[1:]) + # print("fuse convergence order:", conv2) + + assert (np.array(conv1) > conv_rate).all() + # assert (np.array(conv2) > conv_rate).all() + + +@pytest.mark.parametrize("elem_gen,elem_gen2,elem_code,deg,deg2", + [(create_cg1, create_cg1, "CG", 1, 1), + (create_cg2_tri, create_cg2_tri, "CG", 2, 2), + (construct_cg3, construct_cg3, "CG", 3, 3), + (construct_nd2, construct_nd2, "N1curl", 2, 2), + (construct_rt2, construct_rt2, "RT", 2, 2), + ]) +def test_two_form(elem_gen, elem_gen2, elem_code, deg, deg2): cell = polygon(3) - nd = construct_nd2(cell) - ref_el = cell.to_fiat() - sd = ref_el.get_spatial_dimension() - deg = 2 - - from FIAT.nedelec import Nedelec - fiat_elem = Nedelec(ref_el, deg) - print("fiat") - for f in fiat_elem.dual_basis(): - print(f.pt_dict) - - print("fuse") - for d in nd.generate(): - print(d.convert_to_fiat(ref_el, deg).pt_dict) - print(d) - breakpoint() - my_elem = nd.to_fiat() + mesh = UnitSquareMesh(3, 3) - Q = create_quadrature(ref_el, 2*(deg+1)) - Qpts, _ = Q.get_points(), Q.get_weights() + spaces = [] + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + elem = elem_gen(cell) + elem2 = elem_gen2(cell) + spaces += [("fuse", FunctionSpace(mesh, elem.to_ufl()), FunctionSpace(mesh, elem2.to_ufl()))] + else: + spaces += [("fiat", FunctionSpace(mesh, elem_code, deg), FunctionSpace(mesh, elem_code, deg2))] - fiat_vals = fiat_elem.tabulate(0, Qpts) - my_vals = my_elem.tabulate(0, Qpts) + results = [] + for name, V, V2 in spaces: + v = TestFunction(V) + u = TrialFunction(V2) + res = assemble(inner(u, v)*dx).M.values + results += [res] + exp, _ = get_expression(V) + f = Function(V2).interpolate(exp) - fiat_vals = flatten(fiat_vals[(0,) * sd]) - my_vals = flatten(my_vals[(0,) * sd]) + a = inner(v, u) * dx + L = inner(f, v) * dx - (x, res, _, _) = np.linalg.lstsq(fiat_vals.T, my_vals.T) - x1 = np.linalg.inv(x) - assert np.allclose(np.linalg.norm(my_vals.T - fiat_vals.T @ x), 0) - assert np.allclose(np.linalg.norm(fiat_vals.T - my_vals.T @ x1), 0) - assert np.allclose(res, 0) + solution = Function(V2) + solve(a == L, solution) + assert norm(assemble(f - solution)) < 1e-15 diff --git a/test/test_polynomial_space.py b/test/test_polynomial_space.py index cb1c769c..e7dad66d 100644 --- a/test/test_polynomial_space.py +++ b/test/test_polynomial_space.py @@ -153,6 +153,43 @@ def flatten(coeffs): return nc +@pytest.mark.parametrize("deg", [1, 2, 3, 4]) +def test_3d_nd_construction(deg): + cell = make_tetrahedron() + ref_el = cell.to_fiat() + sd = ref_el.get_spatial_dimension() + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + composite = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + + assert composite.set_shape + assert isinstance(composite, ConstructedPolynomialSpace) + on_set = composite.to_ON_polynomial_set(ref_el) + + from FIAT.nedelec import NedelecSpace3D + fiat_nd_space = NedelecSpace3D(ref_el, deg) + + Q = create_quadrature(ref_el, 2*(deg + 1)) + Qpts, _ = Q.get_points(), Q.get_weights() + fiat_vals = fiat_nd_space.tabulate(Qpts)[(0,) * sd] + my_vals = on_set.tabulate(Qpts)[(0,) * sd] + fiat_vals = flatten(fiat_vals) + my_vals = flatten(my_vals) + + (x, res, _, _) = np.linalg.lstsq(fiat_vals.T, my_vals.T) + x1 = np.linalg.inv(x) + assert np.allclose(np.linalg.norm(my_vals.T - fiat_vals.T @ x), 0) + assert np.allclose(np.linalg.norm(fiat_vals.T - my_vals.T @ x1), 0) + assert np.allclose(res, 0) + + @pytest.mark.parametrize("deg", [1, 2, 3, 4]) def test_3d_rt_construction(deg): cell = make_tetrahedron() diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index fa117e7a..7c527203 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -1,4 +1,5 @@ import pytest +import numpy as np from fuse import * from firedrake import * from test_2d_examples_docs import construct_cg1, construct_dg1 From 3c0363c2aba6a581728eb4c9393397c28a31b9bf Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 29 Jan 2026 16:53:01 +0000 Subject: [PATCH 08/98] punt on orientations - how was this so easy --- fuse/tensor_products.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fuse/tensor_products.py b/fuse/tensor_products.py index 9556c76b..cf16e247 100644 --- a/fuse/tensor_products.py +++ b/fuse/tensor_products.py @@ -21,6 +21,7 @@ def __init__(self, A, B, flat=False): self.DOFGenerator = [A.DOFGenerator, B.DOFGenerator] self.cell = TensorProductPoint(A.cell, B.cell) self.flat = flat + self.apply_matrices = False def sub_elements(self): return [self.A, self.B] @@ -28,10 +29,23 @@ def sub_elements(self): def __repr__(self): return "TensorProd(%s, %s)" % (repr(self.A), repr(self.B)) + def setup_matrices(self): + oriented_mats_by_entity, flat_by_entity = self._initialise_entity_dicts(self.A.generate() + self.B.generate()) + breakpoint() + for dim in range(self.cell.dimension): + for dimA in range(self.A.cell.dimension): + pass + for dimB in range(self.B.cell_dimension): + pass + return super().setup_matrices() + def to_ufl(self): if self.flat: return FuseElement(self, self.cell.flatten().to_ufl()) - return TensorProductElement(*[e.to_ufl() for e in self.sub_elements()], cell=self.cell.to_ufl()) + ufl_sub_elements = [e.to_ufl() for e in self.sub_elements()] + # self.setup_matrices() + # breakpoint() + return TensorProductElement(*ufl_sub_elements, cell=self.cell.to_ufl()) def flatten(self): return TensorProductTriple(self.A, self.B, flat=True) From c6de1a239e32412d05b875411425d1f3dc5b49aa Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 3 Feb 2026 14:14:03 +0000 Subject: [PATCH 09/98] CG3 on tets working --- .github/workflows/test.yml | 2 +- fuse/triples.py | 27 +++++++++++------ test/test_convert_to_fiat.py | 58 +++++++++++++++++++----------------- test/test_orientations.py | 47 +++++++---------------------- 4 files changed, 59 insertions(+), 75 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88b3d0f3..e2615731 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: /usr/bin/git config --global --add safe.directory /opt/firedrake/ cd /opt/firedrake/ git fetch - git checkout indiamai/fuse-quads + git checkout indiamai/indiamai/fuse-closures-with-pyop3 git pull pip install pybind11 Cython make diff --git a/fuse/triples.py b/fuse/triples.py index a4557a86..395f502a 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -60,12 +60,14 @@ def setup_ids_and_nodes(self): for dim in sorted(top): entity_ids[dim] = {i: [] for i in top[dim]} + self.dof_id_to_fiat_id = {} entities = [(dim, entity) for dim in sorted(top) for entity in sorted(top[dim])] counter = 0 for entity in entities: dim = entity[0] for i in range(len(dofs)): if entity[1] == dofs[i].cell_defined_on.id - min_ids[dim]: + self.dof_id_to_fiat_id[dofs[i].id] = counter entity_ids[dim][dofs[i].cell_defined_on.id - min_ids[dim]].append(counter) nodes.append(dofs[i].convert_to_fiat(self.ref_el, degree, value_shape)) counter += 1 @@ -73,10 +75,15 @@ def setup_ids_and_nodes(self): def setup_matrices(self): self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) - matrices, _, _ = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) reversed_matrices = self.reverse_dof_perms(matrices) - self.apply_matrices = True - self.entity_perms = False + # self.pure_perm = pure_perm + self.pure_perm = False + if self.pure_perm: + self.apply_matrices = False + else: + self.apply_matrices = True + self.entity_perms = entity_perms return matrices, reversed_matrices def __repr__(self): @@ -139,8 +146,10 @@ def to_fiat(self): self.to_ufl() form_degree = 1 if self.spaces[0].set_shape else 0 degree = self.spaces[0].degree() - - dual = DualSet(self.nodes, self.ref_el, self.entity_ids) + if self.pure_perm: + dual = DualSet(self.nodes, self.ref_el, self.entity_ids, self.entity_perms) + else: + dual = DualSet(self.nodes, self.ref_el, self.entity_ids) return CiarletElement(self.poly_set, dual, degree, form_degree) def to_tikz(self, show=True, scale=3): @@ -264,7 +273,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): dim = e.dim() e_id = e.id - min_ids[dim] res_dict[dim][e_id] = {} - dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] + dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] res_dict[dim][e_id][0] = np.eye(len(dof_ids)) original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) @@ -376,9 +385,9 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): total_ent_dof_ids = [] for dof_gen in entity_associations[dim][e_id].keys(): ent_dofs = entity_associations[dim][e_id][dof_gen] - ent_dofs_ids = np.array([ed.id for ed in ent_dofs], dtype=int) + ent_dofs_ids = np.array([self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs], dtype=int) # (dof_gen, ent_dofs) - total_ent_dof_ids += [ed.id for ed in ent_dofs if ed.id not in total_ent_dof_ids] + total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if ed.id not in total_ent_dof_ids] dof_gen_class = ent_dofs[0].generation if not len(dof_gen_class[dim].g2.members()) == 1 and dim == min(dof_gen_class.keys()): # if DOFs on entity are not perms, get the matrix @@ -416,7 +425,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): sub_e_id = sub_e.id - min_ids[sub_e.dim()] sub_ent_ids = [] for (k, v) in entity_associations[immersed_dim][sub_e_id].items(): - sub_ent_ids += [e.id for e in v] + sub_ent_ids += [self.dof_id_to_fiat_id[e.id] for e in v] sub_mat = oriented_mats_by_entity[immersed_dim][sub_e_id][sub_g.numeric_rep()][np.ix_(sub_ent_ids, sub_ent_ids)] expanded = np.kron(g_sub_mat, sub_mat) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 99fb1cf8..d0c9d54c 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -8,6 +8,8 @@ from test_3d_examples_docs import construct_tet_rt from test_polynomial_space import flatten from element_examples import CR_n +import os +np.set_printoptions(linewidth=90, precision=4, suppress=True) def create_dg0(cell): @@ -592,20 +594,6 @@ def test_project(elem_gen, elem_code, deg): assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) -@pytest.mark.parametrize("elem_gen,elem_code,deg", [pytest.param(construct_nd, "N1curl", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 ND')), - pytest.param(construct_rt, "RT", 2, marks=pytest.mark.xfail(reason='Need to implement order 2 RT'))]) -def test_project_vec(elem_gen, elem_code, deg): - cell = polygon(3) - elem = elem_gen(cell) - mesh = UnitTriangleMesh() - - U = FunctionSpace(mesh, elem_code, deg) - assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) - - U = FunctionSpace(mesh, elem.to_ufl()) - assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) - - @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_dg1_tet, "DG", 1)]) def test_project_3d(elem_gen, elem_code, deg): cell = make_tetrahedron() @@ -620,13 +608,16 @@ def test_project_3d(elem_gen, elem_code, deg): assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) -@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [pytest.param(create_dg1_tet, "DG", 1, 0.8, marks=pytest.mark.xfail(reason="DG on tets - check test written correctly"))]) +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_dg1_tet, "DG", 1, 0.8), + (create_cg1_tet, "CG", 1, 1.8), + (create_cg2_tet, "CG", 2, 2.8), + (create_cg3_tet, "CG", 3, 3.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) function = lambda x: cos((3/4)*pi*x[0]) - scale_range = range(1, 4) + scale_range = range(2, 4) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] for i in scale_range: @@ -635,22 +626,33 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): V = FunctionSpace(mesh, elem_code, deg) res1 = project(V, mesh, function(x)) - diff2[i - 1] = res1 - - V2 = FunctionSpace(mesh, elem.to_ufl()) - res2 = project(V2, mesh, function(x)) - diff[i - 1] = res2 - assert np.allclose(res1, res2) + diff2[i - min(scale_range)] = res1 + # v = TestFunction(V) + # l = assemble(inner(function(x), v)*dx) + # print(l.dat.data[7:10]) + # print(res1) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V2 = FunctionSpace(mesh, elem.to_ufl()) + res2 = project(V2, mesh, function(x)) + diff[i - min(scale_range)] = res2 + # v = TestFunction(V2) + # l_a = assemble(inner(function(x), v)*dx) + # print(l_a.dat.data[7:10]) + # print(V2.cell_node_list[0:2]) + # print(res2) + + # assert np.allclose(res1, res2) print("firedrake l2 error norms:", diff2) diff2 = np.array(diff2) conv1 = np.log2(diff2[:-1] / diff2[1:]) print("firedrake convergence order:", conv1) + assert (np.array(conv1) > conv_rate).all() - print("fuse l2 error norms:", diff) - diff = np.array(diff) - conv2 = np.log2(diff[:-1] / diff[1:]) - print("fuse convergence order:", conv2) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + print("fuse l2 error norms:", diff) + diff = np.array(diff) + conv2 = np.log2(diff[:-1] / diff[1:]) + print("fuse convergence order:", conv2) - assert (np.array(conv1) > conv_rate).all() - assert (np.array(conv2) > conv_rate).all() + assert (np.array(conv2) > conv_rate).all() diff --git a/test/test_orientations.py b/test/test_orientations.py index 5f888b0f..773afd09 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -7,7 +7,7 @@ from test_convert_to_fiat import create_cg1, create_cg2_tri, create_dg1 import os -np.set_printoptions(precision=4, suppress=True) +np.set_printoptions(linewidth=90, precision=4, suppress=True) def construct_nd2(tri=None): @@ -339,45 +339,18 @@ def test_interpolation(elem_gen, elem_code, deg): assert norm(assemble(expect - solution)) < 1e-15 -@pytest.mark.xfail(reason="issues with tets") -def test_projection_convergence(elem_gen, elem_code, deg, conv_rate): - cell = make_tetrahedron() +@pytest.mark.parametrize("elem_gen,elem_code,deg", [pytest.param(construct_nd2, "N1curl", 2), + pytest.param(construct_rt2, "RT", 2)]) +def test_project_vec(elem_gen, elem_code, deg): + cell = polygon(3) elem = elem_gen(cell) - function = lambda x: cos((3/4)*pi*x[0]) - - scale_range = range(1, 6) - diff = [0 for i in scale_range] - for i in scale_range: - mesh = UnitSquareMesh(2 ** i, 2 ** i) - x, y = SpatialCoordinate(mesh) - expr = cos(x*pi*2)*sin(y*pi*2) - expr = as_vector([expr, expr]) - exact = Function(VectorFunctionSpace(mesh, 'CG', 5)) - exact.interpolate(expr) - if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): - V2 = FunctionSpace(mesh, elem.to_ufl()) - res2 = project(function(x), V2) - diff[i - 1] = res2 - else: - V = FunctionSpace(mesh, elem_code, deg) - # res = project(function(x), V) - res = project(expr, V, solver_parameters={'ksp_type': 'preonly', 'pc_type': 'lu'}) - diff[i - 1] = sqrt(assemble(inner((res - exact), (res - exact)) * dx)) - - # assert np.allclose(res1, res2) - - print("l2 error norms:", diff) - diff = np.array(diff) - conv1 = np.log2(diff[:-1] / diff[1:]) - print("convergence order:", conv1) + mesh = UnitTriangleMesh() - # print("fuse l2 error norms:", diff) - # diff = np.array(diff) - # conv2 = np.log2(diff[:-1] / diff[1:]) - # print("fuse convergence order:", conv2) + U = FunctionSpace(mesh, elem_code, deg) + assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) - assert (np.array(conv1) > conv_rate).all() - # assert (np.array(conv2) > conv_rate).all() + U = FunctionSpace(mesh, elem.to_ufl()) + assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) @pytest.mark.parametrize("elem_gen,elem_gen2,elem_code,deg,deg2", From 45861c3973ab6dd92403821c212445a629eb878b Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 4 Feb 2026 14:32:35 +0000 Subject: [PATCH 10/98] nedelec 1 on tets works --- fuse/dof.py | 25 +++++++++--------- fuse/triples.py | 5 ++-- test/test_3d_examples_docs.py | 48 +++++++++++++++++++++++++++++------ test/test_convert_to_fiat.py | 41 +++++++++++++++++++----------- test/test_orientations.py | 14 ---------- 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 4f00948f..88544f65 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -118,13 +118,8 @@ def _from_dict(obj_dict): class BaseKernel(): def __init__(self): - self.cell = None - self.entity = None self.attachment = False - def add_context(self, cell, entity): - return self - def permute(self, g): raise NotImplementedError("This method should be implemented by the subclass") @@ -156,7 +151,7 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed): + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): return np.array([self.pt for _ in Qpts]).astype(np.float64), np.ones_like(Qwts), [[tuple()] for pt in Qpts] def _to_dict(self): @@ -190,10 +185,10 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed): + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): if immersed: - return Qpts, np.array([wt*self.pt for wt in Qwts]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] - return Qpts, np.array([wt*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] + 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*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] def _to_dict(self): o_dict = {"pt": self.pt} @@ -238,8 +233,8 @@ def __call__(self, *args): # return self.g(res) return res - def evaluate(self, Qpts, Qwts, basis_change, immersed): - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(len(pt) + 1)] for pt in Qpts] + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] def _to_dict(self): o_dict = {"fn": self.fn} @@ -271,7 +266,7 @@ def __call__(self, *args): return tuple(args[i] if i in self.comp else 0 for i in range(len(args))) # return tuple(args[c] for c in self.comp) - def evaluate(self, Qpts, Qwts, basis_change, immersed): + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): return Qpts, Qwts, [[self.comp] for pt in Qpts] # return Qpts, np.array([self(*pt) for pt in Qpts]).astype(np.float64) @@ -347,16 +342,20 @@ def to_quadrature(self, arg_degree): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) else: basis_change = np.eye(dim) - pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed) + pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed, self.cell.dimension) if self.immersed: # need to compute jacobian from attachment. pts = [self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts] immersion = self.target_space.tabulate(wts, self.pairing.entity) + # Special case - force evaluation on different orientation of entity for construction of matrix transforms if self.entity_o: immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) + wts = np.outer(wts, immersion) + # else: + # pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed) # pt dict is { pt: (weight, component)} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, wts, comps)} diff --git a/fuse/triples.py b/fuse/triples.py index 395f502a..db090de3 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -274,6 +274,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): e_id = e.id - min_ids[dim] res_dict[dim][e_id] = {} dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] + dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] res_dict[dim][e_id][0] = np.eye(len(dof_ids)) original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) @@ -288,8 +289,8 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): else: new_nodes = [d(g, entity_o=permuted_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) - res_dict[dim][e_id][val] = np.matmul(transformed_basis, original_V.T)[np.ix_(dof_ids, dof_ids)] - + temp = np.matmul(transformed_basis, original_V.T) + res_dict[dim][e_id][val] = temp[np.ix_(dof_ids, dof_ids)] return res_dict def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 10333b96..66937a5f 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -3,6 +3,7 @@ from sympy.combinatorics import Permutation import sympy as sp import numpy as np +np.set_printoptions(linewidth=90, precision=4, suppress=True) def test_dg1(): @@ -95,7 +96,7 @@ def construct_tet_rt(cell=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), PolynomialKernel(1))] + xs = [DOF(L2Pairing(), VectorKernel((1,)))] dofs = DOFGenerator(xs, S1, S3) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) @@ -126,7 +127,7 @@ def construct_tet_ned(cell=None): nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 # [test_tet_ned 0] - xs = [DOF(L2Pairing(), PolynomialKernel(1))] + xs = [DOF(L2Pairing(), VectorKernel((1,)))] dofs = DOFGenerator(xs, S1, S2) edges = ElementTriple(edge, (vec_Pd, CellHCurl, L2), dofs) @@ -140,14 +141,44 @@ def construct_tet_ned(cell=None): return ElementTriple(tri, (nd_space, CellHCurl, L2), [edge_dofs]) +# def construct_tet_nd2(tet=None): +# if tet is None: +# tet = polygon(3) +# deg = 2 +# edge = tri.edges()[0] +# x = sp.Symbol("x") +# y = sp.Symbol("y") + +# xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + +# dofs = DOFGenerator(xs, S2, S2) +# int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) +# v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) +# v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) +# xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + +# center_dofs = DOFGenerator(xs, S2, S3) +# xs = [immerse(tri, int_ned1, TrHCurl)] +# tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), +# Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), +# Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) +# edge_dofs = DOFGenerator(xs, tet_edges, S1) + +# vec_Pk = PolynomialSpace(deg - 1, set_shape=True) +# Pk = PolynomialSpace(deg - 1) +# M = sp.Matrix([[y, -x]]) +# nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M + +# ned = ElementTriple(tri, (nd, CellHCurl, C0), [edge_dofs, center_dofs]) +# return ned + def plot_tet_rt(): rt = construct_tet_rt() rt.plot() def test_tet_rt(): - tetra = make_tetrahedron() - rt1 = construct_tet_rt(tetra) + rt1 = construct_tet_rt() ls = rt1.generate() # TODO make this a proper test for dof in ls: @@ -155,17 +186,18 @@ def test_tet_rt(): rt1.to_fiat() -@pytest.mark.xfail(reason='DOFs not forming full rank matrix') def test_tet_nd(): - tetra = make_tetrahedron() - nd1 = construct_tet_ned(tetra) + nd1 = construct_tet_ned() ls = nd1.generate() # TODO make this a proper test for dof in ls: + # dof_dict = dof.to_quadrature(1) + # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) print(dof) + # plot_tet_ned() nd1.to_fiat() def plot_tet_ned(): ned = construct_tet_ned() - ned.plot() + ned.plot(filename="tet_nd2_fiat.png") diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index d0c9d54c..cccb528e 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -5,7 +5,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 -from test_3d_examples_docs import construct_tet_rt +from test_3d_examples_docs import construct_tet_rt, construct_tet_ned from test_polynomial_space import flatten from element_examples import CR_n import os @@ -611,12 +611,17 @@ def test_project_3d(elem_gen, elem_code, deg): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_dg1_tet, "DG", 1, 0.8), (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), - (create_cg3_tet, "CG", 3, 3.8)]) + (create_cg3_tet, "CG", 3, 3.8), + (construct_tet_rt, "RT", 1, 0.8), + (construct_tet_ned, "N1curl", 1, 0.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) function = lambda x: cos((3/4)*pi*x[0]) - + if elem_code != "CG" and elem_code != "DG": + expr = lambda x: as_vector([function(x), function(x), function(x)]) + else: + expr = function scale_range = range(2, 4) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] @@ -625,34 +630,40 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): x = SpatialCoordinate(mesh) V = FunctionSpace(mesh, elem_code, deg) - res1 = project(V, mesh, function(x)) + res1 = project(V, mesh, expr(x)) diff2[i - min(scale_range)] = res1 - # v = TestFunction(V) - # l = assemble(inner(function(x), v)*dx) - # print(l.dat.data[7:10]) + v = TestFunction(V) + l = assemble(inner(expr(x), v)*dx) + f = assemble(interpolate(expr(x), V)) + # print(l.dat.data) + print(f.dat.data) + print(V.cell_node_list) # print(res1) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) - res2 = project(V2, mesh, function(x)) + res2 = project(V2, mesh, expr(x)) diff[i - min(scale_range)] = res2 - # v = TestFunction(V2) - # l_a = assemble(inner(function(x), v)*dx) - # print(l_a.dat.data[7:10]) - # print(V2.cell_node_list[0:2]) + v = TestFunction(V2) + l_a = assemble(inner(expr(x), v)*dx) + f_fuse = assemble(interpolate(expr(x), V2)) + # print(l_a.dat.data) + print(f_fuse.dat.data) + print(V2.cell_node_list) # print(res2) - + # breakpoint() # assert np.allclose(res1, res2) print("firedrake l2 error norms:", diff2) diff2 = np.array(diff2) conv1 = np.log2(diff2[:-1] / diff2[1:]) print("firedrake convergence order:", conv1) - assert (np.array(conv1) > conv_rate).all() + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): print("fuse l2 error norms:", diff) diff = np.array(diff) conv2 = np.log2(diff[:-1] / diff[1:]) print("fuse convergence order:", conv2) - assert (np.array(conv2) > conv_rate).all() + else: + assert (np.array(conv1) > conv_rate).all() diff --git a/test/test_orientations.py b/test/test_orientations.py index 773afd09..8146d0f0 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -339,20 +339,6 @@ def test_interpolation(elem_gen, elem_code, deg): assert norm(assemble(expect - solution)) < 1e-15 -@pytest.mark.parametrize("elem_gen,elem_code,deg", [pytest.param(construct_nd2, "N1curl", 2), - pytest.param(construct_rt2, "RT", 2)]) -def test_project_vec(elem_gen, elem_code, deg): - cell = polygon(3) - elem = elem_gen(cell) - mesh = UnitTriangleMesh() - - U = FunctionSpace(mesh, elem_code, deg) - assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) - - U = FunctionSpace(mesh, elem.to_ufl()) - assert np.allclose(project(U, mesh, as_vector((1, 1))), 0, rtol=1e-5) - - @pytest.mark.parametrize("elem_gen,elem_gen2,elem_code,deg,deg2", [(create_cg1, create_cg1, "CG", 1, 1), (create_cg2_tri, create_cg2_tri, "CG", 2, 2), From 33306020d35b75e89796e2f83b70c62bac96edf6 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Feb 2026 17:04:22 +0000 Subject: [PATCH 11/98] testing --- fuse/triples.py | 10 +++++++++ test/test_convert_to_fiat.py | 42 ++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/fuse/triples.py b/fuse/triples.py index db090de3..d73b3b7a 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -76,6 +76,16 @@ def setup_ids_and_nodes(self): def setup_matrices(self): self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + # matrices[2][0][4][0] = -1 * matrices[2][0][4][0] + # matrices[2][1][5][1] = -1 * matrices[2][1][5][1] + # matrices[2][3][1][3] = -1 * matrices[2][3][5][3] + # for j in range(4): + # for i in [1, 2, 5]: + # matrices[2][j][i] = np.eye(matrices[2][j][i].shape[0]) + # for j in range(4): + # for i in [0, 3, 4]: + # matrices[2][j][i][j][j] = -1 + # breakpoint() reversed_matrices = self.reverse_dof_perms(matrices) # self.pure_perm = pure_perm self.pure_perm = False diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index cccb528e..9cd1677a 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -613,6 +613,7 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg2_tet, "CG", 2, 2.8), (create_cg3_tet, "CG", 3, 3.8), (construct_tet_rt, "RT", 1, 0.8), + # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), (construct_tet_ned, "N1curl", 1, 0.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() @@ -622,11 +623,16 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): expr = lambda x: as_vector([function(x), function(x), function(x)]) else: expr = function - scale_range = range(2, 4) + scale_range = range(1,2) + true_f0 = 0.866 * np.array([-0, -0, -0, 0.3536, -0.3536, -0, 0, 0,-0, 0.3536, -0.5, -0, -0.3536, 0.3536,0,0,-0.5,-0.3536]) + true_f01= 0.866 * np.array([[0,-0.0957,0.0957,0.3536,0.0957,-0.0957,-0.3536,0, 0,-0.0957,-0,-0, 0.0957,0.1913,0,0,-0.1913,-0,-0,0.1768,0.1768,-0.231,0.231,-0.1768,-0.1768,-0.1768,0.5,-0,0.231,0.231,-0.231,0.1768,0.5,0.231]]) + # true_f1 = 0.866 * np.array([[-0.125,-0,-0,-0.1155,0.125,-0,-0.1155 -0,-0.1155,0.0884,0.125,-0,-0, 0.1155,0.1155,-0,0.0884,-0.125,-0,-0,-0.1155,-0.125,-0, 0,-0.0884,-0.0478,-0,-0.1155,-0.0884,0.125,-0, 0.0884,0.0478,0.1155,0, 0.0884,-0.1155,0,-0, 0,-0, 0,-0.0884,-0.0478,-0, 0.1155,0, 0, 0.0884,0, 0.0478, -0.125,-0, 0, 0.0884,0.0478,0, 0,-0.0478,-0, 0,-0, 0.125, 0,-0.0884,-0.0478,-0, 0, 0.0478,0,-0, 0,-0,-0.1155,0,-0, 0.0478,0.0884,0, 0,-0.0478,0.1155,0, 0,-0.0478,-0.0884,-0, 0,0.0478,-0, 0.0884,-0.0478,0, 0,-0, 0.0478,0.0884,-0.0884,0.0478,0,-0,-0.0478,-0,-0.0884,-0, 0,-0.0478,-0.0884,0, 0.0478,0.0884,0, 0,-0,-0,-0, 0.0478,0.0884,-0.0478,-0.0884]]) + # print(true_f0) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] for i in scale_range: - mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + # 2 ** i, 2 ** i + mesh = UnitCubeMesh(2 ** i, 1, 1) x = SpatialCoordinate(mesh) V = FunctionSpace(mesh, elem_code, deg) @@ -636,9 +642,12 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): l = assemble(inner(expr(x), v)*dx) f = assemble(interpolate(expr(x), V)) # print(l.dat.data) - print(f.dat.data) - print(V.cell_node_list) - # print(res1) + # print(f.dat.data) + # print(V.cell_node_list) + # # print(res1) + from ufl.algebra import Abs + + print(assemble(dot(dot(Abs(Jacobian(mesh)), as_vector((1,1,1))), as_vector((1,1,1)))*dx)) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = project(V2, mesh, expr(x)) @@ -646,10 +655,12 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): v = TestFunction(V2) l_a = assemble(inner(expr(x), v)*dx) f_fuse = assemble(interpolate(expr(x), V2)) + breakpoint() # print(l_a.dat.data) - print(f_fuse.dat.data) - print(V2.cell_node_list) + # print(f_fuse.dat.data) + # print(V2.cell_node_list) # print(res2) + # print(f_fuse.dat.data - true_f01) # breakpoint() # assert np.allclose(res1, res2) @@ -664,6 +675,23 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): diff = np.array(diff) conv2 = np.log2(diff[:-1] / diff[1:]) print("fuse convergence order:", conv2) + breakpoint() assert (np.array(conv2) > conv_rate).all() else: assert (np.array(conv1) > conv_rate).all() + + +def test_tet_mesh(): + i = 0 + tet = make_tetrahedron() + mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + x = SpatialCoordinate(mesh) + print(mesh.entity_orientations) + elem = construct_tet_rt(tet) + V = FunctionSpace(mesh, elem.to_ufl()) + # V = FunctionSpace(mesh, "RT", 1) + function = lambda x: cos((3/4)*pi*x[0]) + expr = lambda x: as_vector([function(x), function(x), function(x)]) + f = assemble(interpolate(expr(x), V)) + print(assemble(dot(dot(Jacobian(mesh), as_vector((1,1,1))), as_vector((1,1,1)))*dx)) + breakpoint() \ No newline at end of file From 73b605b032413dacbf77d1077dc8b11325cf515b Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Feb 2026 16:52:23 +0000 Subject: [PATCH 12/98] it seems RT on tets may be working --- fuse/cells.py | 49 ++++++++++---- fuse/triples.py | 35 ++++++---- test/test_3d_examples_docs.py | 45 +++++++++++++ test/test_convert_to_fiat.py | 122 ++++++++++++++++++++++++++-------- 4 files changed, 198 insertions(+), 53 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 6ac51360..09de0c09 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -226,7 +226,9 @@ def make_tetrahedron(): face4 = Point(2, vertex_num=3, edges=[edges[1], edges[4], edges[5]], edge_orientations={0: [1, 0], 2: [1, 0]}) tetra = Point(3, vertex_num=4, edges=[face3, face1, face4, face2]) + return tetra + # .orient(tetra.group.members()[3]) def ufc_tetrahedron(): @@ -257,7 +259,7 @@ def ufc_tetrahedron(): face4 = Point(2, vertex_num=3, edges=[edges[3], edges[5], edges[1]], edge_orientations={0: [1, 0]}) tet = Point(3, vertex_num=4, edges=[face2, face1, face3, face4]) - # breakpoint() + return tet # return Point(3, vertex_num=4, edges=[face3, face1, face4, face2]) # return Point(3, vertex_num=4, edges=[face1, face4, face3, face4], edge_orientations={3: [2, 1, 0]}) @@ -424,19 +426,35 @@ def get_topology(self, renumber=False): vertices = self.ordered_vertices() relabelled_verts = {vertices[i]: i for i in range(len(vertices))} - self.topology = {} - self.topology_unrelabelled = {} + self._topology = {} + self._topology_unrelabelled = {} for i in range(len(structure)): dimension = structure[i] - self.topology[i] = {} - self.topology_unrelabelled[i] = {} + self._topology[i] = {} + self._topology_unrelabelled[i] = {} for node in dimension: - self.topology[i][node - min_ids[i]] = tuple([relabelled_verts[vert] for vert in self.get_node(node).ordered_vertices()]) - self.topology_unrelabelled[i][node - min_ids[i]] = tuple([vert - min_ids[0] for vert in self.get_node(node).ordered_vertices()]) - self.topology_unrelabelled[i] = dict(sorted(self.topology_unrelabelled[i].items())) + self._topology[i][node - min_ids[i]] = tuple([relabelled_verts[vert] for vert in self.get_node(node).ordered_vertices()]) + self._topology_unrelabelled[i][node - min_ids[i]] = tuple([vert - min_ids[0] for vert in self.get_node(node).ordered_vertices()]) + self._topology_unrelabelled[i] = dict(sorted(self._topology_unrelabelled[i].items())) if renumber: - return self.topology - return self.topology_unrelabelled + return self._topology + return self._topology_unrelabelled + + def get_renumbered_topology(self): + structure = [generation for generation in nx.topological_generations(self.graph())] + structure.reverse() + + min_ids = [min(dimension) for dimension in structure] + vertices = self.ordered_vertices() + relabelled_verts = {vertices[i]: i for i in range(len(vertices))} + + self._topology = {} + for i in range(len(structure)): + dimension = structure[i] + self._topology[i] = {} + for node in dimension: + self._topology[i][node - min_ids[i]] = tuple([relabelled_verts[vert] for vert in self.get_node(node).ordered_vertices()]) + return self._topology def get_sub_entities(self): min_ids = self.get_starter_ids() @@ -445,6 +463,7 @@ def get_sub_entities(self): return self.sub_entities def _subentity_traversal(self, sub_ents, min_ids): + # print(self, sub_ents) dim = self.get_spatial_dimension() self_id = self.id - min_ids[dim] @@ -456,10 +475,12 @@ def _subentity_traversal(self, sub_ents, min_ids): sub_ents = self.get_node(p)._subentity_traversal(sub_ents, min_ids) if dim > 1: connections = [(c.point.id, c.point.group.identity) for c in self.connections] - if self.oriented: - connections = self.permute_entities(self.oriented, dim - 1) + # if self.oriented: + # connections = self.permute_entities(self.oriented, dim - 1) + # if self.dimension == 2: + # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) + # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: - # p = e.point p = self.get_node(e).orient(o) p_dim = p.get_spatial_dimension() p_id = p.id - min_ids[p_dim] @@ -970,6 +991,8 @@ def __init__(self, cell, name=None, renumber=False): shape = cell.get_shape() sub_ents = cell.get_sub_entities() super(CellComplexToFiatSimplex, self).__init__(shape, verts, topology, sub_ents) + # if len(verts) == 4: + # breakpoint() def cellname(self): return self.name diff --git a/fuse/triples.py b/fuse/triples.py index d73b3b7a..8dfde084 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -76,19 +76,21 @@ def setup_ids_and_nodes(self): def setup_matrices(self): self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + # breakpoint() # matrices[2][0][4][0] = -1 * matrices[2][0][4][0] # matrices[2][1][5][1] = -1 * matrices[2][1][5][1] # matrices[2][3][1][3] = -1 * matrices[2][3][5][3] # for j in range(4): # for i in [1, 2, 5]: # matrices[2][j][i] = np.eye(matrices[2][j][i].shape[0]) - # for j in range(4): - # for i in [0, 3, 4]: - # matrices[2][j][i][j][j] = -1 + # for j in [0, 1]: + # # for i in [0, 3, 4]: + # for i in range(6): + # matrices[2][j][i][j][j] = -1 * matrices[2][j][i][j][j] # breakpoint() reversed_matrices = self.reverse_dof_perms(matrices) - # self.pure_perm = pure_perm - self.pure_perm = False + self.pure_perm = pure_perm + # self.pure_perm = False if self.pure_perm: self.apply_matrices = False else: @@ -285,7 +287,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): res_dict[dim][e_id] = {} dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] - res_dict[dim][e_id][0] = np.eye(len(dof_ids)) + # res_dict[dim][e_id][0] = np.eye(len(dof_ids)) original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) for g in self.cell.group.members(): @@ -294,12 +296,21 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): if val not in res_dict[dim][e_id].keys() and permuted_e == e.id: if len(dof_ids) == 0: res_dict[dim][e_id][val] = [] - elif g.perm.is_Identity: - res_dict[dim][e_id][val] = np.eye(len(dof_ids)) + # elif g.perm.is_Identity: + # res_dict[dim][e_id][val] = np.eye(len(dof_ids)) else: - new_nodes = [d(g, entity_o=permuted_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] - transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) - temp = np.matmul(transformed_basis, original_V.T) + def make_mat(perm_g): + new_nodes = [d(g, entity_o=perm_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] + transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) + return np.matmul(transformed_basis, original_V.T) + temp = make_mat(permuted_g) + # if dim == 2: + # print(g.numeric_rep(), ~permuted_g) + # print(e_id, val) + # for d in self.generate(): + # if d.cell_defined_on == e: + # print(d(g).cell_defined_on) + # print(d(g, entity_o=~permuted_g).cell_defined_on) res_dict[dim][e_id][val] = temp[np.ix_(dof_ids, dof_ids)] return res_dict @@ -457,7 +468,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): def orient_mat_perms(self): raise NotImplementedError("This should not be necessary") min_ids = self.cell.get_starter_ids() - entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname()).get_topology(), self.cell.get_topology()) + entity_orientations = compare_topologies(ufc_cell(self.cell.to_ufl().cellname).get_topology(), self.cell.get_topology()) num_ents = 0 for dim in self.matrices.keys(): ents = self.cell.d_entities(dim) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 66937a5f..d196b08a 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -61,6 +61,44 @@ def construct_tet_cg3(): return cg3 +def construct_tet_cg4(cell=None): + tetra = make_tetrahedron() + vert = tetra.vertices()[0] + edge = tetra.edges()[0] + face = tetra.d_entities(2)[0] + + xs = [DOF(DeltaPairing(), PointKernel(()))] + dg0 = ElementTriple(vert, (P0, CellL2, "C0"), + DOFGenerator(xs, S1, S1)) + + xs = [DOF(DeltaPairing(), PointKernel((-np.sqrt(3/7),)))] + center = [DOF(DeltaPairing(), PointKernel((0,)))] + dg2_int = ElementTriple(edge, (P2, CellL2, "C0"), + [DOFGenerator(xs, S2, S1), DOFGenerator(center, S1, S1)]) + + # xs = [DOF(DeltaPairing(), PointKernel((-1/np.sqrt(5), -0.26)))] + xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] + dg1_face = ElementTriple(face, (P1, CellL2, "C0"), + DOFGenerator(xs, C3, S1)) + + xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] + int_dof = DOFGenerator(xs, S1, S1) + + v_xs = [immerse(tetra, dg0, TrH1)] + cgverts = DOFGenerator(v_xs, Z4, S1) + + e_xs = [immerse(tetra, dg2_int, TrH1)] + cgedges = DOFGenerator(e_xs, tet_edges, S1) + + f_xs = [immerse(tetra, dg1_face, TrH1)] + cgfaces = DOFGenerator(f_xs, tet_faces, S1) + P4 = PolynomialSpace(4) + + cg4 = ElementTriple(tetra, (P4, CellH1, "C0"), + [cgverts, cgedges, cgfaces, int_dof]) + + return cg4 + def plot_tet_cg3(): cg3 = construct_tet_cg3() @@ -81,6 +119,13 @@ def test_tet_cg3(): dof.eval(test_func) +def test_tet_cg4(): + cg4 = construct_tet_cg4() + + cg4.plot(filename="tet_cg4.png") + print(cg4.to_fiat()) + + def construct_tet_rt(cell=None): if cell is None: cell = make_tetrahedron() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 9cd1677a..7f2b7e1e 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -1,11 +1,12 @@ import pytest import numpy as np +import sympy as sp from fuse import * from firedrake import * from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 -from test_3d_examples_docs import construct_tet_rt, construct_tet_ned +from test_3d_examples_docs import construct_tet_rt, construct_tet_ned, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -612,8 +613,9 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), (create_cg3_tet, "CG", 3, 3.8), + # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), - # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), + # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), (construct_tet_ned, "N1curl", 1, 0.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() @@ -623,39 +625,28 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): expr = lambda x: as_vector([function(x), function(x), function(x)]) else: expr = function - scale_range = range(1,2) - true_f0 = 0.866 * np.array([-0, -0, -0, 0.3536, -0.3536, -0, 0, 0,-0, 0.3536, -0.5, -0, -0.3536, 0.3536,0,0,-0.5,-0.3536]) - true_f01= 0.866 * np.array([[0,-0.0957,0.0957,0.3536,0.0957,-0.0957,-0.3536,0, 0,-0.0957,-0,-0, 0.0957,0.1913,0,0,-0.1913,-0,-0,0.1768,0.1768,-0.231,0.231,-0.1768,-0.1768,-0.1768,0.5,-0,0.231,0.231,-0.231,0.1768,0.5,0.231]]) - # true_f1 = 0.866 * np.array([[-0.125,-0,-0,-0.1155,0.125,-0,-0.1155 -0,-0.1155,0.0884,0.125,-0,-0, 0.1155,0.1155,-0,0.0884,-0.125,-0,-0,-0.1155,-0.125,-0, 0,-0.0884,-0.0478,-0,-0.1155,-0.0884,0.125,-0, 0.0884,0.0478,0.1155,0, 0.0884,-0.1155,0,-0, 0,-0, 0,-0.0884,-0.0478,-0, 0.1155,0, 0, 0.0884,0, 0.0478, -0.125,-0, 0, 0.0884,0.0478,0, 0,-0.0478,-0, 0,-0, 0.125, 0,-0.0884,-0.0478,-0, 0, 0.0478,0,-0, 0,-0,-0.1155,0,-0, 0.0478,0.0884,0, 0,-0.0478,0.1155,0, 0,-0.0478,-0.0884,-0, 0,0.0478,-0, 0.0884,-0.0478,0, 0,-0, 0.0478,0.0884,-0.0884,0.0478,0,-0,-0.0478,-0,-0.0884,-0, 0,-0.0478,-0.0884,0, 0.0478,0.0884,0, 0,-0,-0,-0, 0.0478,0.0884,-0.0478,-0.0884]]) - # print(true_f0) + scale_range = range(0, 2) + diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] for i in scale_range: - # 2 ** i, 2 ** i - mesh = UnitCubeMesh(2 ** i, 1, 1) + mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + # mesh = UnitTetrahedronMesh() x = SpatialCoordinate(mesh) - - V = FunctionSpace(mesh, elem_code, deg) - res1 = project(V, mesh, expr(x)) - diff2[i - min(scale_range)] = res1 - v = TestFunction(V) - l = assemble(inner(expr(x), v)*dx) - f = assemble(interpolate(expr(x), V)) - # print(l.dat.data) - # print(f.dat.data) - # print(V.cell_node_list) - # # print(res1) - from ufl.algebra import Abs - - print(assemble(dot(dot(Abs(Jacobian(mesh)), as_vector((1,1,1))), as_vector((1,1,1)))*dx)) + # from firedrake.utility_meshes import TwoTetMesh + # mesh = TwoTetMesh() if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) + V = FunctionSpace(mesh, elem_code, deg) res2 = project(V2, mesh, expr(x)) diff[i - min(scale_range)] = res2 - v = TestFunction(V2) - l_a = assemble(inner(expr(x), v)*dx) - f_fuse = assemble(interpolate(expr(x), V2)) - breakpoint() + res1 = project(V, mesh, expr(x)) + diff2[i - min(scale_range)] = res1 + # v = TestFunction(V2) + # l_a = assemble(inner(expr(x), v)*dx) + # breakpoint() + # f_fuse = assemble(interpolate(expr(x), V2)) + # breakpoint() # print(l_a.dat.data) # print(f_fuse.dat.data) # print(V2.cell_node_list) @@ -663,6 +654,21 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): # print(f_fuse.dat.data - true_f01) # breakpoint() # assert np.allclose(res1, res2) + else: + V = FunctionSpace(mesh, elem_code, deg) + res1 = project(V, mesh, expr(x)) + diff2[i - min(scale_range)] = res1 + # v = TestFunction(V) + # l_a = assemble(inner(expr(x), v)*dx) + # breakpoint() + # v = TestFunction(V) + # l = assemble(inner(expr(x), v)*dx) + # f = assemble(interpolate(expr(x), V)) + # print(l.dat.data) + # print(f.dat.data) + # print(V.cell_node_list) + + print("firedrake l2 error norms:", diff2) diff2 = np.array(diff2) @@ -675,12 +681,72 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): diff = np.array(diff) conv2 = np.log2(diff[:-1] / diff[1:]) print("fuse convergence order:", conv2) - breakpoint() assert (np.array(conv2) > conv_rate).all() else: assert (np.array(conv1) > conv_rate).all() +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [ + # (create_dg1_tet, "DG", 1, 0.8), + # (create_cg1_tet, "CG", 1, 1.8), + # (create_cg2_tet, "CG", 2, 2.8), + # (create_cg3_tet, "CG", 3, 3.8), + (construct_tet_rt, "RT", 1, 0.8), + # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), + (construct_tet_ned, "N1curl", 1, 0.8)]) +def test_const_vec(elem_gen, elem_code, deg, conv_rate): + cell = make_tetrahedron() + elem = elem_gen(cell) + vec = as_vector([1, 1, 1]) + + scale_range = range(0, 4) + for i in scale_range: + # 2 ** i, 2 ** i + mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + # from firedrake.utility_meshes import TwoTetMesh, OneTetMesh + # error_gs = [] + # for g in sp.combinatorics.SymmetricGroup(4).elements: + # mesh = TwoTetMesh(perm=g) + print() + print(mesh.entity_orientations) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V2 = FunctionSpace(mesh, elem.to_ufl()) + res2 = assemble(interpolate(vec, V2)) + # print(g) + print(res2.dat.data) + # CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) + CG3 = VectorFunctionSpace(mesh, "CG", 3) + res3 = assemble(interpolate(res2, CG3)) + # print(res2.dat.data) + # print(res3.dat.data) + # print(res3) + for i in range(res3.dat.data.shape[0]): + if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): + # print("FAIL") + # error_gs += [g] + # break + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + print(V2.cell_node_list) + else: + V = FunctionSpace(mesh, elem_code, deg) + res1 = assemble(interpolate(vec, V)) + # print(g) + print(res1.dat.data) + CG3 = VectorFunctionSpace(mesh, "CG", 3) + res3 = assemble(interpolate(res1, CG3)) + for i in range(res3.dat.data.shape[0]): + if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): + # print("FAIL") + # error_gs += [g] + # break + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + print(V.cell_node_list) + # if len(error_gs) > 0: + # breakpoint() + + + + def test_tet_mesh(): i = 0 tet = make_tetrahedron() From cd34ead078b9033b63ba7348091594ffd0926123 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 18 Feb 2026 21:51:58 +0000 Subject: [PATCH 13/98] i think cg4 defined by fiat works --- fuse/cells.py | 3 +- fuse/triples.py | 51 ++++++++++++-- test/test_3d_examples_docs.py | 84 ++++++++++++++--------- test/test_convert_to_fiat.py | 122 ++++++++++++++++++++++++++++++---- 4 files changed, 209 insertions(+), 51 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 09de0c09..c163d609 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -477,7 +477,8 @@ def _subentity_traversal(self, sub_ents, min_ids): connections = [(c.point.id, c.point.group.identity) for c in self.connections] # if self.oriented: # connections = self.permute_entities(self.oriented, dim - 1) - # if self.dimension == 2: + if self.dimension == 2: + connections = connections[1:] + [connections[0]] # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: diff --git a/fuse/triples.py b/fuse/triples.py index 8dfde084..55f009ce 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -48,6 +48,8 @@ def __init__(self, cell, spaces, dof_gen): self.DOFGenerator = dof_gen self.flat = False + self.ref_el = None + def setup_ids_and_nodes(self): dofs = self.generate() degree = self.spaces[0].degree() @@ -71,6 +73,7 @@ def setup_ids_and_nodes(self): entity_ids[dim][dofs[i].cell_defined_on.id - min_ids[dim]].append(counter) nodes.append(dofs[i].convert_to_fiat(self.ref_el, degree, value_shape)) counter += 1 + self.nodes = nodes return entity_ids, nodes def setup_matrices(self): @@ -88,7 +91,39 @@ def setup_matrices(self): # for i in range(6): # matrices[2][j][i][j][j] = -1 * matrices[2][j][i][j][j] # breakpoint() + for i in range(4): + # entity_perms[2][i][5] = [0, 2, 1] + # entity_perms[2][i][2] = [1, 0, 2] + # entity_perms[2][i][1] = [2, 1, 0] + # (2,2) (2,1) pass rest fail, also what firedrake thinks they should be + # entity_perms[2][i][0] = [0, 1, 2] + # entity_perms[2][i][4] = [2, 0, 1] + # entity_perms[2][i][3] = [1, 2, 0] + # entity_perms[2][i][1] = [0, 2, 1] + # entity_perms[2][i][2] = [1, 0, 2] + # entity_perms[2][i][5] = [2, 1, 0] + # + # what matrices by entities comes up with + # entity_perms[2][i][0] = [0, 1, 2] + # entity_perms[2][i][4] = [1, 2, 0] + # entity_perms[2][i][3] = [2, 0, 1] + + entity_perms[2][i][1] = [0, 2, 1] + entity_perms[2][i][2] = [1, 0, 2] + entity_perms[2][i][5] = [2, 1, 0] + entity_perms[2][i][0] = [2, 0, 1] # these three work with rotated face + entity_perms[2][i][4] = [1, 2, 0] # + entity_perms[2][i][3] = [0, 1, 2] + # entity_perms[2][i][0] = [1, 2, 0] + # entity_perms[2][i][4] = [0, 1, 2] + # entity_perms[2][i][3] = [2, 0, 1] + # entity_perms[2][i][1] = [1, 2, 0] # + # entity_perms[2][i][2] = [0, 1, 2] + # entity_perms[2][i][5] = [2, 0, 1] # + # print(entity_perms[2][0]) + print([n.pt_dict for n in self.nodes[22:25]]) reversed_matrices = self.reverse_dof_perms(matrices) + self.pure_perm = pure_perm # self.pure_perm = False if self.pure_perm: @@ -146,12 +181,13 @@ def get_value_shape(self): return () def to_ufl(self): - # set up for eventual conversion to FIAT - self.ref_el = self.cell.to_fiat() - self.poly_set = self.spaces[0].to_ON_polynomial_set(self.ref_el) - self.entity_ids, self.nodes = self.setup_ids_and_nodes() - self.matrices, self.reversed_matrices = self.setup_matrices() - return FuseElement(self) + if self.ref_el is None: + # set up for eventual conversion to FIAT if not already done + self.ref_el = self.cell.to_fiat() + self.poly_set = self.spaces[0].to_ON_polynomial_set(self.ref_el) + self.entity_ids, self.nodes = self.setup_ids_and_nodes() + self.matrices, self.reversed_matrices = self.setup_matrices() + return FuseElement(self) def to_fiat(self): # call this to ensure set up is complete @@ -315,6 +351,7 @@ def make_mat(perm_g): return res_dict def make_overall_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): + raise NotImplementedError("this function should be unnecessary") min_ids = self.cell.get_starter_ids() dim = self.cell.dim() e = self.cell @@ -417,7 +454,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) - elif g in dof_gen_class[dim].g1.members() and dim < self.cell.dim(): + elif dim < self.cell.dim(): #g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on sub_mat = g.matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index d196b08a..16c16231 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -77,7 +77,8 @@ def construct_tet_cg4(cell=None): [DOFGenerator(xs, S2, S1), DOFGenerator(center, S1, S1)]) # xs = [DOF(DeltaPairing(), PointKernel((-1/np.sqrt(5), -0.26)))] - xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] + # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] + xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), DOFGenerator(xs, C3, S1)) @@ -121,8 +122,11 @@ def test_tet_cg3(): def test_tet_cg4(): cg4 = construct_tet_cg4() - + dofs = cg4.generate() cg4.plot(filename="tet_cg4.png") + # breakpoint()/ + cg4.to_ufl() + cg4.setup_ids_and_nodes() print(cg4.to_fiat()) @@ -156,8 +160,8 @@ def construct_tet_rt(cell=None): def construct_tet_ned(cell=None): deg = 1 - tri = make_tetrahedron() - edge = tri.edges()[0] + tet = make_tetrahedron() + edge = tet.edges()[0] x = sp.Symbol("x") y = sp.Symbol("y") @@ -176,46 +180,64 @@ def construct_tet_ned(cell=None): dofs = DOFGenerator(xs, S1, S2) edges = ElementTriple(edge, (vec_Pd, CellHCurl, L2), dofs) - xs = [immerse(tri, edges, TrHCurl)] + xs = [immerse(tet, edges, TrHCurl)] tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) # [test_tet_ned 1] - return ElementTriple(tri, (nd_space, CellHCurl, L2), [edge_dofs]) + return ElementTriple(tet, (nd_space, CellHCurl, L2), [edge_dofs]) + + +def construct_tet_nd2(tet=None): + if tet is None: + tet = make_tetrahedron() + deg = 2 + edge = tet.edges()[0] + face = tet.d_entities(2, get_class=True)[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dofs = DOFGenerator(xs, S2, S2) + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + v_2 = np.array(tet.get_node(tet.ordered_vertices()[2], return_coords=True)) + v_1 = np.array(tet.get_node(tet.ordered_vertices()[1], return_coords=True)) + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + center_dofs = DOFGenerator(xs, S2, S3) + face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) + + xs = [immerse(tet, int_ned1, TrHCurl)] + tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), + Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), + Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) + edge_dofs = DOFGenerator(xs, tet_edges, S1) + + im_xs = [immerse(tet, face_vec, TrHCurl)] + face_dofs = DOFGenerator(im_xs, tet_faces, S1) -# def construct_tet_nd2(tet=None): -# if tet is None: -# tet = polygon(3) -# deg = 2 -# edge = tri.edges()[0] -# x = sp.Symbol("x") -# y = sp.Symbol("y") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 -# xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] -# dofs = DOFGenerator(xs, S2, S2) -# int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) -# v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) -# v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) -# xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + ned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [edge_dofs, face_dofs]) + return ned -# center_dofs = DOFGenerator(xs, S2, S3) -# xs = [immerse(tri, int_ned1, TrHCurl)] -# tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), -# Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), -# Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) -# edge_dofs = DOFGenerator(xs, tet_edges, S1) -# vec_Pk = PolynomialSpace(deg - 1, set_shape=True) -# Pk = PolynomialSpace(deg - 1) -# M = sp.Matrix([[y, -x]]) -# nd = vec_Pk + (Pk.restrict(deg-2, deg-1))*M +def test_plot_tet_nd2(): + nd = construct_tet_nd2() + # nd.plot(filename="new.png") + nd.to_fiat() + breakpoint() -# ned = ElementTriple(tri, (nd, CellHCurl, C0), [edge_dofs, center_dofs]) -# return ned def plot_tet_rt(): rt = construct_tet_rt() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 7f2b7e1e..bcafa110 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -613,7 +613,7 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), (create_cg3_tet, "CG", 3, 3.8), - # (construct_tet_cg4, "CG", 4, 4.8), + (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), (construct_tet_ned, "N1curl", 1, 0.8)]) @@ -625,7 +625,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): expr = lambda x: as_vector([function(x), function(x), function(x)]) else: expr = function - scale_range = range(0, 2) + scale_range = range(0, 1) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] @@ -637,11 +637,11 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): # mesh = TwoTetMesh() if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) - V = FunctionSpace(mesh, elem_code, deg) + # V = FunctionSpace(mesh, elem_code, deg) res2 = project(V2, mesh, expr(x)) diff[i - min(scale_range)] = res2 - res1 = project(V, mesh, expr(x)) - diff2[i - min(scale_range)] = res1 + # res1 = project(V, mesh, expr(x)) + # diff2[i - min(scale_range)] = res1 # v = TestFunction(V2) # l_a = assemble(inner(expr(x), v)*dx) # breakpoint() @@ -670,18 +670,18 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): - print("firedrake l2 error norms:", diff2) - diff2 = np.array(diff2) - conv1 = np.log2(diff2[:-1] / diff2[1:]) - print("firedrake convergence order:", conv1) + # print("firedrake l2 error norms:", diff2) + # diff2 = np.array(diff2) + # conv1 = np.log2(diff2[:-1] / diff2[1:]) + # print("firedrake convergence order:", conv1) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): print("fuse l2 error norms:", diff) diff = np.array(diff) - conv2 = np.log2(diff[:-1] / diff[1:]) - print("fuse convergence order:", conv2) - assert (np.array(conv2) > conv_rate).all() + # conv2 = np.log2(diff[:-1] / diff[1:]) + # print("fuse convergence order:", conv2) + # assert (np.array(conv2) > conv_rate).all() else: assert (np.array(conv1) > conv_rate).all() @@ -746,6 +746,104 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_cg3_tet, "CG", 3, 3.8), + (construct_tet_cg4, "CG", 4, 4.8), + ]) +def test_const_two_tet(elem_gen, elem_code, deg, conv_rate): + cell = make_tetrahedron() + elem = elem_gen(cell) + ufl_elem = elem.to_ufl() + + from firedrake.utility_meshes import TwoTetMesh, OneTetMesh + results = [] + group = [sp.combinatorics.Permutation([0, 1, 2, 3]), + sp.combinatorics.Permutation([0, 2, 3, 1]), + sp.combinatorics.Permutation([0, 3, 1, 2]), + sp.combinatorics.Permutation([0, 1, 3, 2]), + sp.combinatorics.Permutation([0, 3, 2, 1]), + sp.combinatorics.Permutation([0, 2, 1, 3])] + orts = [2, 5, 1, 0, 3, 4] + # [0:3] + # sp.combinatorics.SymmetricGroup(4).elements[0:2] + # elem.to_fiat() + # breakpoint() + for g, o in zip(group, orts): + mesh = TwoTetMesh(perm=g) + x = SpatialCoordinate(mesh) + # from fuse.utils import orientation_value + # print(g, orientation_value([1, 0, 2], g([2, 0, 1, 2])[1:])) + print(mesh.entity_orientations) + # for i in range(4): + # elem.entity_perms[2][i][2] = [0, 1, 2] + # elem.entity_perms[2][i][o] = (~g)([-1, 1, 0, 2])[1:] + # print() + # print("2", elem.entity_perms[2][0][2]) + # print(o, " ", elem.entity_perms[2][0][o]) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + # pass + V2 = FunctionSpace(mesh, elem_code, deg) + fiat_perms = V2.finat_element.entity_permutations[2][0] + # for i, j in zip([0, 3, 4], [3, 4, 0]): + # # 3,4,0 fixes two + # V2.finat_element.entity_permutations[2][0][i] = [2, 0, 1] + # V2.finat_element.entity_permutations[2][0][j] + # V2.finat_element.entity_permutations[2][0][0] = [2, 0, 1] + # V2.finat_element.entity_permutations[2][0][3] = [0, 1, 2] + # V2.finat_element.entity_permutations[2][0][4] = [1, 2, 0] + + V2 = FunctionSpace(mesh, ufl_elem) + if g.is_identity: + original = dict(sorted(V2.finat_element.entity_permutations[2][0].items())) + print(original) + print(fiat_perms) + for i in range(4): + V2.finat_element.entity_permutations[2][i] = fiat_perms + V2.finat_element.entity_permutations[2][i][0] = [2, 0, 1] + V2.finat_element.entity_permutations[2][i][3] = [0, 1, 2] + V2.finat_element.entity_permutations[2][i][4] = [1, 2, 0] + print(V2.finat_element.entity_permutations[2][0]) + #somehow setting these here works but not if they are set before... + # print(V2.finat_element.cell.sub_entities[3][0]) + # print(V2.finat_element.cell.connectivity[(3,1)]) + # breakpoint() + # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V2)) + # cnl = V2.cell_node_list + # less_than = np.where(cnl < 25, True, False) + # greater_than = np.where(cnl > 21, True, False) + # print(res1.dat.data[cnl[:, 22:25]]) + # breakpoint() + res2 = project(V2, mesh, cos((3/4)*pi*x[0])) + print(res2) + # breakpoint() + # print(res2) + # results += [res2] + # print(res1.dat.data) + # results += [res1.dat.data] + # for i in range(res1.dat.data.shape[0]): + # assert np.allclose(res1.dat.data[i], 1) + # for i in range(res3.dat.data.shape[0]): + # if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): + # # print("FAIL") + # # error_gs += [g] + # # break + # assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + # print(V2.cell_node_list) + else: + V = FunctionSpace(mesh, elem_code, deg) + # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V)) + res2 = project(V, mesh, cos((3/4)*pi*x[0])) + print(g) + print(res2) + results += [res2] + # print(res1.dat.data) + # results += [res1.dat.data] + # for i in range(res1.dat.data.shape[0]): + # assert np.allclose(res1.dat.data[i], 1) + # print(V.cell_node_list) + for i in range(1, len(results)): + assert np.allclose(results[0], results[i], atol=1e-3) + breakpoint() + def test_tet_mesh(): i = 0 From 693e37460e97f9606fdeaa1a8a68641e7ae4f072 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 13:43:55 +0000 Subject: [PATCH 14/98] under very precise conditions cg4 works --- fuse/__init__.py | 2 +- fuse/cells.py | 2 ++ fuse/groups.py | 3 ++ fuse/triples.py | 23 +++++++------- test/test_3d_examples_docs.py | 3 +- test/test_convert_to_fiat.py | 56 ++++++++++++++++++++--------------- 6 files changed, 53 insertions(+), 36 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index 605f3cf9..6967c2ce 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,5 +1,5 @@ from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex -from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, tri_C3, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, get_cyc_group, get_sym_group +from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, tri_C3, 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, PolynomialKernel, ComponentKernel from fuse.triples import ElementTriple, DOFGenerator, immerse from fuse.traces import TrH1, TrGrad, TrHess, TrHCurl, TrHDiv diff --git a/fuse/cells.py b/fuse/cells.py index c163d609..8cb1efed 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -479,6 +479,8 @@ def _subentity_traversal(self, sub_ents, min_ids): # connections = self.permute_entities(self.oriented, dim - 1) if self.dimension == 2: connections = connections[1:] + [connections[0]] + # if self.dimension == 2: + # connections = [connections[-1]] + connections[:-1] # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: diff --git a/fuse/groups.py b/fuse/groups.py index 32298de3..5046192a 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -142,6 +142,7 @@ def __init__(self, perm_list, cell=None): self.identity = p_rep self._members.append(p_rep) counter += 1 + # self._members = sorted(self._members, key=lambda g: g.numeric_rep()) def add_cell(self, cell): return PermutationSetRepresentation(self.perm_list, cell=cell) @@ -252,6 +253,7 @@ def __init__(self, base_group, cell=None): # remaining_members = self.compute_reps(self.base_group.identity, # None, temp_group_elems) # assert (len(remaining_members) == 0) + # self._members = sorted(self._members, key=lambda g: g.numeric_rep()) else: self.cell = None @@ -400,6 +402,7 @@ def get_cyc_group(n): A3 = GroupRepresentation(AlternatingGroup(3)) tri_C3 = PermutationSetRepresentation([Permutation([0, 1, 2]), Permutation([2, 0, 1]), Permutation([1, 0, 2])]) +diff_C3 = PermutationSetRepresentation([Permutation([2, 0, 1]), Permutation([0, 1, 2]), Permutation([1, 2, 0])]) # tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([0, 2, 3, 1]), Permutation([1, 2, 0, 3]), # Permutation([0, 3, 1, 2]), Permutation([1, 3, 2, 0]), Permutation([2, 3, 0, 1])]) tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), Permutation([2, 3, 0, 1]), diff --git a/fuse/triples.py b/fuse/triples.py index 55f009ce..52178721 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -74,6 +74,8 @@ def setup_ids_and_nodes(self): nodes.append(dofs[i].convert_to_fiat(self.ref_el, degree, value_shape)) counter += 1 self.nodes = nodes + # for i in range(4): + # entity_ids[2][i] = [entity_ids[2][i][-1]] + entity_ids[2][i][:-1] return entity_ids, nodes def setup_matrices(self): @@ -91,7 +93,7 @@ def setup_matrices(self): # for i in range(6): # matrices[2][j][i][j][j] = -1 * matrices[2][j][i][j][j] # breakpoint() - for i in range(4): + # for i in range(4): # entity_perms[2][i][5] = [0, 2, 1] # entity_perms[2][i][2] = [1, 0, 2] # entity_perms[2][i][1] = [2, 1, 0] @@ -108,12 +110,12 @@ def setup_matrices(self): # entity_perms[2][i][4] = [1, 2, 0] # entity_perms[2][i][3] = [2, 0, 1] - entity_perms[2][i][1] = [0, 2, 1] - entity_perms[2][i][2] = [1, 0, 2] - entity_perms[2][i][5] = [2, 1, 0] - entity_perms[2][i][0] = [2, 0, 1] # these three work with rotated face - entity_perms[2][i][4] = [1, 2, 0] # - entity_perms[2][i][3] = [0, 1, 2] + # entity_perms[2][i][1] = [0, 2, 1] + # entity_perms[2][i][2] = [1, 0, 2] + # entity_perms[2][i][5] = [2, 1, 0] + # entity_perms[2][i][0] = [2, 0, 1] # these three work with rotated face + # entity_perms[2][i][4] = [1, 2, 0] # + # entity_perms[2][i][3] = [0, 1, 2] # entity_perms[2][i][0] = [1, 2, 0] # entity_perms[2][i][4] = [0, 1, 2] # entity_perms[2][i][3] = [2, 0, 1] @@ -187,7 +189,7 @@ def to_ufl(self): self.poly_set = self.spaces[0].to_ON_polynomial_set(self.ref_el) self.entity_ids, self.nodes = self.setup_ids_and_nodes() self.matrices, self.reversed_matrices = self.setup_matrices() - return FuseElement(self) + return FuseElement(self) def to_fiat(self): # call this to ensure set up is complete @@ -416,6 +418,7 @@ def _initialise_entity_dicts(self, dofs): members = e.group.members() oriented_mats_by_entity[dim][e_id] = {} flat_by_entity[dim][e_id] = {} + members = sorted(members, key=lambda g: g.numeric_rep()) for g in members: val = g.numeric_rep() oriented_mats_by_entity[dim][e_id][val] = dof_id_mat.copy() @@ -456,11 +459,11 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) elif dim < self.cell.dim(): #g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on - sub_mat = g.matrix_form() + sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim(): # case for dofs defined on the cell and not immersed - sub_mat = g.matrix_form() + sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() else: # TODO what if an orientation is not in G1 diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 16c16231..cefc25da 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -80,7 +80,8 @@ def construct_tet_cg4(cell=None): # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, C3, S1)) + DOFGenerator(xs, diff_C3, S1)) + xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index bcafa110..ae8f01c9 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -637,11 +637,11 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): # mesh = TwoTetMesh() if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) - # V = FunctionSpace(mesh, elem_code, deg) + V = FunctionSpace(mesh, elem_code, deg) res2 = project(V2, mesh, expr(x)) diff[i - min(scale_range)] = res2 - # res1 = project(V, mesh, expr(x)) - # diff2[i - min(scale_range)] = res1 + res1 = project(V, mesh, expr(x)) + diff2[i - min(scale_range)] = res1 # v = TestFunction(V2) # l_a = assemble(inner(expr(x), v)*dx) # breakpoint() @@ -670,7 +670,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): - # print("firedrake l2 error norms:", diff2) + print("firedrake l2 error norms:", diff2) # diff2 = np.array(diff2) # conv1 = np.log2(diff2[:-1] / diff2[1:]) # print("firedrake convergence order:", conv1) @@ -679,6 +679,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): print("fuse l2 error norms:", diff) diff = np.array(diff) + breakpoint() # conv2 = np.log2(diff[:-1] / diff[1:]) # print("fuse convergence order:", conv2) # assert (np.array(conv2) > conv_rate).all() @@ -698,14 +699,13 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) vec = as_vector([1, 1, 1]) - - scale_range = range(0, 4) + scale_range = range(0, 2) for i in scale_range: - # 2 ** i, 2 ** i mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) # from firedrake.utility_meshes import TwoTetMesh, OneTetMesh # error_gs = [] - # for g in sp.combinatorics.SymmetricGroup(4).elements: + + # for g in group: # mesh = TwoTetMesh(perm=g) print() print(mesh.entity_orientations) @@ -713,7 +713,7 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = assemble(interpolate(vec, V2)) # print(g) - print(res2.dat.data) + # print(res2.dat.data) # CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res2, CG3)) @@ -726,7 +726,6 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): # error_gs += [g] # break assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) - print(V2.cell_node_list) else: V = FunctionSpace(mesh, elem_code, deg) res1 = assemble(interpolate(vec, V)) @@ -781,8 +780,13 @@ def test_const_two_tet(elem_gen, elem_code, deg, conv_rate): # print(o, " ", elem.entity_perms[2][0][o]) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): # pass - V2 = FunctionSpace(mesh, elem_code, deg) - fiat_perms = V2.finat_element.entity_permutations[2][0] + V1 = FunctionSpace(mesh, elem_code, deg) + fiat_perms = V1.finat_element.entity_permutations[2][0] + print("FIAT") + res2 = project(V1, mesh, cos((3/4)*pi*x[0])) + print(res2) + # for i in range(22, 25): + # print(nodes[i].pt_dict) # for i, j in zip([0, 3, 4], [3, 4, 0]): # # 3,4,0 fixes two # V2.finat_element.entity_permutations[2][0][i] = [2, 0, 1] @@ -792,20 +796,24 @@ def test_const_two_tet(elem_gen, elem_code, deg, conv_rate): # V2.finat_element.entity_permutations[2][0][4] = [1, 2, 0] V2 = FunctionSpace(mesh, ufl_elem) - if g.is_identity: - original = dict(sorted(V2.finat_element.entity_permutations[2][0].items())) - print(original) - print(fiat_perms) - for i in range(4): - V2.finat_element.entity_permutations[2][i] = fiat_perms - V2.finat_element.entity_permutations[2][i][0] = [2, 0, 1] - V2.finat_element.entity_permutations[2][i][3] = [0, 1, 2] - V2.finat_element.entity_permutations[2][i][4] = [1, 2, 0] - print(V2.finat_element.entity_permutations[2][0]) - #somehow setting these here works but not if they are set before... + print("FUSE") + # for i in range(22, 25): + # print(nodes[i].pt_dict) + # if g.is_identity: + # original = V2.finat_element.entity_permutations[2][0] + # for i in range(4): + # print(dict(sorted(V2.finat_element.entity_permutations[2][i].items()))) + # print("o", original) + # print("fp", fiat_perms) + # for i in range(4): + # V2.finat_element.entity_permutations[2][i] = fiat_perms + # # V2.finat_element.entity_permutations[2][i][0] = [2, 0, 1] + # # V2.finat_element.entity_permutations[2][i][3] = [0, 1, 2] + # # V2.finat_element.entity_permutations[2][i][4] = [1, 2, 0] + # print(V2.finat_element.entity_permutations[2][0]) + # somehow setting these here works but not if they are set before... # print(V2.finat_element.cell.sub_entities[3][0]) # print(V2.finat_element.cell.connectivity[(3,1)]) - # breakpoint() # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V2)) # cnl = V2.cell_node_list # less_than = np.where(cnl < 25, True, False) From 9ccf3aad2bd14aee3327b748bf1d4f47548f340a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 14:27:23 +0000 Subject: [PATCH 15/98] tidy up, make real test --- fuse/cells.py | 1 + fuse/groups.py | 2 +- fuse/triples.py | 45 +---------- test/test_3d_examples_docs.py | 12 +-- test/test_convert_to_fiat.py | 147 ++++++---------------------------- 5 files changed, 30 insertions(+), 177 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 8cb1efed..f7528b75 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -477,6 +477,7 @@ def _subentity_traversal(self, sub_ents, min_ids): connections = [(c.point.id, c.point.group.identity) for c in self.connections] # if self.oriented: # connections = self.permute_entities(self.oriented, dim - 1) + # ensures compliance with FIAT entity orientations on Faces if self.dimension == 2: connections = connections[1:] + [connections[0]] # if self.dimension == 2: diff --git a/fuse/groups.py b/fuse/groups.py index 5046192a..7ef20b38 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -402,7 +402,7 @@ def get_cyc_group(n): A3 = GroupRepresentation(AlternatingGroup(3)) tri_C3 = PermutationSetRepresentation([Permutation([0, 1, 2]), Permutation([2, 0, 1]), Permutation([1, 0, 2])]) -diff_C3 = PermutationSetRepresentation([Permutation([2, 0, 1]), Permutation([0, 1, 2]), Permutation([1, 2, 0])]) +diff_C3 = PermutationSetRepresentation([Permutation([2, 0, 1]), Permutation([0, 1, 2]), Permutation([1, 2, 0])]) # this group is used for facet dofs # tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([0, 2, 3, 1]), Permutation([1, 2, 0, 3]), # Permutation([0, 3, 1, 2]), Permutation([1, 3, 2, 0]), Permutation([2, 3, 0, 1])]) tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), Permutation([2, 3, 0, 1]), diff --git a/fuse/triples.py b/fuse/triples.py index 52178721..e43f6ee1 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -81,49 +81,6 @@ def setup_ids_and_nodes(self): def setup_matrices(self): self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) - # breakpoint() - # matrices[2][0][4][0] = -1 * matrices[2][0][4][0] - # matrices[2][1][5][1] = -1 * matrices[2][1][5][1] - # matrices[2][3][1][3] = -1 * matrices[2][3][5][3] - # for j in range(4): - # for i in [1, 2, 5]: - # matrices[2][j][i] = np.eye(matrices[2][j][i].shape[0]) - # for j in [0, 1]: - # # for i in [0, 3, 4]: - # for i in range(6): - # matrices[2][j][i][j][j] = -1 * matrices[2][j][i][j][j] - # breakpoint() - # for i in range(4): - # entity_perms[2][i][5] = [0, 2, 1] - # entity_perms[2][i][2] = [1, 0, 2] - # entity_perms[2][i][1] = [2, 1, 0] - # (2,2) (2,1) pass rest fail, also what firedrake thinks they should be - # entity_perms[2][i][0] = [0, 1, 2] - # entity_perms[2][i][4] = [2, 0, 1] - # entity_perms[2][i][3] = [1, 2, 0] - # entity_perms[2][i][1] = [0, 2, 1] - # entity_perms[2][i][2] = [1, 0, 2] - # entity_perms[2][i][5] = [2, 1, 0] - # - # what matrices by entities comes up with - # entity_perms[2][i][0] = [0, 1, 2] - # entity_perms[2][i][4] = [1, 2, 0] - # entity_perms[2][i][3] = [2, 0, 1] - - # entity_perms[2][i][1] = [0, 2, 1] - # entity_perms[2][i][2] = [1, 0, 2] - # entity_perms[2][i][5] = [2, 1, 0] - # entity_perms[2][i][0] = [2, 0, 1] # these three work with rotated face - # entity_perms[2][i][4] = [1, 2, 0] # - # entity_perms[2][i][3] = [0, 1, 2] - # entity_perms[2][i][0] = [1, 2, 0] - # entity_perms[2][i][4] = [0, 1, 2] - # entity_perms[2][i][3] = [2, 0, 1] - # entity_perms[2][i][1] = [1, 2, 0] # - # entity_perms[2][i][2] = [0, 1, 2] - # entity_perms[2][i][5] = [2, 0, 1] # - # print(entity_perms[2][0]) - print([n.pt_dict for n in self.nodes[22:25]]) reversed_matrices = self.reverse_dof_perms(matrices) self.pure_perm = pure_perm @@ -457,7 +414,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) - elif dim < self.cell.dim(): #g in dof_gen_class[dim].g1.members() and + elif dim < self.cell.dim(): # g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index cefc25da..5a23a8b2 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -1,5 +1,4 @@ from fuse import * -import pytest from sympy.combinatorics import Permutation import sympy as sp import numpy as np @@ -61,6 +60,7 @@ def construct_tet_cg3(): return cg3 + def construct_tet_cg4(cell=None): tetra = make_tetrahedron() vert = tetra.vertices()[0] @@ -81,7 +81,6 @@ def construct_tet_cg4(cell=None): xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), DOFGenerator(xs, diff_C3, S1)) - xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) @@ -123,12 +122,9 @@ def test_tet_cg3(): def test_tet_cg4(): cg4 = construct_tet_cg4() - dofs = cg4.generate() + cg4.generate() cg4.plot(filename="tet_cg4.png") - # breakpoint()/ - cg4.to_ufl() - cg4.setup_ids_and_nodes() - print(cg4.to_fiat()) + cg4.to_fiat() def construct_tet_rt(cell=None): @@ -219,7 +215,6 @@ def construct_tet_nd2(tet=None): im_xs = [immerse(tet, face_vec, TrHCurl)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) - M1 = sp.Matrix([[0, z, -y]]) M2 = sp.Matrix([[z, 0, -x]]) M3 = sp.Matrix([[y, -x, 0]]) @@ -228,7 +223,6 @@ def construct_tet_nd2(tet=None): Pd = PolynomialSpace(deg - 1) nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 - ned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [edge_dofs, face_dofs]) return ned diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index ae8f01c9..c6834442 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -612,10 +612,9 @@ def test_project_3d(elem_gen, elem_code, deg): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_dg1_tet, "DG", 1, 0.8), (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), - (create_cg3_tet, "CG", 3, 3.8), - (construct_tet_cg4, "CG", 4, 4.8), + # (create_cg3_tet, "CG", 3, 3.8), + # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), - # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), (construct_tet_ned, "N1curl", 1, 0.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() @@ -625,7 +624,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): expr = lambda x: as_vector([function(x), function(x), function(x)]) else: expr = function - scale_range = range(0, 1) + scale_range = range(1, 4) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] @@ -668,32 +667,22 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): # print(f.dat.data) # print(V.cell_node_list) - - print("firedrake l2 error norms:", diff2) - # diff2 = np.array(diff2) - # conv1 = np.log2(diff2[:-1] / diff2[1:]) - # print("firedrake convergence order:", conv1) - + diff2 = np.array(diff2) + conv1 = np.log2(diff2[:-1] / diff2[1:]) + print("firedrake convergence order:", conv1) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): print("fuse l2 error norms:", diff) diff = np.array(diff) - breakpoint() - # conv2 = np.log2(diff[:-1] / diff[1:]) - # print("fuse convergence order:", conv2) - # assert (np.array(conv2) > conv_rate).all() + conv2 = np.log2(diff[:-1] / diff[1:]) + print("fuse convergence order:", conv2) + assert (np.array(conv2) > conv_rate).all() else: assert (np.array(conv1) > conv_rate).all() -@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [ - # (create_dg1_tet, "DG", 1, 0.8), - # (create_cg1_tet, "CG", 1, 1.8), - # (create_cg2_tet, "CG", 2, 2.8), - # (create_cg3_tet, "CG", 3, 3.8), - (construct_tet_rt, "RT", 1, 0.8), - # pytest.param(construct_tet_rt, "RT", 1, 0.8, marks=pytest.mark.xfail(reason='Orientations of faces not working')), +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8)]) def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() @@ -742,128 +731,40 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): print(V.cell_node_list) # if len(error_gs) > 0: # breakpoint() - -@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_cg3_tet, "CG", 3, 3.8), - (construct_tet_cg4, "CG", 4, 4.8), - ]) -def test_const_two_tet(elem_gen, elem_code, deg, conv_rate): +@pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), + (construct_tet_cg4, "CG", 4, 0.04)]) +def test_const_two_tet(elem_gen, elem_code, deg, max_err): cell = make_tetrahedron() elem = elem_gen(cell) ufl_elem = elem.to_ufl() - from firedrake.utility_meshes import TwoTetMesh, OneTetMesh - results = [] + from firedrake.utility_meshes import TwoTetMesh group = [sp.combinatorics.Permutation([0, 1, 2, 3]), sp.combinatorics.Permutation([0, 2, 3, 1]), sp.combinatorics.Permutation([0, 3, 1, 2]), sp.combinatorics.Permutation([0, 1, 3, 2]), sp.combinatorics.Permutation([0, 3, 2, 1]), sp.combinatorics.Permutation([0, 2, 1, 3])] - orts = [2, 5, 1, 0, 3, 4] - # [0:3] - # sp.combinatorics.SymmetricGroup(4).elements[0:2] - # elem.to_fiat() - # breakpoint() - for g, o in zip(group, orts): + + for g in group: mesh = TwoTetMesh(perm=g) x = SpatialCoordinate(mesh) - # from fuse.utils import orientation_value - # print(g, orientation_value([1, 0, 2], g([2, 0, 1, 2])[1:])) - print(mesh.entity_orientations) - # for i in range(4): - # elem.entity_perms[2][i][2] = [0, 1, 2] - # elem.entity_perms[2][i][o] = (~g)([-1, 1, 0, 2])[1:] - # print() - # print("2", elem.entity_perms[2][0][2]) - # print(o, " ", elem.entity_perms[2][0][o]) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): # pass V1 = FunctionSpace(mesh, elem_code, deg) - fiat_perms = V1.finat_element.entity_permutations[2][0] print("FIAT") - res2 = project(V1, mesh, cos((3/4)*pi*x[0])) - print(res2) - # for i in range(22, 25): - # print(nodes[i].pt_dict) - # for i, j in zip([0, 3, 4], [3, 4, 0]): - # # 3,4,0 fixes two - # V2.finat_element.entity_permutations[2][0][i] = [2, 0, 1] - # V2.finat_element.entity_permutations[2][0][j] - # V2.finat_element.entity_permutations[2][0][0] = [2, 0, 1] - # V2.finat_element.entity_permutations[2][0][3] = [0, 1, 2] - # V2.finat_element.entity_permutations[2][0][4] = [1, 2, 0] - + res = project(V1, mesh, cos((3/4)*pi*x[0])) + print(res) + assert res < max_err V2 = FunctionSpace(mesh, ufl_elem) print("FUSE") - # for i in range(22, 25): - # print(nodes[i].pt_dict) - # if g.is_identity: - # original = V2.finat_element.entity_permutations[2][0] - # for i in range(4): - # print(dict(sorted(V2.finat_element.entity_permutations[2][i].items()))) - # print("o", original) - # print("fp", fiat_perms) - # for i in range(4): - # V2.finat_element.entity_permutations[2][i] = fiat_perms - # # V2.finat_element.entity_permutations[2][i][0] = [2, 0, 1] - # # V2.finat_element.entity_permutations[2][i][3] = [0, 1, 2] - # # V2.finat_element.entity_permutations[2][i][4] = [1, 2, 0] - # print(V2.finat_element.entity_permutations[2][0]) - # somehow setting these here works but not if they are set before... - # print(V2.finat_element.cell.sub_entities[3][0]) - # print(V2.finat_element.cell.connectivity[(3,1)]) - # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V2)) - # cnl = V2.cell_node_list - # less_than = np.where(cnl < 25, True, False) - # greater_than = np.where(cnl > 21, True, False) - # print(res1.dat.data[cnl[:, 22:25]]) - # breakpoint() - res2 = project(V2, mesh, cos((3/4)*pi*x[0])) - print(res2) - # breakpoint() - # print(res2) - # results += [res2] - # print(res1.dat.data) - # results += [res1.dat.data] - # for i in range(res1.dat.data.shape[0]): - # assert np.allclose(res1.dat.data[i], 1) - # for i in range(res3.dat.data.shape[0]): - # if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): - # # print("FAIL") - # # error_gs += [g] - # # break - # assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) - # print(V2.cell_node_list) + res = project(V2, mesh, cos((3/4)*pi*x[0])) + print(res) + assert res < max_err else: V = FunctionSpace(mesh, elem_code, deg) # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V)) - res2 = project(V, mesh, cos((3/4)*pi*x[0])) - print(g) - print(res2) - results += [res2] - # print(res1.dat.data) - # results += [res1.dat.data] - # for i in range(res1.dat.data.shape[0]): - # assert np.allclose(res1.dat.data[i], 1) - # print(V.cell_node_list) - for i in range(1, len(results)): - assert np.allclose(results[0], results[i], atol=1e-3) - breakpoint() - - -def test_tet_mesh(): - i = 0 - tet = make_tetrahedron() - mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) - x = SpatialCoordinate(mesh) - print(mesh.entity_orientations) - elem = construct_tet_rt(tet) - V = FunctionSpace(mesh, elem.to_ufl()) - # V = FunctionSpace(mesh, "RT", 1) - function = lambda x: cos((3/4)*pi*x[0]) - expr = lambda x: as_vector([function(x), function(x), function(x)]) - f = assemble(interpolate(expr(x), V)) - print(assemble(dot(dot(Jacobian(mesh), as_vector((1,1,1))), as_vector((1,1,1)))*dx)) - breakpoint() \ No newline at end of file + res = project(V, mesh, cos((3/4)*pi*x[0])) + assert res < max_err From 36f08b3445e91c5b3166396a626ba46331fd68d1 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 14:42:39 +0000 Subject: [PATCH 16/98] fix branch --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2615731..be52266d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: /usr/bin/git config --global --add safe.directory /opt/firedrake/ cd /opt/firedrake/ git fetch - git checkout indiamai/indiamai/fuse-closures-with-pyop3 + git checkout indiamai/fuse-closures-with-pyop3 git pull pip install pybind11 Cython make From 109dfb063474600896fc5590aba170fcdc4bb004 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 14:48:25 +0000 Subject: [PATCH 17/98] try pip installing firedrake and tidy --- .github/workflows/test.yml | 3 +- test/test_convert_to_fiat.py | 56 ++---------------------------------- 2 files changed, 5 insertions(+), 54 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be52266d..b26e11ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,8 @@ jobs: git fetch git checkout indiamai/fuse-closures-with-pyop3 git pull - pip install pybind11 Cython + python3 -m pip install pybind11 Cython + python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make - name: Install checkedout fuse run: | diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index c6834442..876654a6 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -612,7 +612,7 @@ def test_project_3d(elem_gen, elem_code, deg): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(create_dg1_tet, "DG", 1, 0.8), (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), - # (create_cg3_tet, "CG", 3, 3.8), + (create_cg3_tet, "CG", 3, 3.8), # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8)]) @@ -630,10 +630,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): diff2 = [0 for i in scale_range] for i in scale_range: mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) - # mesh = UnitTetrahedronMesh() x = SpatialCoordinate(mesh) - # from firedrake.utility_meshes import TwoTetMesh - # mesh = TwoTetMesh() if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) V = FunctionSpace(mesh, elem_code, deg) @@ -641,31 +638,10 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): diff[i - min(scale_range)] = res2 res1 = project(V, mesh, expr(x)) diff2[i - min(scale_range)] = res1 - # v = TestFunction(V2) - # l_a = assemble(inner(expr(x), v)*dx) - # breakpoint() - # f_fuse = assemble(interpolate(expr(x), V2)) - # breakpoint() - # print(l_a.dat.data) - # print(f_fuse.dat.data) - # print(V2.cell_node_list) - # print(res2) - # print(f_fuse.dat.data - true_f01) - # breakpoint() - # assert np.allclose(res1, res2) else: V = FunctionSpace(mesh, elem_code, deg) res1 = project(V, mesh, expr(x)) diff2[i - min(scale_range)] = res1 - # v = TestFunction(V) - # l_a = assemble(inner(expr(x), v)*dx) - # breakpoint() - # v = TestFunction(V) - # l = assemble(inner(expr(x), v)*dx) - # f = assemble(interpolate(expr(x), V)) - # print(l.dat.data) - # print(f.dat.data) - # print(V.cell_node_list) print("firedrake l2 error norms:", diff2) diff2 = np.array(diff2) @@ -691,46 +667,20 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): scale_range = range(0, 2) for i in scale_range: mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) - # from firedrake.utility_meshes import TwoTetMesh, OneTetMesh - # error_gs = [] - - # for g in group: - # mesh = TwoTetMesh(perm=g) - print() - print(mesh.entity_orientations) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = assemble(interpolate(vec, V2)) - # print(g) - # print(res2.dat.data) - # CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res2, CG3)) - # print(res2.dat.data) - # print(res3.dat.data) - # print(res3) for i in range(res3.dat.data.shape[0]): - if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): - # print("FAIL") - # error_gs += [g] - # break - assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) else: V = FunctionSpace(mesh, elem_code, deg) res1 = assemble(interpolate(vec, V)) - # print(g) - print(res1.dat.data) CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res1, CG3)) for i in range(res3.dat.data.shape[0]): - if not np.allclose(res3.dat.data[i], np.array([1, 1, 1])): - # print("FAIL") - # error_gs += [g] - # break - assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) - print(V.cell_node_list) - # if len(error_gs) > 0: - # breakpoint() + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) @pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), From ba8526a0b9d5b3886a8b0c96cd5e0ebc7a308ca2 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 14:54:31 +0000 Subject: [PATCH 18/98] try changing docker image --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b26e11ee..2d8c9621 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: # Run on the Github hosted runner runs-on: ubuntu-latest container: - #image: firedrakeproject/firedrake-vanilla-default:latest - image: firedrakeproject/firedrake-vanilla-default:dev-main + image: firedrakeproject/firedrake-vanilla-default:latest + # image: firedrakeproject/firedrake-vanilla-default:dev-main # Steps represent a sequence of tasks that will be executed as # part of the jobs steps: @@ -32,7 +32,7 @@ jobs: python3 -m pip install pybind11 Cython python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make - - name: Install checkedout fuse + - name: Install checked out fuse run: | python3 -m pip install --break-system-packages -e '.[dev]' - name: Checkout correct FIAT branch From 80d172bf0d0b45197e22d198d9e146841bc9dea7 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:00:02 +0000 Subject: [PATCH 19/98] try changing docker image --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d8c9621..21380220 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,7 @@ jobs: # Run on the Github hosted runner runs-on: ubuntu-latest container: - image: firedrakeproject/firedrake-vanilla-default:latest - # image: firedrakeproject/firedrake-vanilla-default:dev-main + image: firedrakeproject/firedrake-vanilla-default:dev-main # Steps represent a sequence of tasks that will be executed as # part of the jobs steps: @@ -29,6 +28,7 @@ jobs: git fetch git checkout indiamai/fuse-closures-with-pyop3 git pull + echo $PETSC_DIR python3 -m pip install pybind11 Cython python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make From 70e8951ade42e9527b68731398612a55017eb8af Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:02:39 +0000 Subject: [PATCH 20/98] experiment with petsctools --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21380220..8141fdda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,8 @@ jobs: git checkout indiamai/fuse-closures-with-pyop3 git pull echo $PETSC_DIR - python3 -m pip install pybind11 Cython + pip list + python3 -m pip install pybind11 Cython petsctools python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make - name: Install checked out fuse From a70a981c6d20344de2d5d319d47b19f6633224f2 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:09:44 +0000 Subject: [PATCH 21/98] try purging and specifically install petsc4py --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8141fdda..6bf5d3a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,9 @@ jobs: git fetch git checkout indiamai/fuse-closures-with-pyop3 git pull - echo $PETSC_DIR + pip cache purge + pip install $PETSC_DIR/src/binding/petsc4py + pip install -r ./firedrake/requirements-build.txt pip list python3 -m pip install pybind11 Cython petsctools python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' From e06063f7c2acc066a40d24ad266f478245ec7c78 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:13:07 +0000 Subject: [PATCH 22/98] fix directory --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bf5d3a5..de9ab0e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: git pull pip cache purge pip install $PETSC_DIR/src/binding/petsc4py - pip install -r ./firedrake/requirements-build.txt + pip install -r ./requirements-build.txt pip list python3 -m pip install pybind11 Cython petsctools python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' From e4bd65fe4d605c29d7a29a016e46d20318ff910b Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:36:35 +0000 Subject: [PATCH 23/98] remove added petsctools --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de9ab0e8..a3ca5d36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,6 @@ jobs: pip install $PETSC_DIR/src/binding/petsc4py pip install -r ./requirements-build.txt pip list - python3 -m pip install pybind11 Cython petsctools python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make - name: Install checked out fuse From ec7756ecf1b6a0ee7a83776550222ad3aeb9b1b3 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:44:04 +0000 Subject: [PATCH 24/98] try installing specific version of petsctools --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3ca5d36..09b00288 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,7 @@ jobs: pip cache purge pip install $PETSC_DIR/src/binding/petsc4py pip install -r ./requirements-build.txt + pip install git+git@github.com:firedrakeproject/petsctools.git@connorjward/cpetsc pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make From 32d82c43a5e0ed43ce6245eb8780e99ebc782e94 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:46:38 +0000 Subject: [PATCH 25/98] try installing specific version of petsctools - better url --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09b00288..f276c16f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: pip cache purge pip install $PETSC_DIR/src/binding/petsc4py pip install -r ./requirements-build.txt - pip install git+git@github.com:firedrakeproject/petsctools.git@connorjward/cpetsc + pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make From e7da80036d360efe3f20c936555d9844ff7d4a3b Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:50:22 +0000 Subject: [PATCH 26/98] uninstall + python -m everywhere --- .github/workflows/test.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f276c16f..cb0574e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,11 +28,12 @@ jobs: git fetch git checkout indiamai/fuse-closures-with-pyop3 git pull - pip cache purge - pip install $PETSC_DIR/src/binding/petsc4py - pip install -r ./requirements-build.txt - pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc - pip list + python3 -m pip cache purge + python3 -m pip install $PETSC_DIR/src/binding/petsc4py + python3 -m pip install -r ./requirements-build.txt + python3 -m pip uninstall petsctools + python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc + python3 -m pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' make - name: Install checked out fuse From 9fc414fc6331e8af4822409c238ad0e441adbc13 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 15:52:37 +0000 Subject: [PATCH 27/98] remove uninstall --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb0574e0..657f4a65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py python3 -m pip install -r ./requirements-build.txt - python3 -m pip uninstall petsctools + # python3 -m pip uninstall petsctools python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc python3 -m pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' From eb96b9a31d31f63cb5a45c661f017f8a75613245 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 16:11:56 +0000 Subject: [PATCH 28/98] fix syntax issue --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 657f4a65..b6001399 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,6 @@ jobs: python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py python3 -m pip install -r ./requirements-build.txt - # python3 -m pip uninstall petsctools python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc python3 -m pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' From 6edc9195eccba2dd00bd141e3fa2dbd90b21ba34 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:03:15 +0000 Subject: [PATCH 29/98] try installing fresh firedrake --- .github/workflows/test.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6001399..84552ae0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,12 +22,15 @@ jobs: run: echo "HOME=/root" >> "$GITHUB_ENV" - uses: actions/checkout@v4 - name: Checkout correct firedrake branch - run: | + run: | /usr/bin/git config --global --add safe.directory /opt/firedrake/ - cd /opt/firedrake/ - git fetch - git checkout indiamai/fuse-closures-with-pyop3 - git pull + rm -rf /opt/firedrake + cd /opt/ + git clone https://github.com/firedrakeproject/firedrake.git --branch indiamai/fuse-closures-with-pyop3 + # cd /opt/firedrake/ + # git fetch + # git checkout indiamai/fuse-closures-with-pyop3 + # git pull python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py python3 -m pip install -r ./requirements-build.txt From c9b9dd9af0b273abaa789c8abc91168aacaaf17c Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:05:06 +0000 Subject: [PATCH 30/98] fix syntax --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84552ae0..5b4c37e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,10 @@ on: branches: - main # And all pull requests + # cd /opt/firedrake/ + # git fetch + # git checkout indiamai/fuse-closures-with-pyop3 + # git pull pull_request: jobs: @@ -27,10 +31,7 @@ jobs: rm -rf /opt/firedrake cd /opt/ git clone https://github.com/firedrakeproject/firedrake.git --branch indiamai/fuse-closures-with-pyop3 - # cd /opt/firedrake/ - # git fetch - # git checkout indiamai/fuse-closures-with-pyop3 - # git pull + python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py python3 -m pip install -r ./requirements-build.txt From 297d5435acd055eaa5f68ce0459952a70ea1c586 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:07:42 +0000 Subject: [PATCH 31/98] add directory back in --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b4c37e0..73f9cf3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py - python3 -m pip install -r ./requirements-build.txt + python3 -m pip install -r ./firedrake/requirements-build.txt python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc python3 -m pip list python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' From 97673f3774e8cda802838a953f48f4be30777e86 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:10:26 +0000 Subject: [PATCH 32/98] add directory back in... again --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 73f9cf3c..a4f26d2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: python3 -m pip install -r ./firedrake/requirements-build.txt python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc python3 -m pip list - python3 -m pip install --no-build-isolation --no-binary h5py --editable './[check,docs]' + python3 -m pip install --no-build-isolation --no-binary h5py --editable './firedrake[check,docs]' make - name: Install checked out fuse run: | From 2e9c9f388920f5a3605865a9fa934196f7ead354 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:21:02 +0000 Subject: [PATCH 33/98] try building everything from scratch --- .github/workflows/test.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4f26d2c..1717a662 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,21 +17,27 @@ jobs: name: Run tests # Run on the Github hosted runner runs-on: ubuntu-latest - container: - image: firedrakeproject/firedrake-vanilla-default:dev-main + # container: + # image: ubuntu-late + # image: firedrakeproject/firedrake-vanilla-default:dev-main # Steps represent a sequence of tasks that will be executed as # part of the jobs steps: - name: fix home run: echo "HOME=/root" >> "$GITHUB_ENV" - uses: actions/checkout@v4 - - name: Checkout correct firedrake branch + - name: Checkout correct firedrake branch/install firedrake run: | + curl -O https://raw.githubusercontent.com/firedrakeproject/firedrake/release/scripts/firedrake-configure + sudo apt install $(python3 firedrake-configure --show-system-packages) + git clone https://gitlab.com/petsc/petsc.git + cd petsc + python3 ../firedrake-configure --show-petsc-configure-options | xargs -L1 ./configure + make PETSC_DIR=/path/to/petsc PETSC_ARCH=arch-firedrake-default all + make check + cd .. /usr/bin/git config --global --add safe.directory /opt/firedrake/ - rm -rf /opt/firedrake - cd /opt/ git clone https://github.com/firedrakeproject/firedrake.git --branch indiamai/fuse-closures-with-pyop3 - python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py python3 -m pip install -r ./firedrake/requirements-build.txt From cc0c9f15b0173efa7cce5ae0fb64b1eb31c709c4 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:34:29 +0000 Subject: [PATCH 34/98] more fixing --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1717a662..c29f2bc0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,8 @@ jobs: - uses: actions/checkout@v4 - name: Checkout correct firedrake branch/install firedrake run: | + /usr/bin/git config --global --add safe.directory /opt/ + cd /opt/ curl -O https://raw.githubusercontent.com/firedrakeproject/firedrake/release/scripts/firedrake-configure sudo apt install $(python3 firedrake-configure --show-system-packages) git clone https://gitlab.com/petsc/petsc.git @@ -36,7 +38,7 @@ jobs: make PETSC_DIR=/path/to/petsc PETSC_ARCH=arch-firedrake-default all make check cd .. - /usr/bin/git config --global --add safe.directory /opt/firedrake/ + git clone https://github.com/firedrakeproject/firedrake.git --branch indiamai/fuse-closures-with-pyop3 python3 -m pip cache purge python3 -m pip install $PETSC_DIR/src/binding/petsc4py From ed978838e5753f75c0dc13edf2f2055c522db2a0 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 19 Feb 2026 18:54:25 +0000 Subject: [PATCH 35/98] more fixing --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c29f2bc0..2ba1dd20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: steps: - name: fix home run: echo "HOME=/root" >> "$GITHUB_ENV" - - uses: actions/checkout@v4 + # - uses: actions/checkout@v4 - name: Checkout correct firedrake branch/install firedrake run: | /usr/bin/git config --global --add safe.directory /opt/ From 4f5ce53a94c2e8abe398ac2cf8277443fb1a0d8c Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 13:18:46 +0000 Subject: [PATCH 36/98] steal firedrake workflow --- .github/workflows/test.yml | 230 ++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 90 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ba1dd20..82c6cf9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,101 +13,151 @@ on: pull_request: jobs: - test: - name: Run tests - # Run on the Github hosted runner - runs-on: ubuntu-latest - # container: - # image: ubuntu-late - # image: firedrakeproject/firedrake-vanilla-default:dev-main - # Steps represent a sequence of tasks that will be executed as - # part of the jobs + test_linux: + name: Build and test Firedrake (Linux) + strategy: + # We want to know all of the tests which fail, so don't kill real if + # complex fails and vice-versa + fail-fast: false + matrix: + arch: [default, complex] + runs-on: [self-hosted, Linux] + container: + image: ubuntu:latest + env: + OMPI_ALLOW_RUN_AS_ROOT: 1 + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1 + OMP_NUM_THREADS: 1 + OPENBLAS_NUM_THREADS: 1 + FIREDRAKE_CI: 1 + PYOP2_SPMD_STRICT: 1 + # NOTE: One should occasionally update test_durations.json by running + # 'make test_durations' inside a 'firedrake:latest' Docker image. + EXTRA_PYTEST_ARGS: --splitting-algorithm least_duration --timeout=600 --timeout-method=thread -o faulthandler_timeout=660 --durations-path=./firedrake-repo/tests/test_durations.json --durations=50 + PYTEST_MPI_MAX_NPROCS: 8 steps: - - name: fix home + - name: Fix HOME + # For unknown reasons GitHub actions overwrite HOME to /github/home + # which will break everything unless fixed + # (https://github.com/actions/runner/issues/863) run: echo "HOME=/root" >> "$GITHUB_ENV" - # - uses: actions/checkout@v4 - - name: Checkout correct firedrake branch/install firedrake - run: | - /usr/bin/git config --global --add safe.directory /opt/ - cd /opt/ - curl -O https://raw.githubusercontent.com/firedrakeproject/firedrake/release/scripts/firedrake-configure - sudo apt install $(python3 firedrake-configure --show-system-packages) - git clone https://gitlab.com/petsc/petsc.git - cd petsc - python3 ../firedrake-configure --show-petsc-configure-options | xargs -L1 ./configure - make PETSC_DIR=/path/to/petsc PETSC_ARCH=arch-firedrake-default all - make check - cd .. - - git clone https://github.com/firedrakeproject/firedrake.git --branch indiamai/fuse-closures-with-pyop3 - python3 -m pip cache purge - python3 -m pip install $PETSC_DIR/src/binding/petsc4py - python3 -m pip install -r ./firedrake/requirements-build.txt - python3 -m pip install git+https://github.com/firedrakeproject/petsctools.git@connorjward/cpetsc - python3 -m pip list - python3 -m pip install --no-build-isolation --no-binary h5py --editable './firedrake[check,docs]' - make - - name: Install checked out fuse + + - name: Pre-run cleanup + # Make sure the current directory is empty + run: find . -delete + + # Use a different mirror to fetch apt packages from to get around + # temporary outage. + # (https://askubuntu.com/questions/1549622/problem-with-archive-ubuntu-com-most-of-the-servers-are-not-responding) + # The mirror was chosen from https://launchpad.net/ubuntu/+archivemirrors. + - name: Configure apt + run: | + sed -i 's|http://archive.ubuntu.com/ubuntu|http://www.mirrorservice.org/sites/archive.ubuntu.com/ubuntu/|g' /etc/apt/sources.list.d/ubuntu.sources + apt-get update + + # Git is needed for actions/checkout and Python for firedrake-configure + - name: Install system dependencies (1) + run: apt-get -y install git python3 + + - uses: actions/checkout@v5 + with: + name: firedrakeproject/firedrake + path: firedrake-repo + ref: refs/heads/indiamai/fuse-closures-with-pyop3 + + - name: Validate single source of truth + run: ./firedrake-repo/scripts/check-config + + # Check that the Dockerfile is using the latest Ubuntu version. + # The version is hardcoded into the Dockerfile so that the OS + # for each release is fixed. + - name: Check Dockerfile Ubuntu version run: | - python3 -m pip install --break-system-packages -e '.[dev]' - - name: Checkout correct FIAT branch + latest_version=$(grep "VERSION_ID=" /etc/os-release | cut -d '"' -f 2) + docker_version=$(grep FROM firedrake-repo/docker/Dockerfile.vanilla | cut -d ':' -f 2) + echo "Latest version: $latest_version" + echo "Docker version: $docker_version" + if [[ "$docker_version" != "$latest_version" ]]; then + echo "Ubuntu version ${docker_version} in Dockerfile is out of date with latest version ${latest_version}" + exit 1 + fi + + - name: Install system dependencies (2) run: | - /usr/bin/git config --global --add safe.directory ~/ - cd ~ - git clone https://github.com/firedrakeproject/fiat.git - /usr/bin/git config --global --add safe.directory ~/fiat - cd fiat - git fetch - git checkout indiamai/integrate_fuse - git status - python3 -m pip install --break-system-packages -e . - - name: Run tests + apt-get -y install \ + $(python3 ./firedrake-repo/scripts/firedrake-configure --arch ${{ matrix.arch }} --show-system-packages) + apt-get -y install python3-venv + : # Dependencies needed to run the test suite + apt-get -y install fonts-dejavu graphviz graphviz-dev parallel poppler-utils + + - name: Install PETSc + run: | + git clone --depth 1 https://gitlab.com/petsc/petsc.git + cd petsc + python3 ../firedrake-repo/scripts/firedrake-configure \ + --arch ${{ matrix.arch }} --show-petsc-configure-options | \ + xargs -L1 ./configure --with-make-np=8 --download-slepc + make PETSC_DIR=/__w/firedrake/firedrake/petsc PETSC_ARCH=arch-firedrake-${{ matrix.arch }} + make check + { + echo "PETSC_DIR=/__w/firedrake/firedrake/petsc" + echo "PETSC_ARCH=arch-firedrake-${{ matrix.arch }}" + echo "SLEPC_DIR=/__w/firedrake/firedrake/petsc/arch-firedrake-${{ matrix.arch }}" + } >> "$GITHUB_ENV" + + - name: Install Firedrake + id: install run: | + export $(python3 ./firedrake-repo/scripts/firedrake-configure --arch "${{ matrix.arch }}" --show-env) + python3 -m venv venv + . venv/bin/activate + + : # Empty the pip cache to ensure that everything is compiled from scratch + pip cache purge + + : # Fix for petsc4py+slepc4py build + echo 'setuptools<81' > constraints.txt + export PIP_CONSTRAINT=constraints.txt + + : # Install build dependencies + pip install "$PETSC_DIR"/src/binding/petsc4py + pip install -r ./firedrake-repo/requirements-build.txt + + : # Install runtime dependencies that have been removed from the pyproject.toml + : # because they rely on non-PyPI versions of petsc4py. + pip install --no-build-isolation --no-deps \ + "$PETSC_DIR"/"$PETSC_ARCH"/externalpackages/git.slepc/src/binding/slepc4py + pip install --no-deps git+https://github.com/NGSolve/ngsPETSc.git netgen-mesher netgen-occt + + : # We have to pass '--no-build-isolation' to use a custom petsc4py + EXTRA_BUILD_ARGS='--no-isolation' + EXTRA_PIP_FLAGS='--no-build-isolation' + + : # Install from an sdist so we can make sure that it is not ill-formed + pip install build + python -m build ./firedrake-repo --sdist $EXTRA_BUILD_ARGS + + pip install --verbose $EXTRA_PIP_FLAGS \ + --no-binary h5py \ + --extra-index-url https://download.pytorch.org/whl/cpu \ + "$(echo ./firedrake-repo/dist/firedrake-*.tar.gz)[ci]" + + firedrake-clean pip list - make tests - - name: Upload coverage + + - name: Upload sdist (default ARCH only) + if: matrix.arch == 'default' uses: actions/upload-artifact@v4 with: - name: covdata - include-hidden-files: true - path: .coverage.* - - coverage: - name: Coverage - needs: test - runs-on: ubuntu-latest - if: success() && github.ref == 'refs/heads/main' - container: - image: firedrakeproject/firedrake-vanilla-default:latest - # Steps represent a sequence of tasks that will be executed as - # part of the jobs - steps: - - name: "Check out the repo" - uses: "actions/checkout@v2" - - name: Install checkedout fuse - run: | - python3 -m pip install --break-system-packages -e '.[dev]' - - name: "Download coverage data" - uses: actions/download-artifact@v4 - with: - name: covdata - - - name: "Combine" - run: | - make coverage - export TOTAL=$(python3 -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") - echo "total=$TOTAL" >> $GITHUB_ENV - echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY - - - name: "Make badge" - uses: schneegans/dynamic-badges-action@v1.4.0 - with: - # GIST_TOKEN is a GitHub personal access token with scope "gist". - auth: ${{ secrets.GIST_SECRET }} - gistID: 8d09e14999153441dba99d1759e90707 # replace with your real Gist id. - filename: covbadge.json - label: Coverage - message: ${{ env.total }}% - minColorRange: 50 - maxColorRange: 90 - valColorRange: ${{ env.total }} + name: dist + path: firedrake-repo/dist/* + + - name: Report sdist build status + id: report_sdist + run: echo "conclusion=success" >> "$GITHUB_OUTPUT" + + - name: Run firedrake-check + run: | + . venv/bin/activate + firedrake-check + timeout-minutes: 5 From dcc2e12c9f97be7bdf287ddfbe4914d305d2934d Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 13:27:50 +0000 Subject: [PATCH 37/98] incorrect attribute --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82c6cf9e..fbab6ef7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: - uses: actions/checkout@v5 with: - name: firedrakeproject/firedrake + repository: firedrakeproject/firedrake path: firedrake-repo ref: refs/heads/indiamai/fuse-closures-with-pyop3 From 803c19efb6025b3ff5445e504e728022071a1d16 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 13:33:26 +0000 Subject: [PATCH 38/98] change petsc path --- .github/workflows/test.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbab6ef7..4247160d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,5 @@ name: Run tests +# I stole this from the main firedrake repo on: # Run on pushes to main @@ -20,7 +21,7 @@ jobs: # complex fails and vice-versa fail-fast: false matrix: - arch: [default, complex] + arch: [default] runs-on: [self-hosted, Linux] container: image: ubuntu:latest @@ -97,12 +98,12 @@ jobs: python3 ../firedrake-repo/scripts/firedrake-configure \ --arch ${{ matrix.arch }} --show-petsc-configure-options | \ xargs -L1 ./configure --with-make-np=8 --download-slepc - make PETSC_DIR=/__w/firedrake/firedrake/petsc PETSC_ARCH=arch-firedrake-${{ matrix.arch }} + make PETSC_DIR=/__w/fuse/fuse/petsc PETSC_ARCH=arch-firedrake-${{ matrix.arch }} make check { - echo "PETSC_DIR=/__w/firedrake/firedrake/petsc" + echo "PETSC_DIR=/__w/fuse/fuse/petsc" echo "PETSC_ARCH=arch-firedrake-${{ matrix.arch }}" - echo "SLEPC_DIR=/__w/firedrake/firedrake/petsc/arch-firedrake-${{ matrix.arch }}" + echo "SLEPC_DIR=/__w/fuse/fuse/petsc/arch-firedrake-${{ matrix.arch }}" } >> "$GITHUB_ENV" - name: Install Firedrake @@ -161,3 +162,7 @@ jobs: . venv/bin/activate firedrake-check timeout-minutes: 5 + + - uses: actions/checkout@v5 + with: + path: fuse-repo From 3d60b8546026fe176fb642247b3edad48c456719 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 13:52:57 +0000 Subject: [PATCH 39/98] try different firedrake branch --- .github/workflows/test.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4247160d..86e231ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: with: repository: firedrakeproject/firedrake path: firedrake-repo - ref: refs/heads/indiamai/fuse-closures-with-pyop3 + ref: refs/heads/connorjward/pyop3-develop - name: Validate single source of truth run: ./firedrake-repo/scripts/check-config @@ -166,3 +166,17 @@ jobs: - uses: actions/checkout@v5 with: path: fuse-repo + + - name: Install checked out fuse + run: | + python3 -m pip install --break-system-packages -e './fuse-repo[dev]' + + - uses: actions/checkout@v5 + with: + repository: firedrakeproject/fiat + path: fiat-repo + ref: refs/heads/indiamai/integrate_fuse + + - name: Install checked out FIAT + run: | + python3 -m pip install --break-system-packages -e './fiat-repo' From 82c94f9de96a4764efa42ff1501e201eff9c489a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 14:15:53 +0000 Subject: [PATCH 40/98] try different firedrake branch --- .github/workflows/test.yml | 61 ++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86e231ad..9ba7bf6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,12 +156,6 @@ jobs: - name: Report sdist build status id: report_sdist run: echo "conclusion=success" >> "$GITHUB_OUTPUT" - - - name: Run firedrake-check - run: | - . venv/bin/activate - firedrake-check - timeout-minutes: 5 - uses: actions/checkout@v5 with: @@ -169,6 +163,7 @@ jobs: - name: Install checked out fuse run: | + . venv/bin/activate python3 -m pip install --break-system-packages -e './fuse-repo[dev]' - uses: actions/checkout@v5 @@ -179,4 +174,58 @@ jobs: - name: Install checked out FIAT run: | + . venv/bin/activate python3 -m pip install --break-system-packages -e './fiat-repo' + + - name: Run tests + run: | + . venv/bin/activate + pip list + make tests + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: covdata + include-hidden-files: true + path: .coverage.* + + coverage: + name: Coverage + needs: test + runs-on: ubuntu-latest + if: success() && github.ref == 'refs/heads/main' + container: + image: firedrakeproject/firedrake-vanilla-default:latest + # Steps represent a sequence of tasks that will be executed as + # part of the jobs + steps: + - name: "Check out the repo" + uses: "actions/checkout@v2" + - name: Install checkedout fuse + run: | + python3 -m pip install --break-system-packages -e '.[dev]' + - name: "Download coverage data" + uses: actions/download-artifact@v4 + with: + name: covdata + + - name: "Combine" + run: | + make coverage + export TOTAL=$(python3 -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") + echo "total=$TOTAL" >> $GITHUB_ENV + echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY + + - name: "Make badge" + uses: schneegans/dynamic-badges-action@v1.4.0 + with: + # GIST_TOKEN is a GitHub personal access token with scope "gist". + auth: ${{ secrets.GIST_SECRET }} + gistID: 8d09e14999153441dba99d1759e90707 # replace with your real Gist id. + filename: covbadge.json + label: Coverage + message: ${{ env.total }}% + minColorRange: 50 + maxColorRange: 90 + valColorRange: ${{ env.total }} + From 0a7c56b4a7b30ce5744b11e6f8e1108322bf941a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 14:22:43 +0000 Subject: [PATCH 41/98] try different firedrake branch --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ba7bf6c..3970ed92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ on: pull_request: jobs: - test_linux: + test: name: Build and test Firedrake (Linux) strategy: # We want to know all of the tests which fail, so don't kill real if @@ -64,7 +64,7 @@ jobs: with: repository: firedrakeproject/firedrake path: firedrake-repo - ref: refs/heads/connorjward/pyop3-develop + ref: refs/heads/connorjward/fuse-cleanup - name: Validate single source of truth run: ./firedrake-repo/scripts/check-config From cb7e95230964a7fa7f24345677351502a402a9f9 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 16:55:58 +0000 Subject: [PATCH 42/98] finalise sub ents change --- fuse/cells.py | 5 +---- test/test_3d_examples_docs.py | 2 +- test/test_cells.py | 16 +++++++++++++ test/test_convert_to_fiat.py | 42 ++++++++++++++++++++++++++++++----- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index f7528b75..d15aff7c 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -477,11 +477,8 @@ def _subentity_traversal(self, sub_ents, min_ids): connections = [(c.point.id, c.point.group.identity) for c in self.connections] # if self.oriented: # connections = self.permute_entities(self.oriented, dim - 1) - # ensures compliance with FIAT entity orientations on Faces if self.dimension == 2: - connections = connections[1:] + [connections[0]] - # if self.dimension == 2: - # connections = [connections[-1]] + connections[:-1] + connections = [connections[-1]] + connections[:-1] # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 5a23a8b2..0176c855 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -80,7 +80,7 @@ def construct_tet_cg4(cell=None): # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, diff_C3, S1)) + DOFGenerator(xs, C3, S1)) xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) diff --git a/test/test_cells.py b/test/test_cells.py index ced8e36d..782d480f 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -293,3 +293,19 @@ def make_entity_cone_lists(fiat_cell): _n = _n1 _offset_list.append(_offset) return _list, _offset_list + + + +def test_tet_sub_ents(): + tet = make_tetrahedron() + flip_false = tet.get_sub_entities(flip=False) + flip_true = tet.get_sub_entities(flip=True) + print("False") + for i in range(3): + for j in range(3): + print(f"({i},{j})", flip_false[i][j]) + print("True") + for i in range(3): + for j in range(3): + print(f"({i},{j})", flip_true[i][j]) + breakpoint() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 876654a6..962f5ed0 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -683,6 +683,38 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), + (construct_tet_ned, "N1curl", 1, 0.8)]) +def test_const_vec_two_tet(elem_gen, elem_code, deg, conv_rate): + cell = make_tetrahedron() + elem = elem_gen(cell) + vec = as_vector([1, 1, 1]) + from firedrake.utility_meshes import TwoTetMesh + group = [sp.combinatorics.Permutation([0, 1, 2, 3]), + sp.combinatorics.Permutation([0, 2, 3, 1]), + sp.combinatorics.Permutation([0, 3, 1, 2]), + sp.combinatorics.Permutation([0, 1, 3, 2]), + sp.combinatorics.Permutation([0, 3, 2, 1]), + sp.combinatorics.Permutation([0, 2, 1, 3])] + + for g in group: + mesh = TwoTetMesh(perm=g) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V2 = FunctionSpace(mesh, elem.to_ufl()) + res2 = assemble(interpolate(vec, V2)) + CG3 = VectorFunctionSpace(mesh, "CG", 3) + res3 = assemble(interpolate(res2, CG3)) + for i in range(res3.dat.data.shape[0]): + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + else: + V = FunctionSpace(mesh, elem_code, deg) + res1 = assemble(interpolate(vec, V)) + CG3 = VectorFunctionSpace(mesh, "CG", 3) + res3 = assemble(interpolate(res1, CG3)) + for i in range(res3.dat.data.shape[0]): + assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + + @pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), (construct_tet_cg4, "CG", 4, 0.04)]) def test_const_two_tet(elem_gen, elem_code, deg, max_err): @@ -703,11 +735,11 @@ def test_const_two_tet(elem_gen, elem_code, deg, max_err): x = SpatialCoordinate(mesh) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): # pass - V1 = FunctionSpace(mesh, elem_code, deg) - print("FIAT") - res = project(V1, mesh, cos((3/4)*pi*x[0])) - print(res) - assert res < max_err + # V1 = FunctionSpace(mesh, elem_code, deg) + # print("FIAT") + # res = project(V1, mesh, cos((3/4)*pi*x[0])) + # print(res) + # assert res < max_err V2 = FunctionSpace(mesh, ufl_elem) print("FUSE") res = project(V2, mesh, cos((3/4)*pi*x[0])) From e312e847cbb469558f97418cdad5e92c994b03e2 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 23 Feb 2026 17:24:50 +0000 Subject: [PATCH 43/98] try installing one branch then using another --- .github/workflows/test.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3970ed92..bca1437a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: with: repository: firedrakeproject/firedrake path: firedrake-repo - ref: refs/heads/connorjward/fuse-cleanup + ref: refs/heads/connorjward/pyop3-develop - name: Validate single source of truth run: ./firedrake-repo/scripts/check-config @@ -132,7 +132,7 @@ jobs: : # We have to pass '--no-build-isolation' to use a custom petsc4py EXTRA_BUILD_ARGS='--no-isolation' - EXTRA_PIP_FLAGS='--no-build-isolation' + EXTRA_PIP_FLAGS='-e --no-build-isolation' : # Install from an sdist so we can make sure that it is not ill-formed pip install build @@ -146,16 +146,11 @@ jobs: firedrake-clean pip list - - name: Upload sdist (default ARCH only) - if: matrix.arch == 'default' - uses: actions/upload-artifact@v4 + - uses: actions/checkout@v5 with: - name: dist - path: firedrake-repo/dist/* - - - name: Report sdist build status - id: report_sdist - run: echo "conclusion=success" >> "$GITHUB_OUTPUT" + repository: firedrakeproject/firedrake + path: firedrake-repo + ref: refs/heads/indiamai/fuse-closures-with-pyop3 - uses: actions/checkout@v5 with: From c9cf9e02d8ff3d76f9ba132f7ded4271c739c2e1 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 24 Feb 2026 16:21:08 +0000 Subject: [PATCH 44/98] try not building sdist to get editable --- .github/workflows/test.yml | 13 +++++++++---- fuse/dof.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bca1437a..998aeadd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -132,16 +132,21 @@ jobs: : # We have to pass '--no-build-isolation' to use a custom petsc4py EXTRA_BUILD_ARGS='--no-isolation' - EXTRA_PIP_FLAGS='-e --no-build-isolation' + EXTRA_PIP_FLAGS='--no-build-isolation' : # Install from an sdist so we can make sure that it is not ill-formed - pip install build - python -m build ./firedrake-repo --sdist $EXTRA_BUILD_ARGS + : # pip install build + : # python -m build ./firedrake-repo --sdist $EXTRA_BUILD_ARGS + + : #pip install --verbose $EXTRA_PIP_FLAGS \ + : # --no-binary h5py \ + : # --extra-index-url https://download.pytorch.org/whl/cpu \ + : # "$(echo ./firedrake-repo/dist/firedrake-*.tar.gz)[ci]" pip install --verbose $EXTRA_PIP_FLAGS \ --no-binary h5py \ --extra-index-url https://download.pytorch.org/whl/cpu \ - "$(echo ./firedrake-repo/dist/firedrake-*.tar.gz)[ci]" + -e ./[ci] firedrake-clean pip list diff --git a/fuse/dof.py b/fuse/dof.py index 88544f65..3ad53357 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -1,5 +1,6 @@ from FIAT.functional import Functional from fuse.utils import sympy_to_numpy +from fuse.traces import TrH1 import numpy as np import sympy as sp @@ -173,7 +174,10 @@ def __init__(self, x, g=None): super(VectorKernel, self).__init__() def __repr__(self): - x = list(map(str, list(self.pt))) + if isinstance(self.pt, tuple): + x = list(map(str, list(self.pt))) + else: + x = [str(self.pt)] return ','.join(x) def degree(self, interpolant_degree): @@ -186,7 +190,7 @@ def __call__(self, *args): return self.pt def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): - if immersed: + 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*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] @@ -342,23 +346,34 @@ def to_quadrature(self, arg_degree): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) else: basis_change = np.eye(dim) + + if self.immersed and isinstance(self.kernel, VectorKernel): + # this probably needs generalising + basis_change = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*bv) for bv in basis_change]) pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed, self.cell.dimension) if self.immersed: # need to compute jacobian from attachment. - pts = [self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts] - immersion = self.target_space.tabulate(wts, self.pairing.entity) + pts = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*pt) for pt in pts]) + # 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.pairing.entity) # Special case - force evaluation on different orientation of entity for construction of matrix transforms if self.entity_o: immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) - - wts = np.outer(wts, immersion) + # print("after change", immersion) + if isinstance(self.target_space, TrH1): + new_wts = wts + else: + new_wts = np.outer(wts, immersion) + else: + new_wts = wts # else: # pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed) - - # pt dict is { pt: (weight, component)} - pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, wts, comps)} + # pt dict is { pt: [(weight, component)]} + pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} return pt_dict def __repr__(self, fn="v"): From f0a27b1760d3d62be7997bc147893e7b79a688b8 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 24 Feb 2026 16:33:58 +0000 Subject: [PATCH 45/98] fix directory --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 998aeadd..7f264a44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -146,7 +146,7 @@ jobs: pip install --verbose $EXTRA_PIP_FLAGS \ --no-binary h5py \ --extra-index-url https://download.pytorch.org/whl/cpu \ - -e ./[ci] + -e ./firedrake-repo[ci] firedrake-clean pip list From 6123d48c752ca434064db9593a05f6a9fd9eb649 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 24 Feb 2026 16:52:08 +0000 Subject: [PATCH 46/98] cd into fuse before making test --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f264a44..68092f13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -180,6 +180,7 @@ jobs: - name: Run tests run: | . venv/bin/activate + cd ./fuse-repo pip list make tests - name: Upload coverage From ff901a6b1a7aa1e0a52ce65393121b3f8d4f6047 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 24 Feb 2026 17:16:06 +0000 Subject: [PATCH 47/98] run make --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68092f13..c4153676 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -157,6 +157,12 @@ jobs: path: firedrake-repo ref: refs/heads/indiamai/fuse-closures-with-pyop3 + - name: Make cython + run: | + . venv/bin/activate + cd ./firedrake-repo + make + - uses: actions/checkout@v5 with: path: fuse-repo From 45028d322995469400d1e9f15ce4aece5524ae5f Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 24 Feb 2026 19:06:15 +0000 Subject: [PATCH 48/98] Fix tests now i have ci --- fuse/traces.py | 6 ++- fuse/triples.py | 28 +++++++---- test/test_2d_examples_docs.py | 4 +- test/test_3d_examples_docs.py | 50 ++++++++++--------- test/test_cells.py | 16 ------ test/test_convert_to_fiat.py | 94 ++++++++++++++++++++++++++++------- test/test_orientations.py | 15 +++--- 7 files changed, 130 insertions(+), 83 deletions(-) diff --git a/fuse/traces.py b/fuse/traces.py index a699eaae..f96f77ef 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -54,8 +54,9 @@ def plot(self, ax, coord, trace_entity, **kwargs): def to_tikz(self, coord, trace_entity, scale, color="black"): return f"\\filldraw[{color}] {numpy_to_str_tuple(coord, scale)} circle (2pt) node[anchor = south] {{}};" - def tabulate(self, Qwts, trace_entity): - return Qwts + def tabulate(self, Qpts, trace_entity): + return np.ones(len(Qpts)) + # return Qwts def __repr__(self): return "H1" @@ -123,6 +124,7 @@ def tabulate(self, Qwts, trace_entity): subEntityBasis = np.array(self.domain.basis_vectors(entity=trace_entity)) # result = np.matmul(tangent, subEntityBasis) return subEntityBasis + # return result def plot(self, ax, coord, trace_entity, **kwargs): vec = self.tabulate([], trace_entity).squeeze() diff --git a/fuse/triples.py b/fuse/triples.py index e43f6ee1..68f6ff89 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -27,7 +27,7 @@ class ElementTriple(): :param: dof_gen: Generator Triple to generate the degrees of freedom. """ - def __init__(self, cell, spaces, dof_gen): + def __init__(self, cell, spaces, dof_gen, perm=True): assert isinstance(cell, Point) or isinstance(cell, TensorProductPoint) if isinstance(dof_gen, DOFGenerator): dof_gen = [dof_gen] @@ -50,6 +50,10 @@ def __init__(self, cell, spaces, dof_gen): self.ref_el = None + self.dofs = None + self.dofs = self.generate() + self.perm = perm + def setup_ids_and_nodes(self): dofs = self.generate() degree = self.spaces[0].degree() @@ -82,9 +86,10 @@ def setup_matrices(self): self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) reversed_matrices = self.reverse_dof_perms(matrices) - - self.pure_perm = pure_perm - # self.pure_perm = False + if self.perm: + self.pure_perm = pure_perm + else: + self.pure_perm = False if self.pure_perm: self.apply_matrices = False else: @@ -98,13 +103,14 @@ def __repr__(self): @cache def generate(self): - res = [] - id_counter = 0 - for dof_gen in self.DOFGenerator: - generated = dof_gen.generate(self.cell, self.spaces[1], id_counter) - res.extend(generated) - id_counter += len(generated) - return res + if self.dofs is None: + self.dofs = [] + id_counter = 0 + for dof_gen in self.DOFGenerator: + generated = dof_gen.generate(self.cell, self.spaces[1], id_counter) + self.dofs.extend(generated) + id_counter += len(generated) + return self.dofs def __iter__(self): yield self.cell diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index a6af1e96..21447d32 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -166,7 +166,7 @@ def construct_nd(tri=None): # xs = [DOF(L2Pairing(), PointKernel(edge.basis_vectors()[0]))] # xs = [DOF(L2Pairing(), PointKernel((1,)))] - xs = [DOF(L2Pairing(), VectorKernel((1,)))] + xs = [DOF(L2Pairing(), VectorKernel(1))] dofs = DOFGenerator(xs, S1, S2) int_ned = ElementTriple(edge, (P1, CellHCurl, C0), dofs) @@ -216,7 +216,7 @@ def construct_rt(tri=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), VectorKernel((1,)))] + xs = [DOF(L2Pairing(), VectorKernel(1))] dofs = DOFGenerator(xs, S1, S2) int_rt = ElementTriple(edge, (vec_Pd, CellHDiv, C0), dofs) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 0176c855..0646f0c8 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -61,7 +61,7 @@ def construct_tet_cg3(): return cg3 -def construct_tet_cg4(cell=None): +def construct_tet_cg4(cell=None, perm=True): tetra = make_tetrahedron() vert = tetra.vertices()[0] edge = tetra.edges()[0] @@ -69,18 +69,18 @@ def construct_tet_cg4(cell=None): xs = [DOF(DeltaPairing(), PointKernel(()))] dg0 = ElementTriple(vert, (P0, CellL2, "C0"), - DOFGenerator(xs, S1, S1)) + DOFGenerator(xs, S1, S1), perm) xs = [DOF(DeltaPairing(), PointKernel((-np.sqrt(3/7),)))] center = [DOF(DeltaPairing(), PointKernel((0,)))] dg2_int = ElementTriple(edge, (P2, CellL2, "C0"), - [DOFGenerator(xs, S2, S1), DOFGenerator(center, S1, S1)]) + [DOFGenerator(xs, S2, S1), DOFGenerator(center, S1, S1)], perm) # xs = [DOF(DeltaPairing(), PointKernel((-1/np.sqrt(5), -0.26)))] # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, C3, S1)) + DOFGenerator(xs, C3, S1), perm) xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) @@ -96,7 +96,7 @@ def construct_tet_cg4(cell=None): P4 = PolynomialSpace(4) cg4 = ElementTriple(tetra, (P4, CellH1, "C0"), - [cgverts, cgedges, cgfaces, int_dof]) + [cgverts, cgedges, cgfaces, int_dof], perm) return cg4 @@ -142,7 +142,7 @@ def construct_tet_rt(cell=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), VectorKernel((1,)))] + xs = [DOF(L2Pairing(), VectorKernel(1))] dofs = DOFGenerator(xs, S1, S3) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) @@ -173,7 +173,7 @@ def construct_tet_ned(cell=None): nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 # [test_tet_ned 0] - xs = [DOF(L2Pairing(), VectorKernel((1,)))] + xs = [DOF(L2Pairing(), VectorKernel(1))] dofs = DOFGenerator(xs, S1, S2) edges = ElementTriple(edge, (vec_Pd, CellHCurl, L2), dofs) @@ -187,7 +187,7 @@ def construct_tet_ned(cell=None): return ElementTriple(tet, (nd_space, CellHCurl, L2), [edge_dofs]) -def construct_tet_nd2(tet=None): +def construct_tet_ned2(tet=None): if tet is None: tet = make_tetrahedron() deg = 2 @@ -196,15 +196,29 @@ def construct_tet_nd2(tet=None): x = sp.Symbol("x") y = sp.Symbol("y") z = sp.Symbol("z") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) - v_2 = np.array(tet.get_node(tet.ordered_vertices()[2], return_coords=True)) - v_1 = np.array(tet.get_node(tet.ordered_vertices()[1], return_coords=True)) + + v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) + v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] center_dofs = DOFGenerator(xs, S2, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) + im_xs = [immerse(tet, face_vec, TrH1)] + face_dofs = DOFGenerator(im_xs, tet_faces, S1) + # tempned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [face_dofs]) + # ptdicts = [d.to_quadrature(1) for d in tempned.generate()] + # print(ptdicts[0]) + # print(ptdicts[1]) xs = [immerse(tet, int_ned1, TrHCurl)] tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), @@ -212,26 +226,14 @@ def construct_tet_nd2(tet=None): Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) - im_xs = [immerse(tet, face_vec, TrHCurl)] - face_dofs = DOFGenerator(im_xs, tet_faces, S1) - - M1 = sp.Matrix([[0, z, -y]]) - M2 = sp.Matrix([[z, 0, -x]]) - M3 = sp.Matrix([[y, -x, 0]]) - - vec_Pd = PolynomialSpace(deg - 1, set_shape=True) - Pd = PolynomialSpace(deg - 1) - nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 - ned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [edge_dofs, face_dofs]) return ned -def test_plot_tet_nd2(): - nd = construct_tet_nd2() +def test_plot_tet_ned2(): + nd = construct_tet_ned2() # nd.plot(filename="new.png") nd.to_fiat() - breakpoint() def plot_tet_rt(): diff --git a/test/test_cells.py b/test/test_cells.py index 782d480f..ced8e36d 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -293,19 +293,3 @@ def make_entity_cone_lists(fiat_cell): _n = _n1 _offset_list.append(_offset) return _list, _offset_list - - - -def test_tet_sub_ents(): - tet = make_tetrahedron() - flip_false = tet.get_sub_entities(flip=False) - flip_true = tet.get_sub_entities(flip=True) - print("False") - for i in range(3): - for j in range(3): - print(f"({i},{j})", flip_false[i][j]) - print("True") - for i in range(3): - for j in range(3): - print(f"({i},{j})", flip_true[i][j]) - breakpoint() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 962f5ed0..a30bbba5 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 -from test_3d_examples_docs import construct_tet_rt, construct_tet_ned, construct_tet_cg4 +from test_3d_examples_docs import construct_tet_rt, construct_tet_ned, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -213,7 +213,7 @@ def create_cg2_tet(cell): return cg2 -def create_cg3_tet(cell): +def create_cg3_tet(cell, perm=True): vert = cell.vertices()[0] edge = cell.edges()[0] @@ -241,7 +241,7 @@ def create_cg3_tet(cell): cgfaces = DOFGenerator(f_xs, tet_faces, S1) cg3 = ElementTriple(cell, (P3, CellH1, "C0"), - [cgverts, cgedges, cgfaces]) + [cgverts, cgedges, cgfaces], perm) return cg3 @@ -500,9 +500,7 @@ def helmholtz_solve(V, mesh): np.set_printoptions(precision=4, suppress=True) print() # print(assemble(a).M.values) - l_a = assemble(L) - print(l_a.dat.data[0:2]) - print(V.cell_node_list[0:2]) + # l_a = assemble(L) # print(mesh.entity_orientations) # breakpoint() @@ -615,7 +613,8 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg3_tet, "CG", 3, 3.8), # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), - (construct_tet_ned, "N1curl", 1, 0.8)]) + (construct_tet_ned, "N1curl", 1, 0.8), + (construct_tet_ned2, "N1curl", 2, 0.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -659,7 +658,8 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), - (construct_tet_ned, "N1curl", 1, 0.8)]) + (construct_tet_ned, "N1curl", 1, 0.8), + (construct_tet_ned2, "N1curl", 2, 1.8)]) def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -684,7 +684,8 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), - (construct_tet_ned, "N1curl", 1, 0.8)]) + (construct_tet_ned, "N1curl", 1, 0.8), + (construct_tet_ned2, "N1curl", 2, 1.8)]) def test_const_vec_two_tet(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -719,8 +720,10 @@ def test_const_vec_two_tet(elem_gen, elem_code, deg, conv_rate): (construct_tet_cg4, "CG", 4, 0.04)]) def test_const_two_tet(elem_gen, elem_code, deg, max_err): cell = make_tetrahedron() - elem = elem_gen(cell) - ufl_elem = elem.to_ufl() + elem_perms = elem_gen(cell, perm=True) + elem_mats = elem_gen(cell, perm=False) + ufl_elem_perms = elem_perms.to_ufl() + ufl_elem_mats = elem_mats.to_ufl() from firedrake.utility_meshes import TwoTetMesh group = [sp.combinatorics.Permutation([0, 1, 2, 3]), @@ -732,16 +735,16 @@ def test_const_two_tet(elem_gen, elem_code, deg, max_err): for g in group: mesh = TwoTetMesh(perm=g) + print(g) + print(mesh.entity_orientations) x = SpatialCoordinate(mesh) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): - # pass - # V1 = FunctionSpace(mesh, elem_code, deg) - # print("FIAT") - # res = project(V1, mesh, cos((3/4)*pi*x[0])) - # print(res) - # assert res < max_err - V2 = FunctionSpace(mesh, ufl_elem) - print("FUSE") + V = FunctionSpace(mesh, ufl_elem_perms) + res = project(V, mesh, cos((3/4)*pi*x[0])) + print(res) + assert res < max_err + + V2 = FunctionSpace(mesh, ufl_elem_mats) res = project(V2, mesh, cos((3/4)*pi*x[0])) print(res) assert res < max_err @@ -750,3 +753,56 @@ def test_const_two_tet(elem_gen, elem_code, deg, max_err): # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V)) res = project(V, mesh, cos((3/4)*pi*x[0])) assert res < max_err + + +# TODO this is not a real test +def test_scaling_mesh(): + mesh1 = RectangleMesh(2, 1, 1, 1) + mesh2 = RectangleMesh(2, 1, 0.5, 1) + vec = as_vector([1, 1]) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + + elem = construct_rt(polygon(3)) + V1 = FunctionSpace(mesh1, elem.to_ufl()) + V2 = FunctionSpace(mesh2, elem.to_ufl()) + else: + V1 = FunctionSpace(mesh1, "RT", 1) + V2 = FunctionSpace(mesh2, "RT", 1) + + res1 = assemble(interpolate(vec, V1)) + print(res1.dat.data) + res2 = assemble(interpolate(vec, V2)) + print(res2.dat.data) + + +@pytest.mark.skip(reason="Obscenely slow") +def test_quartic_poisson_solve(): + # Create mesh and define function space + r = 1 + m = UnitCubeMesh(2 ** r, 2 ** r, 2 ** r) + x = SpatialCoordinate(m) + V = FunctionSpace(m, "CG", 4) + + # Define variational problem + u = TrialFunction(V) + v = TestFunction(V) + a = dot(grad(u), grad(v)) * dx + u_e = x[0]*x[0]*x[0]*x[0] + 2*x[0]*x[1]*x[1] + 3*x[2]*x[2]*x[2]*x[2] + 6 + # u_e = 1 + x[0]*x[0] + 2*x[1]*x[1] + 3*x[2]*x[2] + + bcs = [DirichletBC(V, u_e, "on_boundary")] + f = Function(V) + f.interpolate(-12*x[0]*x[0] - 4*x[0] - 36*x[2]*x[2]) + # f = Constant(-12.0) + L = f*v*dx + + # Compute solution + u_r = Function(V) + solve(a == L, u_r, bcs=bcs, solver_parameters={'ksp_type': 'cg', 'pc_type': 'lu'}) + + true = Function(V) + true.interpolate(u_e) + + res = sqrt(assemble(inner(u_r - true, u_r - true) * dx)) + print(res) + assert np.allclose(res, 0) diff --git a/test/test_orientations.py b/test/test_orientations.py index 8146d0f0..af7fd788 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -216,7 +216,7 @@ def get_expression(V): exact = Function(FunctionSpace(mesh, 'CG', 5)) expression = x + y + z elif len(shape) == 1: - exact = Function(FunctionSpace(mesh, 'CG', 5)) + exact = Function(VectorFunctionSpace(mesh, 'CG', 5)) expression = as_vector([x, y, z]) return expression, exact @@ -324,7 +324,7 @@ def test_interpolation(elem_gen, elem_code, deg): expression, _ = get_expression(V) expect = project(expression, V) f = assemble(interpolate(expression, V)) - assert np.allclose(f.dat.data, expect.dat.data) + assert np.allclose(f.dat.data, expect.dat.data, rtol=1e-14) expect = project(expression, V) v = TestFunction(V) @@ -336,7 +336,7 @@ def test_interpolation(elem_gen, elem_code, deg): solution = Function(V) solve(a == L, solution) - assert norm(assemble(expect - solution)) < 1e-15 + assert norm(assemble(expect - solution)) < 1e-14 @pytest.mark.parametrize("elem_gen,elem_gen2,elem_code,deg,deg2", @@ -347,8 +347,8 @@ def test_interpolation(elem_gen, elem_code, deg): (construct_rt2, construct_rt2, "RT", 2, 2), ]) def test_two_form(elem_gen, elem_gen2, elem_code, deg, deg2): - cell = polygon(3) + cell = polygon(3) mesh = UnitSquareMesh(3, 3) spaces = [] @@ -359,14 +359,11 @@ def test_two_form(elem_gen, elem_gen2, elem_code, deg, deg2): else: spaces += [("fiat", FunctionSpace(mesh, elem_code, deg), FunctionSpace(mesh, elem_code, deg2))] - results = [] for name, V, V2 in spaces: v = TestFunction(V) u = TrialFunction(V2) - res = assemble(inner(u, v)*dx).M.values - results += [res] exp, _ = get_expression(V) - f = Function(V2).interpolate(exp) + f = assemble(interpolate(exp, V2)) a = inner(v, u) * dx L = inner(f, v) * dx @@ -374,4 +371,4 @@ def test_two_form(elem_gen, elem_gen2, elem_code, deg, deg2): solution = Function(V2) solve(a == L, solution) - assert norm(assemble(f - solution)) < 1e-15 + assert norm(assemble(f - solution)) < 1e-14 From 1de003895dd90df4a534ba94351443618d1d2815 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 25 Feb 2026 10:41:11 +0000 Subject: [PATCH 49/98] clean up tests, xfail ned2 --- fuse/cells.py | 3 +++ fuse/groups.py | 2 +- test/test_convert_to_fiat.py | 18 +++++++++--------- test/test_dofs.py | 1 - 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index d15aff7c..1cf2ce6d 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -935,6 +935,9 @@ def __init__(self, A, B, flat=False): self.dimension = self.A.dimension + self.B.dimension self.flat = flat + def ordered_vertices(self): + return self.A.ordered_vertices() + self.B.ordered_vertices() + def get_spatial_dimension(self): return self.dimension diff --git a/fuse/groups.py b/fuse/groups.py index 7ef20b38..fc80c8a7 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -221,7 +221,7 @@ def __init__(self, base_group, cell=None): self.generators = [] if cell is not None: self.cell = cell - vertices = cell.vertices(return_coords=True) + # vertices = cell.vertices(return_coords=True) verts = cell.ordered_vertices() vertices = [cell.get_node(v, return_coords=True) for v in verts] diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index a30bbba5..2fe59df1 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -110,8 +110,8 @@ def create_cg1(cell): def create_cg1_quad(): deg = 1 - # cell = polygon(4) - cell = constructCellComplex("quadrilateral").cell_complex + cell = polygon(4) + # cell = constructCellComplex("quadrilateral").cell_complex vert_dg = create_dg0(cell.vertices()[0]) xs = [immerse(cell, vert_dg, TrH1)] @@ -378,7 +378,7 @@ def test_immersed_entity_perms(elem_gen, cell, expected): def test_1d(elem_gen, elem_code, deg): cell = Point(1, [Point(0), Point(0)], vertex_num=2) elem = elem_gen(cell) - scale_range = range(1, 6) + scale_range = range(2, 6) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] @@ -440,7 +440,7 @@ def test_helmholtz_2d(elem_gen, elem_code, deg, conv_rate): def test_helmholtz_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) - scale_range = range(3, 6) + scale_range = range(2, 4) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] for i in scale_range: @@ -586,8 +586,8 @@ def test_project(elem_gen, elem_code, deg): elem = elem_gen(cell) mesh = UnitTriangleMesh() - U = FunctionSpace(mesh, elem_code, deg) - assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) + # U = FunctionSpace(mesh, elem_code, deg) + # assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) U = FunctionSpace(mesh, elem.to_ufl()) assert np.allclose(project(U, mesh, Constant(1)), 0, rtol=1e-5) @@ -614,7 +614,7 @@ def test_project_3d(elem_gen, elem_code, deg): # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8), - (construct_tet_ned2, "N1curl", 2, 0.8)]) + pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -659,7 +659,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8), - (construct_tet_ned2, "N1curl", 2, 1.8)]) + pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -685,7 +685,7 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8), - (construct_tet_ned2, "N1curl", 2, 1.8)]) + pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) def test_const_vec_two_tet(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) diff --git a/test/test_dofs.py b/test/test_dofs.py index 068c6dfc..ec7424f3 100644 --- a/test/test_dofs.py +++ b/test/test_dofs.py @@ -101,7 +101,6 @@ def test_permute_nd2(): if 0 < i < 2: print(dof(g).convert_to_fiat(cell.to_fiat(), 2, (2,)).pt_dict) print(dof, "->", dof(g), "eval, ", dof(g).eval(func)) - breakpoint() def test_permute_nd_old(): From 9d2451ca322d5d48ad4632adf47287d33bbcb193 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 25 Feb 2026 11:22:07 +0000 Subject: [PATCH 50/98] fix final test --- test/test_convert_to_fiat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 2fe59df1..3ffbb894 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -378,7 +378,7 @@ def test_immersed_entity_perms(elem_gen, cell, expected): def test_1d(elem_gen, elem_code, deg): cell = Point(1, [Point(0), Point(0)], vertex_num=2) elem = elem_gen(cell) - scale_range = range(2, 6) + scale_range = range(3, 6) diff = [0 for i in scale_range] diff2 = [0 for i in scale_range] From 87262ee0acd688f79db1edc5581be0ec4c84b0a7 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 26 Feb 2026 06:58:04 +0000 Subject: [PATCH 51/98] redo non trivial transform groups --- fuse/traces.py | 15 +++++++++++++++ fuse/triples.py | 48 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/fuse/traces.py b/fuse/traces.py index f96f77ef..560e607e 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -58,6 +58,9 @@ def tabulate(self, Qpts, trace_entity): return np.ones(len(Qpts)) # return Qwts + def manipulate_basis(self, basis): + return np.ones_like(basis) + def __repr__(self): return "H1" @@ -96,6 +99,15 @@ def tabulate(self, Qwts, trace_entity): return result + def manipulate_basis(self, basis): + if basis.shape[0] == 1: + result = np.matmul(basis, np.array([[0, -1], [1, 0]])) + elif basis.shape[0] == 2: + result = np.cross(basis[0], basis[1]) + else: + raise ValueError("Immersion of HDiv edges not defined in 3D") + return result + def to_tikz(self, coord, trace_entity, scale, color="black"): vec = self.tabulate([], trace_entity).squeeze() end_point = [coord[i] + 0.25*vec[i] for i in range(len(coord))] @@ -126,6 +138,9 @@ def tabulate(self, Qwts, trace_entity): return subEntityBasis # return result + def manipulate_basis(self, basis): + return basis[0] + def plot(self, ax, coord, trace_entity, **kwargs): vec = self.tabulate([], trace_entity).squeeze() ax.quiver(*coord, *vec, **kwargs) diff --git a/fuse/triples.py b/fuse/triples.py index 68f6ff89..eb7a607d 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -83,9 +83,10 @@ def setup_ids_and_nodes(self): return entity_ids, nodes def setup_matrices(self): - self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + # self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) reversed_matrices = self.reverse_dof_perms(matrices) + print(pure_perm) if self.perm: self.pure_perm = pure_perm else: @@ -286,7 +287,7 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): dim = e.dim() e_id = e.id - min_ids[dim] res_dict[dim][e_id] = {} - dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] + # dof_ids = [self.dof_id_to_fiat_id[d.id] for d in self.generate() if d.cell_defined_on == e] dof_ids = [d.id for d in self.generate() if d.cell_defined_on == e] # res_dict[dim][e_id][0] = np.eye(len(dof_ids)) original_V, original_basis = self.compute_dense_matrix(ref_el, entity_ids, nodes, poly_set) @@ -301,12 +302,19 @@ def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): # res_dict[dim][e_id][val] = np.eye(len(dof_ids)) else: def make_mat(perm_g): + # , entity_o=perm_g new_nodes = [d(g, entity_o=perm_g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] + # new_nodes = [d(g).convert_to_fiat(ref_el, degree, self.get_value_shape()) if d.cell_defined_on == e else d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] transformed_V, transformed_basis = self.compute_dense_matrix(ref_el, entity_ids, new_nodes, poly_set) return np.matmul(transformed_basis, original_V.T) temp = make_mat(permuted_g) - # if dim == 2: - # print(g.numeric_rep(), ~permuted_g) + # if dim == 1 and e_id == 0: + # print(g, val) + # print(temp[np.ix_(dof_ids, dof_ids)]) + # sub_mat = (~permuted_g).matrix_form() + # print(sub_mat) + + # breakpoint() # print(e_id, val) # for d in self.generate(): # if d.cell_defined_on == e: @@ -413,16 +421,46 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): ent_dofs_ids = np.array([self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs], dtype=int) # (dof_gen, ent_dofs) total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if ed.id not in total_ent_dof_ids] + dof_idx = [total_ent_dof_ids.index(id) for id in ent_dofs_ids] dof_gen_class = ent_dofs[0].generation + if not len(dof_gen_class[dim].g2.members()) == 1 and dim == min(dof_gen_class.keys()): # if DOFs on entity are not perms, get the matrix # only get this if they are defined on the current dimension - oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] + bvs = np.array(e.basis_vectors()) + new_bvs = np.array(e.orient(g).basis_vectors()) + basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) + if dim == 1: + J = np.zeros_like(basis_change) + for i in range(dim): + J[dim - i - 1, i] = 1 + basis_change = np.matmul(np.matmul(J, basis_change.T), J) + if len(ent_dofs_ids) == basis_change.shape[0]: + sub_mat = basis_change + elif len(ent_dofs_ids) != 1: + sub_mat = np.kron((~g).matrix_form(), basis_change) + elif len(ent_dofs_ids) == 1: + # case for single dof with vector immersion + sub_mat = ent_dofs[0].target_space.manipulate_basis(basis_change) + else: + raise NotImplementedError("Unconsidered permuation case") + # res = np.kron(sub_mat, basis_change) + # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] + oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat + # print(sub_mat) + # print(self.matrices_by_entity[dim][e_id][val]) + # print() + # if not np.allclose(res, self.matrices_by_entity[dim][e_id][val]): + # breakpoint() elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) elif dim < self.cell.dim(): # g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on sub_mat = (~g).matrix_form() + # if not np.allclose(self.matrices_by_entity[dim][e_id][val][np.ix_(dof_idx, dof_idx)], sub_mat): + # print(g) + # print(self.matrices_by_entity[dim][e_id][val][np.ix_(dof_idx, dof_idx)]) + # print(sub_mat) oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim(): # case for dofs defined on the cell and not immersed From 35dc8a169eb008a7aa3204f269d77a9a7391d6d5 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 2 Mar 2026 11:42:42 +0000 Subject: [PATCH 52/98] try to run ci using split tests --- .github/workflows/test.yml | 55 +++----------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4153676..09823416 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,10 +32,9 @@ jobs: OPENBLAS_NUM_THREADS: 1 FIREDRAKE_CI: 1 PYOP2_SPMD_STRICT: 1 - # NOTE: One should occasionally update test_durations.json by running - # 'make test_durations' inside a 'firedrake:latest' Docker image. - EXTRA_PYTEST_ARGS: --splitting-algorithm least_duration --timeout=600 --timeout-method=thread -o faulthandler_timeout=660 --durations-path=./firedrake-repo/tests/test_durations.json --durations=50 + EXTRA_PYTEST_ARGS: --splitting-algorithm least_duration --timeout=600 --timeout-method=thread -o faulthandler_timeout=660 --durations-path=./fuse-repo/test/test_durations.json --durations=50 PYTEST_MPI_MAX_NPROCS: 8 + FIREDRAKE_USE_FUSE: 1 steps: - name: Fix HOME # For unknown reasons GitHub actions overwrite HOME to /github/home @@ -187,52 +186,4 @@ jobs: run: | . venv/bin/activate cd ./fuse-repo - pip list - make tests - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: covdata - include-hidden-files: true - path: .coverage.* - - coverage: - name: Coverage - needs: test - runs-on: ubuntu-latest - if: success() && github.ref == 'refs/heads/main' - container: - image: firedrakeproject/firedrake-vanilla-default:latest - # Steps represent a sequence of tasks that will be executed as - # part of the jobs - steps: - - name: "Check out the repo" - uses: "actions/checkout@v2" - - name: Install checkedout fuse - run: | - python3 -m pip install --break-system-packages -e '.[dev]' - - name: "Download coverage data" - uses: actions/download-artifact@v4 - with: - name: covdata - - - name: "Combine" - run: | - make coverage - export TOTAL=$(python3 -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") - echo "total=$TOTAL" >> $GITHUB_ENV - echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY - - - name: "Make badge" - uses: schneegans/dynamic-badges-action@v1.4.0 - with: - # GIST_TOKEN is a GitHub personal access token with scope "gist". - auth: ${{ secrets.GIST_SECRET }} - gistID: 8d09e14999153441dba99d1759e90707 # replace with your real Gist id. - filename: covbadge.json - label: Coverage - message: ${{ env.total }}% - minColorRange: 50 - maxColorRange: 90 - valColorRange: ${{ env.total }} - + firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" fuse-repo/test \ No newline at end of file From 2410d17403641fcd7d400ba46c0e6c9fe4b126f8 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 2 Mar 2026 12:26:49 +0000 Subject: [PATCH 53/98] fix path --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09823416..1f1ef5e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -186,4 +186,4 @@ jobs: run: | . venv/bin/activate cd ./fuse-repo - firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" fuse-repo/test \ No newline at end of file + firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" ./test \ No newline at end of file From f785def78cad7bb4700b55416b8d1910469f3352 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 2 Mar 2026 18:50:05 +0000 Subject: [PATCH 54/98] maybe nd2 on tets --- fuse/cells.py | 8 +- fuse/dof.py | 18 +++-- fuse/traces.py | 2 +- fuse/triples.py | 46 ++++++----- test/test_3d_examples_docs.py | 56 ++++++++++++-- test/test_convert_to_fiat.py | 142 ++++++++++++++++++++++++---------- test/test_orientations.py | 3 + 7 files changed, 190 insertions(+), 85 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 1cf2ce6d..ab06eb98 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -477,8 +477,8 @@ def _subentity_traversal(self, sub_ents, min_ids): connections = [(c.point.id, c.point.group.identity) for c in self.connections] # if self.oriented: # connections = self.permute_entities(self.oriented, dim - 1) - if self.dimension == 2: - connections = [connections[-1]] + connections[:-1] + # if self.dimension == 2: + # connections = [connections[-1]] + connections[:-1] # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: @@ -639,10 +639,10 @@ def permute_entities(self, g, d): return reordered_entities - def basis_vectors(self, return_coords=True, entity=None): + def basis_vectors(self, return_coords=True, entity=None, order=False): if not entity: entity = self - self_levels = [sorted(generation) for generation in nx.topological_generations(self.G)] + self_levels = [generation for generation in nx.topological_generations(self.G)] vertices = entity.ordered_vertices() if self.dimension == 0: # return [[] diff --git a/fuse/dof.py b/fuse/dof.py index 3ad53357..4388bce3 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -192,7 +192,9 @@ def __call__(self, *args): def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): 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*np.matmul(self.pt, basis_change) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + 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] def _to_dict(self): o_dict = {"pt": self.pt} @@ -340,17 +342,20 @@ def to_quadrature(self, arg_degree): Qpts, Qwts = self.cell_defined_on.quadrature(arg_degree) Qwts = Qwts.reshape(Qwts.shape + (1,)) dim = self.cell_defined_on.get_spatial_dimension() - if dim > 0 and self.pairing.orientation: + if dim > 0: bvs = np.array(self.cell_defined_on.basis_vectors()) new_bvs = np.array(self.cell_defined_on.orient(self.pairing.orientation).basis_vectors()) basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) else: basis_change = np.eye(dim) - + immersed = self.immersed if self.immersed and isinstance(self.kernel, VectorKernel): - # this probably needs generalising - basis_change = np.array([self.cell.attachment(self.cell.id, self.cell_defined_on.id)(*bv) for bv in basis_change]) - pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed, self.cell.dimension) + def immersed(pt): + basis = np.array(self.cell_defined_on.basis_vectors()).T + basis_coeffs = np.matmul(np.linalg.inv(basis), np.array(pt)) + immersed_basis = np.array(self.cell.basis_vectors(entity=self.cell_defined_on)) + return np.matmul(basis_coeffs, immersed_basis) + pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) if self.immersed: # need to compute jacobian from attachment. @@ -359,7 +364,6 @@ def to_quadrature(self, arg_degree): # immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.pairing.orientation))[0] # else: immersion = self.target_space.tabulate(pts, self.pairing.entity) - # Special case - force evaluation on different orientation of entity for construction of matrix transforms if self.entity_o: immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) diff --git a/fuse/traces.py b/fuse/traces.py index 560e607e..b216660a 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -59,7 +59,7 @@ def tabulate(self, Qpts, trace_entity): # return Qwts def manipulate_basis(self, basis): - return np.ones_like(basis) + return np.array([1]) def __repr__(self): return "H1" diff --git a/fuse/triples.py b/fuse/triples.py index eb7a607d..da4699b8 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -86,7 +86,6 @@ def setup_matrices(self): # self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) reversed_matrices = self.reverse_dof_perms(matrices) - print(pure_perm) if self.perm: self.pure_perm = pure_perm else: @@ -160,6 +159,8 @@ def to_fiat(self): self.to_ufl() form_degree = 1 if self.spaces[0].set_shape else 0 degree = self.spaces[0].degree() + # sanity check that the dofs span the space + original_V, original_basis = self.compute_dense_matrix(self.ref_el, self.entity_ids, self.nodes, self.poly_set) if self.pure_perm: dual = DualSet(self.nodes, self.ref_el, self.entity_ids, self.entity_perms) else: @@ -275,6 +276,7 @@ def compute_dense_matrix(self, ref_el, entity_ids, nodes, poly_set): return A, new_coeffs_flat def make_entity_dense_matrices(self, ref_el, entity_ids, nodes, poly_set): + raise NotImplementedError("This should be deprecated") degree = self.spaces[0].degree() min_ids = self.cell.get_starter_ids() nodes = [d.convert_to_fiat(ref_el, degree, self.get_value_shape()) for d in self.generate()] @@ -428,39 +430,35 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): # if DOFs on entity are not perms, get the matrix # only get this if they are defined on the current dimension bvs = np.array(e.basis_vectors()) - new_bvs = np.array(e.orient(g).basis_vectors()) + new_bvs = np.array(e.orient(~g).basis_vectors()) basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) - if dim == 1: - J = np.zeros_like(basis_change) - for i in range(dim): - J[dim - i - 1, i] = 1 - basis_change = np.matmul(np.matmul(J, basis_change.T), J) + # print((~g).numeric_rep()) + # print((~g).matrix_form()) + # print(~g).ordered_vertices()) + # print(basis_change) if len(ent_dofs_ids) == basis_change.shape[0]: sub_mat = basis_change - elif len(ent_dofs_ids) != 1: - sub_mat = np.kron((~g).matrix_form(), basis_change) - elif len(ent_dofs_ids) == 1: - # case for single dof with vector immersion + elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: + # equivalently g1 trivial sub_mat = ent_dofs[0].target_space.manipulate_basis(basis_change) else: - raise NotImplementedError("Unconsidered permuation case") - # res = np.kron(sub_mat, basis_change) - # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = self.matrices_by_entity[dim][e_id][val] - oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat - # print(sub_mat) - # print(self.matrices_by_entity[dim][e_id][val]) - # print() - # if not np.allclose(res, self.matrices_by_entity[dim][e_id][val]): - # breakpoint() + # len(dof_gen_class[dim].g2.members()) == 2: + # case where value change is a restriction of the full transformation of the basis + value_change = ent_dofs[0].target_space.manipulate_basis(basis_change) + sub_mat = np.kron((~g).matrix_form(), value_change) + # sub_mat = (~g).matrix_form() + # elif len(ent_dofs_ids) != 1:# more dofs than dimension of g? + # case for transforms where the basis vector is already included in the dof + # sub_mat = np.kron((~g).matrix_form(), basis_change) + # else: + # raise NotImplementedError("Unconsidered permuation case") + oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) elif dim < self.cell.dim(): # g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on sub_mat = (~g).matrix_form() - # if not np.allclose(self.matrices_by_entity[dim][e_id][val][np.ix_(dof_idx, dof_idx)], sub_mat): - # print(g) - # print(self.matrices_by_entity[dim][e_id][val][np.ix_(dof_idx, dof_idx)]) - # print(sub_mat) oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim(): # case for dofs defined on the cell and not immersed diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 0646f0c8..3fa1a22f 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -143,17 +143,51 @@ def construct_tet_rt(cell=None): rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M xs = [DOF(L2Pairing(), VectorKernel(1))] - dofs = DOFGenerator(xs, S1, S3) + dofs = DOFGenerator(xs, S1, S2) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] - face = DOFGenerator(im_xs, tet_faces, S4) + face = DOFGenerator(im_xs, tet_faces, S1) rt1 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [face]) # [test_tet_rt 1] return rt1 +def construct_tet_rt2(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 2 + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M = sp.Matrix([[x, y, z]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M + + xs = [DOF(L2Pairing(), PolynomialKernel(1 - (1/2)*x + (np.sqrt(3)/2)*y, symbols=(x, y)))] + dofs = DOFGenerator(xs, C3, S2) + face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) + + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + v_0 = np.array(cell.get_node(cell.ordered_vertices()[0], return_coords=True)) + v_1 = np.array(cell.get_node(cell.ordered_vertices()[1], return_coords=True)) + v_2 = np.array(cell.get_node(cell.ordered_vertices()[2], return_coords=True)) + v_3 = np.array(cell.get_node(cell.ordered_vertices()[3], return_coords=True)) + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)), + DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2)), + DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] + interior = DOFGenerator(xs, S1, S4) + + rt1 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), + [faces, interior]) + # [test_tet_rt 1] + return rt1 def construct_tet_ned(cell=None): deg = 1 @@ -187,7 +221,7 @@ def construct_tet_ned(cell=None): return ElementTriple(tet, (nd_space, CellHCurl, L2), [edge_dofs]) -def construct_tet_ned2(tet=None): +def construct_tet_ned2(tet=None, perm=None): if tet is None: tet = make_tetrahedron() deg = 2 @@ -208,9 +242,11 @@ def construct_tet_ned2(tet=None): dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) - v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) + v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] + # xs = [DOF(L2Pairing(), VectorKernel((v_1 - v_0)/2)), DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)),] center_dofs = DOFGenerator(xs, S2, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] @@ -226,7 +262,7 @@ def construct_tet_ned2(tet=None): Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) - ned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [edge_dofs, face_dofs]) + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) return ned @@ -240,6 +276,14 @@ def plot_tet_rt(): rt = construct_tet_rt() rt.plot() +def test_tet_rt2(): + rt2 = construct_tet_rt2() + ls = rt2.generate() + # TODO make this a proper test + for dof in ls: + print(dof) + rt2.to_fiat() + breakpoint() def test_tet_rt(): rt1 = construct_tet_rt() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 3ffbb894..edbe2832 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 -from test_3d_examples_docs import construct_tet_rt, construct_tet_ned, construct_tet_ned2, construct_tet_cg4 +from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -358,18 +358,6 @@ def test_entity_perms(elem_gen, cell): print(elem.matrices[dim][0][i]) -@pytest.mark.parametrize("elem_gen, cell, expected", [(create_cg1, Point(1, [Point(0), Point(0)], vertex_num=2), (0, 0, [[[1]]])), - (create_cg2, Point(1, [Point(0), Point(0)], vertex_num=2), (1, 0, [[[1]], [[1]]])), - (construct_rt, polygon(3), (1, 0, [[[1]], [[-1]]]))]) -def test_immersed_entity_perms(elem_gen, cell, expected): - elem = elem_gen(cell) - elem.to_fiat() - dim, ent_id, matrices = expected - - for key in elem.matrices_by_entity[dim][ent_id]: - assert any([np.allclose(e_mat, elem.matrices_by_entity[dim][ent_id][key]) for e_mat in matrices]) - - @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), (create_dg2, "DG", 2), @@ -659,6 +647,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8), + (construct_tet_rt2, "RT", 2, 1.8), pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() @@ -683,48 +672,114 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) -@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), - (construct_tet_ned, "N1curl", 1, 0.8), - pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) -def test_const_vec_two_tet(elem_gen, elem_code, deg, conv_rate): +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_ned2, "N1curl", 2), + pytest.param(construct_tet_rt2, "RT", 2, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) +def test_linear_vec(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell) - vec = as_vector([1, 1, 1]) - from firedrake.utility_meshes import TwoTetMesh group = [sp.combinatorics.Permutation([0, 1, 2, 3]), sp.combinatorics.Permutation([0, 2, 3, 1]), sp.combinatorics.Permutation([0, 3, 1, 2]), sp.combinatorics.Permutation([0, 1, 3, 2]), sp.combinatorics.Permutation([0, 3, 2, 1]), - sp.combinatorics.Permutation([0, 2, 1, 3])] - - for g in group: - mesh = TwoTetMesh(perm=g) + sp.combinatorics.Permutation([0, 2, 1, 3]) + ] + scale_range = range(0, 2) + for i in scale_range: + mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + # from firedrake.utility_meshes import TwoTetMesh + # mesh = TwoTetMesh(perm=g) + x = SpatialCoordinate(mesh) + vec = as_vector([2*x[0], 5*x[1], x[2]]) + # vec = as_vector([0, 1, 0]) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = assemble(interpolate(vec, V2)) CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res2, CG3)) - for i in range(res3.dat.data.shape[0]): - assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + res4 = assemble(interpolate(vec, CG3)) + assert np.allclose(res3.dat.data, res4.dat.data) else: V = FunctionSpace(mesh, elem_code, deg) res1 = assemble(interpolate(vec, V)) CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res1, CG3)) + res4 = assemble(interpolate(vec, CG3)) + assert np.allclose(res3.dat.data, res4.dat.data) + + +@pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_rt, "RT", 1), + (construct_tet_rt2, "RT", 2), + (construct_tet_ned, "N1curl", 1), + (construct_tet_ned2, "N1curl", 2)]) +def test_vec_two_tet(elem_gen, elem_code, deg): + cell = make_tetrahedron() + elem = elem_gen(cell) + + def vec(mesh, array=False): + if array: + return np.array([1, 1, 1]) + return as_vector([1, 1, 1]) + + from firedrake.utility_meshes import TwoTetMesh + group = [sp.combinatorics.Permutation([0, 1, 2, 3]), + sp.combinatorics.Permutation([0, 2, 3, 1]), + sp.combinatorics.Permutation([0, 3, 1, 2]), + sp.combinatorics.Permutation([0, 1, 3, 2]), + sp.combinatorics.Permutation([0, 3, 2, 1]), + sp.combinatorics.Permutation([0, 2, 1, 3]) + ] + error_gs = [] + error_row_lists = [] + for g in group: + # mesh = UnitTetrahedronMesh() + mesh = TwoTetMesh(perm=g) + print(mesh.entity_orientations) + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + V2 = FunctionSpace(mesh, elem.to_ufl()) + res2 = assemble(interpolate(vec(mesh), V2)) + CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) + res3 = assemble(interpolate(res2, CG3)) + error_rows = [] + print(g) + # breakpoint() for i in range(res3.dat.data.shape[0]): - assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + if not np.allclose(res3.dat.data[i], vec(mesh, array=True)): + print(res3.dat.data[i]) + error_gs += [g] + error_rows += [i] + error_row_lists += [error_rows] + else: + V = FunctionSpace(mesh, elem_code, deg) + res1 = assemble(interpolate(vec(mesh), V)) + CG3 = VectorFunctionSpace(mesh, "CG", 3) + res3 = assemble(interpolate(res1, CG3)) + # for i in range(res3.dat.data.shape[0]): + # assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) + assert len(error_gs) == 0 + # print(set(error_gs)) + # breakpoint() @pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), - (construct_tet_cg4, "CG", 4, 0.04)]) + (construct_tet_cg4, "CG", 4, 0.04), + (construct_tet_rt2, "RT", 2, 1e-13), + (construct_tet_ned2, "N1curl", 2, 1e-13)]) def test_const_two_tet(elem_gen, elem_code, deg, max_err): cell = make_tetrahedron() elem_perms = elem_gen(cell, perm=True) elem_mats = elem_gen(cell, perm=False) ufl_elem_perms = elem_perms.to_ufl() + # breakpoint() ufl_elem_mats = elem_mats.to_ufl() + def expr(mesh): + x = SpatialCoordinate(mesh) + if elem_code == "RT" or elem_code == "N1curl": + return as_vector([x[0], 2*x[1], 3*x[2]]) + # return as_vector([cos((3/4)*pi*x[0]), cos((3/4)*pi*x[1]), cos((3/4)*pi*x[2])]) + return cos((3/4)*pi*x[0]) + errors = [] from firedrake.utility_meshes import TwoTetMesh group = [sp.combinatorics.Permutation([0, 1, 2, 3]), sp.combinatorics.Permutation([0, 2, 3, 1]), @@ -737,23 +792,26 @@ def test_const_two_tet(elem_gen, elem_code, deg, max_err): mesh = TwoTetMesh(perm=g) print(g) print(mesh.entity_orientations) - x = SpatialCoordinate(mesh) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): - V = FunctionSpace(mesh, ufl_elem_perms) - res = project(V, mesh, cos((3/4)*pi*x[0])) - print(res) - assert res < max_err + if elem_code != "RT" and elem_code != "N1curl": + V = FunctionSpace(mesh, ufl_elem_perms) + + res = project(V, mesh, expr(mesh)) + print(res) + assert res < max_err V2 = FunctionSpace(mesh, ufl_elem_mats) - res = project(V2, mesh, cos((3/4)*pi*x[0])) + print(elem_mats.matrices[2][0][mesh.entity_orientations[1][10]][np.ix_([12, 13], [12, 13])]) + res = project(V2, mesh, expr(mesh)) print(res) - assert res < max_err + errors += [res] + #assert res < max_err else: V = FunctionSpace(mesh, elem_code, deg) - # res1 = assemble(interpolate(cos((3/4)*pi*x[0]), V)) - res = project(V, mesh, cos((3/4)*pi*x[0])) - assert res < max_err - + res = project(V, mesh, expr(mesh)) + print(res) + # assert res < max_err + assert all([res < max_err for res in errors]) # TODO this is not a real test def test_scaling_mesh(): @@ -775,25 +833,23 @@ def test_scaling_mesh(): print(res2.dat.data) -@pytest.mark.skip(reason="Obscenely slow") def test_quartic_poisson_solve(): # Create mesh and define function space - r = 1 + r = 0 m = UnitCubeMesh(2 ** r, 2 ** r, 2 ** r) x = SpatialCoordinate(m) - V = FunctionSpace(m, "CG", 4) + elem = construct_tet_cg4(make_tetrahedron()) + V = FunctionSpace(m, elem.to_ufl()) # Define variational problem u = TrialFunction(V) v = TestFunction(V) a = dot(grad(u), grad(v)) * dx u_e = x[0]*x[0]*x[0]*x[0] + 2*x[0]*x[1]*x[1] + 3*x[2]*x[2]*x[2]*x[2] + 6 - # u_e = 1 + x[0]*x[0] + 2*x[1]*x[1] + 3*x[2]*x[2] bcs = [DirichletBC(V, u_e, "on_boundary")] f = Function(V) f.interpolate(-12*x[0]*x[0] - 4*x[0] - 36*x[2]*x[2]) - # f = Constant(-12.0) L = f*v*dx # Compute solution diff --git a/test/test_orientations.py b/test/test_orientations.py index af7fd788..4d649edd 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -38,6 +38,9 @@ def construct_nd2(tri=None): ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) return ned +def test_nd2(): + elem = construct_nd2() + elem.to_fiat() def construct_rt2(tri=None): if tri is None: From 86ab83b8db341fdcb0954ec69e68699a6402d628 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 3 Mar 2026 10:23:44 +0000 Subject: [PATCH 55/98] use different group for cg4 --- fuse/__init__.py | 2 +- fuse/groups.py | 1 - test/test_3d_examples_docs.py | 4 ++-- test/test_groups.py | 7 ------- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index 6967c2ce..e7ed0618 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,5 +1,5 @@ from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex -from fuse.groups import S1, S2, S3, D4, Z3, Z4, C3, C4, S4, A4, tri_C3, diff_C3, tet_edges, tet_faces, sq_edges, GroupRepresentation, PermutationSetRepresentation, get_cyc_group, get_sym_group +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, PolynomialKernel, ComponentKernel from fuse.triples import ElementTriple, DOFGenerator, immerse from fuse.traces import TrH1, TrGrad, TrHess, TrHCurl, TrHDiv diff --git a/fuse/groups.py b/fuse/groups.py index fc80c8a7..759f43f4 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -401,7 +401,6 @@ def get_cyc_group(n): A4 = GroupRepresentation(AlternatingGroup(4)) A3 = GroupRepresentation(AlternatingGroup(3)) -tri_C3 = PermutationSetRepresentation([Permutation([0, 1, 2]), Permutation([2, 0, 1]), Permutation([1, 0, 2])]) diff_C3 = PermutationSetRepresentation([Permutation([2, 0, 1]), Permutation([0, 1, 2]), Permutation([1, 2, 0])]) # this group is used for facet dofs # tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([0, 2, 3, 1]), Permutation([1, 2, 0, 3]), # Permutation([0, 3, 1, 2]), Permutation([1, 3, 2, 0]), Permutation([2, 3, 0, 1])]) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 3fa1a22f..9f0c89cd 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -78,9 +78,9 @@ def construct_tet_cg4(cell=None, perm=True): # xs = [DOF(DeltaPairing(), PointKernel((-1/np.sqrt(5), -0.26)))] # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] - xs = [DOF(DeltaPairing(), PointKernel((-1/3, -np.sqrt(3)/9)))] + xs = [DOF(DeltaPairing(), PointKernel((0, 2*np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, C3, S1), perm) + DOFGenerator(xs, diff_C3, S1), perm) xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) diff --git a/test/test_groups.py b/test/test_groups.py index cc3765f4..1b26787e 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -82,13 +82,6 @@ def test_conj(): # for row in g.transform_matrix: # print(row) - # print("tri_c3") - # g1 = tri_C3.add_cell(cell) - # for g in g1.members(): - # print("g", g) - # print("g", g.perm.cycle_structure) - # # for row in g.matrix_form(): - # # print(row) # print("others") # for g in cell.group.members(): # if g not in g1.members(): From fc0bbdd7488621e7a8dfa8d23791650bb54f22b3 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 3 Mar 2026 17:27:53 +0000 Subject: [PATCH 56/98] fix up RT --- fuse/dof.py | 3 +-- fuse/traces.py | 6 ++--- test/test_3d_examples_docs.py | 4 ++-- test/test_convert_to_fiat.py | 41 +++++++++++++++++++---------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 4388bce3..7cfa9db5 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -363,11 +363,10 @@ def immersed(pt): # 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.pairing.entity) + immersion = self.target_space.tabulate(pts, self.cell_defined_on) # Special case - force evaluation on different orientation of entity for construction of matrix transforms if self.entity_o: immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) - # print("after change", immersion) if isinstance(self.target_space, TrH1): new_wts = wts else: diff --git a/fuse/traces.py b/fuse/traces.py index b216660a..b5f97225 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -89,18 +89,18 @@ def tabulate(self, Qwts, trace_entity): cellEntityBasis = np.array(self.domain.basis_vectors(entity=trace_entity)) # basis = np.matmul(entityBasis, cellEntityBasis) basis = cellEntityBasis - if trace_entity.dimension == 1: result = np.matmul(basis, np.array([[0, -1], [1, 0]])) elif trace_entity.dimension == 2: result = np.cross(basis[0], basis[1]) else: raise ValueError("Immersion of HDiv edges not defined in 3D") - return result def manipulate_basis(self, basis): - if basis.shape[0] == 1: + if basis.shape == (1, 1): + return basis + elif basis.shape == (1, 2): result = np.matmul(basis, np.array([[0, -1], [1, 0]])) elif basis.shape[0] == 2: result = np.cross(basis[0], basis[1]) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 9f0c89cd..ec7ea678 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -168,8 +168,8 @@ def construct_tet_rt2(cell=None, perm=None): Pd = PolynomialSpace(deg - 1) rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M - xs = [DOF(L2Pairing(), PolynomialKernel(1 - (1/2)*x + (np.sqrt(3)/2)*y, symbols=(x, y)))] - dofs = DOFGenerator(xs, C3, S2) + xs = [DOF(L2Pairing(), PolynomialKernel(1/3 - (1/2)*x + y/(2*np.sqrt(3)), symbols=(x, y)))] + dofs = DOFGenerator(xs, diff_C3, S2) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index edbe2832..a4a92cce 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -648,7 +648,7 @@ def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_tet_rt, "RT", 1, 0.8), (construct_tet_ned, "N1curl", 1, 0.8), (construct_tet_rt2, "RT", 2, 1.8), - pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) + (construct_tet_ned2, "N1curl", 2, 1.8)]) def test_const_vec(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -673,29 +673,33 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_ned2, "N1curl", 2), - pytest.param(construct_tet_rt2, "RT", 2, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) + (construct_tet_rt2, "RT", 2)]) def test_linear_vec(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell) - group = [sp.combinatorics.Permutation([0, 1, 2, 3]), - sp.combinatorics.Permutation([0, 2, 3, 1]), - sp.combinatorics.Permutation([0, 3, 1, 2]), - sp.combinatorics.Permutation([0, 1, 3, 2]), - sp.combinatorics.Permutation([0, 3, 2, 1]), - sp.combinatorics.Permutation([0, 2, 1, 3]) - ] + i = 1 + mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + x = SpatialCoordinate(mesh) + candidate_vecs = [ + [x[0], 0, 0], [0, x[0], 0], [0, 0, x[0]], + [x[1], 0, 0], [0, x[1], 0], [0, 0, x[1]], + [x[2], 0, 0], [0, x[2], 0], [0, 0, x[2]], + [x[0], x[1], 0], [x[1], x[0], 0], [x[1], 0, x[0]], [x[0], x[1], x[2]] + ] + # group = sp.combinatorics.SymmetricGroup(4).elements scale_range = range(0, 2) - for i in scale_range: - mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) + error_rows = [] + # for i in scale_range: + # for g in group: + for v in candidate_vecs: # from firedrake.utility_meshes import TwoTetMesh + # g = group[3] # mesh = TwoTetMesh(perm=g) - x = SpatialCoordinate(mesh) - vec = as_vector([2*x[0], 5*x[1], x[2]]) - # vec = as_vector([0, 1, 0]) + vec = as_vector(v) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = assemble(interpolate(vec, V2)) - CG3 = VectorFunctionSpace(mesh, "CG", 3) + CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) res3 = assemble(interpolate(res2, CG3)) res4 = assemble(interpolate(vec, CG3)) assert np.allclose(res3.dat.data, res4.dat.data) @@ -770,7 +774,6 @@ def test_const_two_tet(elem_gen, elem_code, deg, max_err): elem_perms = elem_gen(cell, perm=True) elem_mats = elem_gen(cell, perm=False) ufl_elem_perms = elem_perms.to_ufl() - # breakpoint() ufl_elem_mats = elem_mats.to_ufl() def expr(mesh): @@ -797,11 +800,11 @@ def expr(mesh): V = FunctionSpace(mesh, ufl_elem_perms) res = project(V, mesh, expr(mesh)) - print(res) - assert res < max_err + print("perms", res) + # assert res < max_err + errors += [res] V2 = FunctionSpace(mesh, ufl_elem_mats) - print(elem_mats.matrices[2][0][mesh.entity_orientations[1][10]][np.ix_([12, 13], [12, 13])]) res = project(V2, mesh, expr(mesh)) print(res) errors += [res] From f1a21b86065cf9430b6d2c5e76bbe34c2eee2c59 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 3 Mar 2026 18:08:12 +0000 Subject: [PATCH 57/98] lint --- fuse/dof.py | 7 ++++- fuse/triples.py | 3 +- test/test_3d_examples_docs.py | 8 ++++-- test/test_convert_to_fiat.py | 52 +++++++++++++++++++++++++++++------ test/test_orientations.py | 4 ++- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 7cfa9db5..115c4897 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -348,14 +348,19 @@ def to_quadrature(self, arg_degree): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) else: basis_change = np.eye(dim) - immersed = self.immersed if self.immersed and isinstance(self.kernel, VectorKernel): def immersed(pt): basis = np.array(self.cell_defined_on.basis_vectors()).T basis_coeffs = np.matmul(np.linalg.inv(basis), np.array(pt)) immersed_basis = np.array(self.cell.basis_vectors(entity=self.cell_defined_on)) return np.matmul(basis_coeffs, immersed_basis) + else: + immersed = self.immersed pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) + if isinstance(self.kernel, PolynomialKernel): + print(self) + print(self.kernel(*np.matmul(basis_change, Qpts[0]))) + # breakpoint() if self.immersed: # need to compute jacobian from attachment. diff --git a/fuse/triples.py b/fuse/triples.py index da4699b8..5aee502e 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -315,7 +315,6 @@ def make_mat(perm_g): # print(temp[np.ix_(dof_ids, dof_ids)]) # sub_mat = (~permuted_g).matrix_form() # print(sub_mat) - # breakpoint() # print(e_id, val) # for d in self.generate(): @@ -423,7 +422,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): ent_dofs_ids = np.array([self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs], dtype=int) # (dof_gen, ent_dofs) total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if ed.id not in total_ent_dof_ids] - dof_idx = [total_ent_dof_ids.index(id) for id in ent_dofs_ids] + # dof_idx = [total_ent_dof_ids.index(id) for id in ent_dofs_ids] dof_gen_class = ent_dofs[0].generation if not len(dof_gen_class[dim].g2.members()) == 1 and dim == min(dof_gen_class.keys()): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index ec7ea678..cb717f8e 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -154,6 +154,7 @@ def construct_tet_rt(cell=None): # [test_tet_rt 1] return rt1 + def construct_tet_rt2(cell=None, perm=None): if cell is None: cell = make_tetrahedron() @@ -169,7 +170,7 @@ def construct_tet_rt2(cell=None, perm=None): rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M xs = [DOF(L2Pairing(), PolynomialKernel(1/3 - (1/2)*x + y/(2*np.sqrt(3)), symbols=(x, y)))] - dofs = DOFGenerator(xs, diff_C3, S2) + dofs = DOFGenerator(xs, C3, S2) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] @@ -189,6 +190,7 @@ def construct_tet_rt2(cell=None, perm=None): # [test_tet_rt 1] return rt1 + def construct_tet_ned(cell=None): deg = 1 tet = make_tetrahedron() @@ -243,7 +245,7 @@ def construct_tet_ned2(tet=None, perm=None): int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) - v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) + # v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] # xs = [DOF(L2Pairing(), VectorKernel((v_1 - v_0)/2)), DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)),] @@ -276,6 +278,7 @@ def plot_tet_rt(): rt = construct_tet_rt() rt.plot() + def test_tet_rt2(): rt2 = construct_tet_rt2() ls = rt2.generate() @@ -285,6 +288,7 @@ def test_tet_rt2(): rt2.to_fiat() breakpoint() + def test_tet_rt(): rt1 = construct_tet_rt() ls = rt1.generate() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index a4a92cce..5e9ccebd 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -601,8 +601,9 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg3_tet, "CG", 3, 3.8), # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), + (construct_tet_rt2, "RT", 2, 1.8), (construct_tet_ned, "N1curl", 1, 0.8), - pytest.param(construct_tet_ned2, "N1curl", 2, 1.8, marks=pytest.mark.xfail(reason='Facet matrix-valued orientations in 3D'))]) + (construct_tet_ned2, "N1curl", 2, 1.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) @@ -677,17 +678,18 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): def test_linear_vec(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell) - i = 1 + i = 0 mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) x = SpatialCoordinate(mesh) candidate_vecs = [ + [1, 0, 0], [0, 0, 0], [x[0], 0, 0], [0, x[0], 0], [0, 0, x[0]], [x[1], 0, 0], [0, x[1], 0], [0, 0, x[1]], [x[2], 0, 0], [0, x[2], 0], [0, 0, x[2]], [x[0], x[1], 0], [x[1], x[0], 0], [x[1], 0, x[0]], [x[0], x[1], x[2]] ] # group = sp.combinatorics.SymmetricGroup(4).elements - scale_range = range(0, 2) + # scale_range = range(0, 2) error_rows = [] # for i in scale_range: # for g in group: @@ -699,10 +701,12 @@ def test_linear_vec(elem_gen, elem_code, deg): if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) res2 = assemble(interpolate(vec, V2)) - CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) + CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res2, CG3)) res4 = assemble(interpolate(vec, CG3)) - assert np.allclose(res3.dat.data, res4.dat.data) + if not np.allclose(res3.dat.data, res4.dat.data): + error_rows += [v] + breakpoint() else: V = FunctionSpace(mesh, elem_code, deg) res1 = assemble(interpolate(vec, V)) @@ -710,6 +714,7 @@ def test_linear_vec(elem_gen, elem_code, deg): res3 = assemble(interpolate(res1, CG3)) res4 = assemble(interpolate(vec, CG3)) assert np.allclose(res3.dat.data, res4.dat.data) + breakpoint() @pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_rt, "RT", 1), @@ -761,8 +766,6 @@ def vec(mesh, array=False): # for i in range(res3.dat.data.shape[0]): # assert np.allclose(res3.dat.data[i], np.array([1, 1, 1])) assert len(error_gs) == 0 - # print(set(error_gs)) - # breakpoint() @pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), @@ -808,7 +811,7 @@ def expr(mesh): res = project(V2, mesh, expr(mesh)) print(res) errors += [res] - #assert res < max_err + # assert res < max_err else: V = FunctionSpace(mesh, elem_code, deg) res = project(V, mesh, expr(mesh)) @@ -816,6 +819,39 @@ def expr(mesh): # assert res < max_err assert all([res < max_err for res in errors]) + +@pytest.mark.parametrize("elem_gen,elem_code,deg", + [(construct_tet_rt2, "RT", 2), (construct_tet_ned2, "N1curl", 2) + ]) +def test_3d_two_form(elem_gen, elem_code, deg): + + cell = make_tetrahedron() + mesh = UnitCubeMesh(3, 3, 3) + x = SpatialCoordinate(mesh) + + spaces = [] + if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): + elem = elem_gen(cell) + elem2 = elem_gen(cell) + spaces += [("fuse", FunctionSpace(mesh, elem.to_ufl()), FunctionSpace(mesh, elem2.to_ufl()))] + else: + spaces += [("fiat", FunctionSpace(mesh, elem_code, deg), FunctionSpace(mesh, elem_code, deg))] + + for name, V, V2 in spaces: + v = TestFunction(V) + u = TrialFunction(V2) + exp = as_vector([cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0])]) + f = assemble(interpolate(exp, V2)) + + a = inner(v, u) * dx + L = inner(f, v) * dx + + solution = Function(V2) + solve(a == L, solution) + + assert norm(assemble(f - solution)) < 1e-14 + + # TODO this is not a real test def test_scaling_mesh(): mesh1 = RectangleMesh(2, 1, 1, 1) diff --git a/test/test_orientations.py b/test/test_orientations.py index 4d649edd..31780c9c 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -38,10 +38,12 @@ def construct_nd2(tri=None): ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs, center_dofs]) return ned + def test_nd2(): elem = construct_nd2() elem.to_fiat() + def construct_rt2(tri=None): if tri is None: tri = polygon(3) @@ -231,7 +233,7 @@ def interpolate_vs_project(V, expression, exact): return sqrt(assemble(inner((expect - exact), (expect - exact)) * dx)), sqrt(assemble(inner((f - exact), (f - exact)) * dx)) -@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_cg3, "CG", 3, 3.8)]) +@pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_cg3, "CG", 3, 3.8), ]) def test_convergence(elem_gen, elem_code, deg, conv_rate): cell = polygon(3) elem = elem_gen(cell) From 9c2b5b845ee725e55aed06255c97fb66e86dd1f0 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 4 Mar 2026 16:31:01 +0000 Subject: [PATCH 58/98] ...it might be working --- fuse/dof.py | 4 ---- fuse/groups.py | 1 + fuse/triples.py | 4 ---- test/test_3d_examples_docs.py | 11 +++++++---- test/test_convert_to_fiat.py | 31 +++++++++++++++++++------------ 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 115c4897..5cd0439e 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -357,10 +357,6 @@ def immersed(pt): else: immersed = self.immersed pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) - if isinstance(self.kernel, PolynomialKernel): - print(self) - print(self.kernel(*np.matmul(basis_change, Qpts[0]))) - # breakpoint() if self.immersed: # need to compute jacobian from attachment. diff --git a/fuse/groups.py b/fuse/groups.py index 759f43f4..e63a9d32 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -401,6 +401,7 @@ def get_cyc_group(n): A4 = GroupRepresentation(AlternatingGroup(4)) A3 = GroupRepresentation(AlternatingGroup(3)) +basis_S2 = PermutationSetRepresentation([Permutation([0, 1, 2]), Permutation([0, 2, 1])]) diff_C3 = PermutationSetRepresentation([Permutation([2, 0, 1]), Permutation([0, 1, 2]), Permutation([1, 2, 0])]) # this group is used for facet dofs # tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([0, 2, 3, 1]), Permutation([1, 2, 0, 3]), # Permutation([0, 3, 1, 2]), Permutation([1, 3, 2, 0]), Permutation([2, 3, 0, 1])]) diff --git a/fuse/triples.py b/fuse/triples.py index 5aee502e..32a72208 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -431,10 +431,6 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): bvs = np.array(e.basis_vectors()) new_bvs = np.array(e.orient(~g).basis_vectors()) basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) - # print((~g).numeric_rep()) - # print((~g).matrix_form()) - # print(~g).ordered_vertices()) - # print(basis_change) if len(ent_dofs_ids) == basis_change.shape[0]: sub_mat = basis_change elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index cb717f8e..31120d7c 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -80,7 +80,7 @@ def construct_tet_cg4(cell=None, perm=True): # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] xs = [DOF(DeltaPairing(), PointKernel((0, 2*np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, diff_C3, S1), perm) + DOFGenerator(xs, C3, S1), perm) xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) @@ -123,7 +123,9 @@ def test_tet_cg3(): def test_tet_cg4(): cg4 = construct_tet_cg4() cg4.generate() - cg4.plot(filename="tet_cg4.png") + # cg4.plot(filename="tet_cg4.png") + for dof in cg4.generate(): + print(dof) cg4.to_fiat() @@ -248,6 +250,8 @@ def construct_tet_ned2(tet=None, perm=None): # v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] + # breakpoint() + # xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] # xs = [DOF(L2Pairing(), VectorKernel((v_1 - v_0)/2)), DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)),] center_dofs = DOFGenerator(xs, S2, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) @@ -284,9 +288,8 @@ def test_tet_rt2(): ls = rt2.generate() # TODO make this a proper test for dof in ls: - print(dof) + print(dof.to_quadrature(1)) rt2.to_fiat() - breakpoint() def test_tet_rt(): diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 5e9ccebd..f0829cd6 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -678,7 +678,7 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): def test_linear_vec(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell) - i = 0 + i = 1 mesh = UnitCubeMesh(2 ** i, 2 ** i, 2 ** i) x = SpatialCoordinate(mesh) candidate_vecs = [ @@ -690,7 +690,7 @@ def test_linear_vec(elem_gen, elem_code, deg): ] # group = sp.combinatorics.SymmetricGroup(4).elements # scale_range = range(0, 2) - error_rows = [] + # error_rows = [] # for i in scale_range: # for g in group: for v in candidate_vecs: @@ -704,9 +704,7 @@ def test_linear_vec(elem_gen, elem_code, deg): CG3 = VectorFunctionSpace(mesh, "CG", 3) res3 = assemble(interpolate(res2, CG3)) res4 = assemble(interpolate(vec, CG3)) - if not np.allclose(res3.dat.data, res4.dat.data): - error_rows += [v] - breakpoint() + assert np.allclose(res3.dat.data, res4.dat.data) else: V = FunctionSpace(mesh, elem_code, deg) res1 = assemble(interpolate(vec, V)) @@ -714,7 +712,6 @@ def test_linear_vec(elem_gen, elem_code, deg): res3 = assemble(interpolate(res1, CG3)) res4 = assemble(interpolate(vec, CG3)) assert np.allclose(res3.dat.data, res4.dat.data) - breakpoint() @pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_rt, "RT", 1), @@ -751,7 +748,6 @@ def vec(mesh, array=False): res3 = assemble(interpolate(res2, CG3)) error_rows = [] print(g) - # breakpoint() for i in range(res3.dat.data.shape[0]): if not np.allclose(res3.dat.data[i], vec(mesh, array=True)): print(res3.dat.data[i]) @@ -821,12 +817,12 @@ def expr(mesh): @pytest.mark.parametrize("elem_gen,elem_code,deg", - [(construct_tet_rt2, "RT", 2), (construct_tet_ned2, "N1curl", 2) + [(construct_tet_cg4, "CG", 4), (construct_tet_rt2, "RT", 2), (construct_tet_ned2, "N1curl", 2) ]) def test_3d_two_form(elem_gen, elem_code, deg): cell = make_tetrahedron() - mesh = UnitCubeMesh(3, 3, 3) + mesh = UnitTetrahedronMesh() x = SpatialCoordinate(mesh) spaces = [] @@ -840,11 +836,22 @@ def test_3d_two_form(elem_gen, elem_code, deg): for name, V, V2 in spaces: v = TestFunction(V) u = TrialFunction(V2) - exp = as_vector([cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0])]) + if elem_code == "CG": + exp = cos((3/4)*pi*x[0]) + else: + exp = as_vector([cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0]), cos((3/4)*pi*x[0])]) + # exp = as_vector([x[0], x[1], x[2]]) f = assemble(interpolate(exp, V2)) - a = inner(v, u) * dx - L = inner(f, v) * dx + # print("A") + a = assemble(inner(u, v) * dx) + np.set_printoptions(linewidth=90, precision=4, suppress=True) + # print(a.M.values[np.ix_(V.cell_node_list[0][12:16], V.cell_node_list[0][12:16])]) + # np.matmul(L.M.values[np.ix_([0, 1],[0, 1])], transform.T) + print("L") + L = assemble(inner(f, v) * dx) + print(L.dat.data) + # L.dat.data_rw[0:2] = np.matmul(L.dat.data[0:2], transform.T) solution = Function(V2) solve(a == L, solution) From 9bf3a1bbbe3c381f17a56fd029241dec9324a9e2 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 4 Mar 2026 16:35:23 +0000 Subject: [PATCH 59/98] switch group for cg4 back over --- test/test_3d_examples_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 31120d7c..a4259b92 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -80,7 +80,7 @@ def construct_tet_cg4(cell=None, perm=True): # xs = [DOF(DeltaPairing(), PointKernel((-0.3919 * 0.8516, -0.226 * 0.8516)))] xs = [DOF(DeltaPairing(), PointKernel((0, 2*np.sqrt(3)/9)))] dg1_face = ElementTriple(face, (P1, CellL2, "C0"), - DOFGenerator(xs, C3, S1), perm) + DOFGenerator(xs, diff_C3, S1), perm) xs = [DOF(DeltaPairing(), PointKernel((0, 0, 0)))] int_dof = DOFGenerator(xs, S1, S1) From 3a1f6fc52d60874852e63c082158468fc85c6851 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 4 Mar 2026 17:39:09 +0000 Subject: [PATCH 60/98] some tidying --- test/test_convert_to_fiat.py | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index f0829cd6..a05e5343 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -688,15 +688,8 @@ def test_linear_vec(elem_gen, elem_code, deg): [x[2], 0, 0], [0, x[2], 0], [0, 0, x[2]], [x[0], x[1], 0], [x[1], x[0], 0], [x[1], 0, x[0]], [x[0], x[1], x[2]] ] - # group = sp.combinatorics.SymmetricGroup(4).elements - # scale_range = range(0, 2) - # error_rows = [] - # for i in scale_range: - # for g in group: + for v in candidate_vecs: - # from firedrake.utility_meshes import TwoTetMesh - # g = group[3] - # mesh = TwoTetMesh(perm=g) vec = as_vector(v) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) @@ -738,7 +731,6 @@ def vec(mesh, array=False): error_gs = [] error_row_lists = [] for g in group: - # mesh = UnitTetrahedronMesh() mesh = TwoTetMesh(perm=g) print(mesh.entity_orientations) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): @@ -747,7 +739,6 @@ def vec(mesh, array=False): CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) res3 = assemble(interpolate(res2, CG3)) error_rows = [] - print(g) for i in range(res3.dat.data.shape[0]): if not np.allclose(res3.dat.data[i], vec(mesh, array=True)): print(res3.dat.data[i]) @@ -764,23 +755,22 @@ def vec(mesh, array=False): assert len(error_gs) == 0 -@pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 0.05), - (construct_tet_cg4, "CG", 4, 0.04), +@pytest.mark.parametrize("elem_gen,elem_code,deg,max_err", [(create_cg3_tet, "CG", 3, 1e-13), + (construct_tet_cg4, "CG", 4, 1e-13), (construct_tet_rt2, "RT", 2, 1e-13), (construct_tet_ned2, "N1curl", 2, 1e-13)]) def test_const_two_tet(elem_gen, elem_code, deg, max_err): cell = make_tetrahedron() - elem_perms = elem_gen(cell, perm=True) + # elem_perms = elem_gen(cell, perm=True) elem_mats = elem_gen(cell, perm=False) - ufl_elem_perms = elem_perms.to_ufl() + # ufl_elem_perms = elem_perms.to_ufl() ufl_elem_mats = elem_mats.to_ufl() def expr(mesh): x = SpatialCoordinate(mesh) if elem_code == "RT" or elem_code == "N1curl": return as_vector([x[0], 2*x[1], 3*x[2]]) - # return as_vector([cos((3/4)*pi*x[0]), cos((3/4)*pi*x[1]), cos((3/4)*pi*x[2])]) - return cos((3/4)*pi*x[0]) + return x[0] errors = [] from firedrake.utility_meshes import TwoTetMesh group = [sp.combinatorics.Permutation([0, 1, 2, 3]), @@ -795,13 +785,13 @@ def expr(mesh): print(g) print(mesh.entity_orientations) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): - if elem_code != "RT" and elem_code != "N1curl": - V = FunctionSpace(mesh, ufl_elem_perms) + # if elem_code != "RT" and elem_code != "N1curl": + # V = FunctionSpace(mesh, ufl_elem_perms) - res = project(V, mesh, expr(mesh)) - print("perms", res) - # assert res < max_err - errors += [res] + # res = project(V, mesh, expr(mesh)) + # print("perms", res) + # # assert res < max_err + # errors += [res] V2 = FunctionSpace(mesh, ufl_elem_mats) res = project(V2, mesh, expr(mesh)) From 6ae8526207e9e49d75d87e921cc2b70200fec319 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 10:20:48 +0000 Subject: [PATCH 61/98] xfail test --- test/test_convert_to_fiat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index a05e5343..2464583f 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -360,7 +360,7 @@ def test_entity_perms(elem_gen, cell): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), - (create_dg2, "DG", 2), + pytest.param(create_dg2, "DG", 2, marks=pytest.mark.xfail(reason='Need to update TSFC in CI')), (create_cg2, "CG", 2) ]) def test_1d(elem_gen, elem_code, deg): From c12608e7ab0338c661040dba5054ad340bbadd5d Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 11:29:27 +0000 Subject: [PATCH 62/98] fix quadrature degree, implement bdm and ned 2nd kind on triangles --- fuse/dof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 5cd0439e..7b35229f 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -223,7 +223,7 @@ def __repr__(self): def degree(self, interpolant_degree): if len(self.fn.free_symbols) == 0: return interpolant_degree - return self.fn.as_poly().total_degree() * interpolant_degree + return self.fn.as_poly().total_degree() + interpolant_degree def permute(self, g): # new_fn = self.fn.subs({self.syms[i]: g(self.syms)[i] for i in range(len(self.syms))}) @@ -339,7 +339,7 @@ def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree), {}, str(self)) def to_quadrature(self, arg_degree): - Qpts, Qwts = self.cell_defined_on.quadrature(arg_degree) + Qpts, Qwts = self.cell_defined_on.quadrature(self.kernel.degree(arg_degree)) Qwts = Qwts.reshape(Qwts.shape + (1,)) dim = self.cell_defined_on.get_spatial_dimension() if dim > 0: From 16a67c2ae2d0727853e84f9f97e24457ed47c83c Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 11:30:15 +0000 Subject: [PATCH 63/98] remember to add test files --- test/test_2d_examples_docs.py | 40 +++++++++++++++++++++++++++++++++++ test/test_orientations.py | 4 +++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 21447d32..52a5b254 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -183,6 +183,46 @@ def construct_nd(tri=None): return ned +def construct_nd_2nd_kind(tri=None): + if tri is None: + tri = polygon(3) + deg = 1 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dofs = DOFGenerator(xs, S2, S2) + int_ned = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + + xs = [immerse(tri, int_ned, TrHCurl)] + tri_dofs = DOFGenerator(xs, C3, S1) + + nd = PolynomialSpace(deg, set_shape=True) + + ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs]) + return ned + +def construct_bdm(tri=None): + if tri is None: + tri = polygon(3) + deg = 1 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dofs = DOFGenerator(xs, S2, S2) + int_rt = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHDiv, C0), dofs) + + xs = [immerse(tri, int_rt, TrHDiv)] + tri_dofs = DOFGenerator(xs, C3, S1) + + nd = PolynomialSpace(deg, set_shape=True) + + rt = ElementTriple(tri, (nd, CellHDiv, C0), [tri_dofs]) + return rt + def test_nd_example(): tri = polygon(3) diff --git a/test/test_orientations.py b/test/test_orientations.py index 31780c9c..1816e63c 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -3,7 +3,7 @@ from fuse import * import numpy as np import sympy as sp -from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt +from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt, construct_nd_2nd_kind, construct_bdm from test_convert_to_fiat import create_cg1, create_cg2_tri, create_dg1 import os @@ -277,7 +277,9 @@ def test_convergence(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg,conv_rate", [(construct_nd, "N1curl", 1, 0.8), + (construct_nd_2nd_kind, "N2curl", 1, 1.8), (construct_rt2, "RT", 2, 1.8), + (construct_bdm, "BDM", 1, 1.8), (construct_nd2, "N1curl", 2, 1.8)]) def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): cell = polygon(3) From 76a650baecfa82d7ac1e817f446f44ae9a6a51f8 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 11:31:08 +0000 Subject: [PATCH 64/98] lint --- test/test_2d_examples_docs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 52a5b254..bf30e1cb 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -189,7 +189,6 @@ def construct_nd_2nd_kind(tri=None): deg = 1 edge = tri.edges()[0] x = sp.Symbol("x") - y = sp.Symbol("y") xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] dofs = DOFGenerator(xs, S2, S2) @@ -203,13 +202,13 @@ def construct_nd_2nd_kind(tri=None): ned = ElementTriple(tri, (nd, CellHCurl, C0), [tri_dofs]) return ned + def construct_bdm(tri=None): if tri is None: tri = polygon(3) deg = 1 edge = tri.edges()[0] x = sp.Symbol("x") - y = sp.Symbol("y") xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] dofs = DOFGenerator(xs, S2, S2) @@ -223,6 +222,7 @@ def construct_bdm(tri=None): rt = ElementTriple(tri, (nd, CellHDiv, C0), [tri_dofs]) return rt + def test_nd_example(): tri = polygon(3) From 9593bce5fb2253bdcb4a8a2ac2a1aaacab8d37f1 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 11:50:57 +0000 Subject: [PATCH 65/98] tet BDM and ned 2nd kind --- test/test_3d_examples_docs.py | 54 ++++++++++++++++++++++++++++++----- test/test_convert_to_fiat.py | 7 +++-- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index a4259b92..0977d6e0 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -187,10 +187,29 @@ def construct_tet_rt2(cell=None, perm=None): DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] interior = DOFGenerator(xs, S1, S4) - rt1 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), + rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [faces, interior]) - # [test_tet_rt 1] - return rt1 + return rt2 + + +def construct_tet_bdm(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 1 + x = sp.Symbol("x") + y = sp.Symbol("y") + + rt_space = PolynomialSpace(deg, set_shape=True) + + xs = [DOF(L2Pairing(), PolynomialKernel(1/3 - (1/2)*x + y/(2*np.sqrt(3)), symbols=(x, y)))] + dofs = DOFGenerator(xs, C3, S2) + face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [faces]) + return rt2 def construct_tet_ned(cell=None): @@ -225,6 +244,31 @@ def construct_tet_ned(cell=None): return ElementTriple(tet, (nd_space, CellHCurl, L2), [edge_dofs]) +def construct_tet_ned_2nd_kind(tet=None, perm=None): + if tet is None: + tet = make_tetrahedron() + deg = 1 + edge = tet.edges()[0] + face = tet.d_entities(2, get_class=True)[0] + x = sp.Symbol("x") + + vec_Pd = PolynomialSpace(deg, set_shape=True) + nd_space = vec_Pd + + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dofs = DOFGenerator(xs, S2, S2) + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + + xs = [immerse(tet, int_ned1, TrHCurl)] + tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), + Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), + Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) + edge_dofs = DOFGenerator(xs, tet_edges, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs]) + return ned + + def construct_tet_ned2(tet=None, perm=None): if tet is None: tet = make_tetrahedron() @@ -257,10 +301,6 @@ def construct_tet_ned2(tet=None, perm=None): face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) - # tempned = ElementTriple(tet, (nd_space, TrHCurl(tet), C0), [face_dofs]) - # ptdicts = [d.to_quadrature(1) for d in tempned.generate()] - # print(ptdicts[0]) - # print(ptdicts[1]) xs = [immerse(tet, int_ned1, TrHCurl)] tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 2464583f..bee86500 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 -from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned2, construct_tet_cg4 +from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -599,11 +599,12 @@ def test_project_3d(elem_gen, elem_code, deg): (create_cg1_tet, "CG", 1, 1.8), (create_cg2_tet, "CG", 2, 2.8), (create_cg3_tet, "CG", 3, 3.8), - # (construct_tet_cg4, "CG", 4, 4.8), (construct_tet_rt, "RT", 1, 0.8), (construct_tet_rt2, "RT", 2, 1.8), (construct_tet_ned, "N1curl", 1, 0.8), - (construct_tet_ned2, "N1curl", 2, 1.8)]) + (construct_tet_ned2, "N1curl", 2, 1.8), + (construct_tet_ned_2nd_kind, "N2curl", 1, 1.8), + (construct_tet_bdm, "BDM", 1, 1.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) From a9c3dd60d48dabf273183d00ae0276edf87ad861 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 5 Mar 2026 13:11:05 +0000 Subject: [PATCH 66/98] lint --- test/test_3d_examples_docs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 0977d6e0..803c3619 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -249,7 +249,6 @@ def construct_tet_ned_2nd_kind(tet=None, perm=None): tet = make_tetrahedron() deg = 1 edge = tet.edges()[0] - face = tet.d_entities(2, get_class=True)[0] x = sp.Symbol("x") vec_Pd = PolynomialSpace(deg, set_shape=True) From 50720518355ac339a77f4093f6d6661e0253f760 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Fri, 6 Mar 2026 11:56:34 +0000 Subject: [PATCH 67/98] enable polynomial kernel to be vector valued, bdm2 on triangles --- fuse/dof.py | 39 ++++++++++++++++++++++------------- test/test_2d_examples_docs.py | 35 +++++++++++++++++++++++++++++++ test/test_orientations.py | 4 +++- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 7b35229f..126a37a6 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -209,10 +209,15 @@ def _from_dict(obj_dict): class PolynomialKernel(BaseKernel): - def __init__(self, fn, g=None, symbols=[]): - if len(symbols) != 0 and not sp.sympify(fn).as_poly(): - raise ValueError("Function argument must be able to be interpreted as a sympy polynomial") - self.fn = sp.sympify(fn) + def __init__(self, fn, g=None, symbols=[], shape=0): + if len(symbols) != 0 and (shape != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape))) and not sp.sympify(fn).as_poly(): + raise ValueError("Function argument or its components must be able to be interpreted as a sympy polynomial") + if shape != 0: + self.fn = [sp.sympify(fn[i]).as_poly() for i in range(shape)] + self.shape = shape + else: + self.fn = sp.sympify(fn) + self.shape = 0 self.g = g self.syms = symbols super(PolynomialKernel, self).__init__() @@ -221,22 +226,24 @@ def __repr__(self): return str(self.fn) def degree(self, interpolant_degree): - if len(self.fn.free_symbols) == 0: + if self.shape != 0: + return max([self.fn[i].as_poly().total_degree() for i in range(self.shape)]) + interpolant_degree + if len(self.fn.free_symbols) == 0: # this should probably be removed return interpolant_degree return self.fn.as_poly().total_degree() + interpolant_degree def permute(self, g): # new_fn = self.fn.subs({self.syms[i]: g(self.syms)[i] for i in range(len(self.syms))}) new_fn = self.fn - return PolynomialKernel(new_fn, g=g, symbols=self.syms) + return PolynomialKernel(new_fn, g=g, symbols=self.syms, shape=self.shape) def __call__(self, *args): - res = sympy_to_numpy(self.fn, self.syms, args[:len(self.syms)]) - # if not hasattr(res, '__iter__'): - # return [res] - # if self.g: - # print(self.g, self.g(res)) - # return self.g(res) + 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)])] return res def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): @@ -356,6 +363,8 @@ def immersed(pt): return np.matmul(basis_coeffs, immersed_basis) else: immersed = self.immersed + if self.cell_defined_on.dimension == 2: + print(basis_change) pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) if self.immersed: @@ -374,10 +383,12 @@ def immersed(pt): new_wts = np.outer(wts, immersion) else: new_wts = wts - # else: - # pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, self.immersed) # pt dict is { pt: [(weight, component)]} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} + if self.cell_defined_on.dimension == 2: + np.set_printoptions(linewidth=90, precision=4, suppress=True) + for key, val in pt_dict.items(): + print(np.array(key), ":", np.array([v[0] for v in val])) return pt_dict def __repr__(self, fn="v"): diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index bf30e1cb..c2c38d66 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -223,6 +223,41 @@ def construct_bdm(tri=None): return rt +def construct_bdm2(tri=None): + if tri is None: + tri = polygon(3) + deg = 2 + edge = tri.edges()[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + + dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] + int_rt = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHDiv, C0), dofs) + + xs = [immerse(tri, int_rt, TrHDiv)] + tri_dofs = DOFGenerator(xs, C3, S1) + + v_0 = np.array(tri.get_node(tri.ordered_vertices()[0], return_coords=True)) + v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) + v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) + + phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] + xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y), shape=2)), + DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y), shape=2)), + DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y), shape=2))] + interior = DOFGenerator(xs, S1, S1) + + nd = PolynomialSpace(deg, set_shape=True) + + rt = ElementTriple(tri, (nd, CellHDiv, C0), [tri_dofs, interior]) + return rt + + def test_nd_example(): tri = polygon(3) diff --git a/test/test_orientations.py b/test/test_orientations.py index 1816e63c..63c4a492 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -3,7 +3,7 @@ from fuse import * import numpy as np import sympy as sp -from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt, construct_nd_2nd_kind, construct_bdm +from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt, construct_nd_2nd_kind, construct_bdm, construct_bdm2 from test_convert_to_fiat import create_cg1, create_cg2_tri, create_dg1 import os @@ -280,6 +280,7 @@ def test_convergence(elem_gen, elem_code, deg, conv_rate): (construct_nd_2nd_kind, "N2curl", 1, 1.8), (construct_rt2, "RT", 2, 1.8), (construct_bdm, "BDM", 1, 1.8), + (construct_bdm2, "BDM", 2, 2.8), (construct_nd2, "N1curl", 2, 1.8)]) def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): cell = polygon(3) @@ -291,6 +292,7 @@ def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): mesh = UnitSquareMesh(2**n, 2**n) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, elem.to_ufl()) + breakpoint() else: V = FunctionSpace(mesh, elem_code, deg) x, y = SpatialCoordinate(mesh) From a54346ca07152e2af2567d01dde11b060d7fa10a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Fri, 6 Mar 2026 14:28:17 +0000 Subject: [PATCH 68/98] remove breakpoint and lint --- fuse/dof.py | 2 -- test/test_2d_examples_docs.py | 4 ---- test/test_orientations.py | 1 - 3 files changed, 7 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 126a37a6..7c07f1c9 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -363,8 +363,6 @@ def immersed(pt): return np.matmul(basis_coeffs, immersed_basis) else: immersed = self.immersed - if self.cell_defined_on.dimension == 2: - print(basis_change) pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) if self.immersed: diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index c2c38d66..a7117416 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -240,10 +240,6 @@ def construct_bdm2(tri=None): xs = [immerse(tri, int_rt, TrHDiv)] tri_dofs = DOFGenerator(xs, C3, S1) - v_0 = np.array(tri.get_node(tri.ordered_vertices()[0], return_coords=True)) - v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) - v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) - phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] diff --git a/test/test_orientations.py b/test/test_orientations.py index 63c4a492..b82edbe3 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -292,7 +292,6 @@ def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): mesh = UnitSquareMesh(2**n, 2**n) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V = FunctionSpace(mesh, elem.to_ufl()) - breakpoint() else: V = FunctionSpace(mesh, elem_code, deg) x, y = SpatialCoordinate(mesh) From 160d166f4cdfd94d3893e3708d4bab404210c7f8 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Fri, 6 Mar 2026 19:04:29 +0000 Subject: [PATCH 69/98] draft nedelec 3 on tets, refactor some code in poly kernel --- fuse/dof.py | 14 ++++----- test/test_3d_examples_docs.py | 56 +++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 7c07f1c9..4e3cc9bb 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -210,14 +210,14 @@ def _from_dict(obj_dict): class PolynomialKernel(BaseKernel): def __init__(self, fn, g=None, symbols=[], shape=0): - if len(symbols) != 0 and (shape != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape))) and not sp.sympify(fn).as_poly(): + self.shape = shape + try: + if shape != 0: + self.fn = [sp.sympify(fn[i]).as_poly() for i in range(shape)] + else: + self.fn = sp.sympify(fn) + except ValueError: raise ValueError("Function argument or its components must be able to be interpreted as a sympy polynomial") - if shape != 0: - self.fn = [sp.sympify(fn[i]).as_poly() for i in range(shape)] - self.shape = shape - else: - self.fn = sp.sympify(fn) - self.shape = 0 self.g = g self.syms = symbols super(PolynomialKernel, self).__init__() diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 803c3619..5bc87be1 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -293,24 +293,58 @@ def construct_tet_ned2(tet=None, perm=None): # v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] - # breakpoint() - # xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] - # xs = [DOF(L2Pairing(), VectorKernel((v_1 - v_0)/2)), DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)),] + center_dofs = DOFGenerator(xs, S2, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) xs = [immerse(tet, int_ned1, TrHCurl)] - tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), - Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), - Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) return ned +def construct_tet_ned3(tet=None, perm=None): + if tet is None: + tet = make_tetrahedron() + deg = 3 + edge = tet.edges()[0] + face = tet.d_entities(2, get_class=True)[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) + + phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] + phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] + xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2)), + DOF(L2Pairing(), PolynomialKernel(phi1, symbols=(x, y), shape=2))] + face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S3)) + face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) + + xs = [DOF(L2Pairing(), VectorKernel([1, 0, 0])), + DOF(L2Pairing(), VectorKernel([0, 1, 0])), + DOF(L2Pairing(), VectorKernel([0, 0, 1]))] + int_dofs = DOFGenerator(xs, S1, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs, int_dofs]) + return ned + + def test_plot_tet_ned2(): nd = construct_tet_ned2() # nd.plot(filename="new.png") @@ -352,6 +386,16 @@ def test_tet_nd(): nd1.to_fiat() +def test_tet_nd3(): + nd3 = construct_tet_ned3() + ls = nd3.generate() + for dof in ls: + # dof_dict = dof.to_quadrature(1) + # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) + print(dof) + nd3.to_fiat() + + def plot_tet_ned(): ned = construct_tet_ned() ned.plot(filename="tet_nd2_fiat.png") From e7d4fb02b8b40f7e2120c2c9053b777753a4af26 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 9 Mar 2026 12:59:46 +0000 Subject: [PATCH 70/98] compute basis group, fix inconsistency in group application, tidy --- fuse/cells.py | 19 ++++++++++++++----- fuse/dof.py | 25 +++++++++---------------- fuse/groups.py | 27 ++------------------------- test/test_3d_examples_docs.py | 17 ++++------------- test/test_cells.py | 16 ++++++++++++++++ test/test_orientations.py | 14 +++++--------- 6 files changed, 50 insertions(+), 68 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index ab06eb98..0ca41d59 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -315,8 +315,8 @@ def __init__(self, d, edges=[], vertex_num=None, oriented=False, group=None, edg self.group = group if not group: self.group = self.compute_cell_group() - self.group = self.group.add_cell(self) + self.basis_group = self.compute_basis_group() def compute_attachments(self, n, points, orientations=None): """ @@ -388,6 +388,19 @@ def compute_cell_group(self): break return fuse_groups.PermutationSetRepresentation(list(accepted_perms)) + def compute_basis_group(self): + if self.dimension == 0: + return fuse_groups.S1.add_cell(self) + bvs = self.basis_vectors() + bv_0 = bvs[0] + basis_group_members = [] + for bv in bvs: + for g in self.group.members(): + if np.allclose(np.array(g(bv_0)), np.array(bv)): + basis_group_members += [g.perm] + break + return fuse_groups.PermutationSetRepresentation(list(basis_group_members)).add_cell(self) + def get_spatial_dimension(self): return self.dimension @@ -475,12 +488,8 @@ def _subentity_traversal(self, sub_ents, min_ids): sub_ents = self.get_node(p)._subentity_traversal(sub_ents, min_ids) if dim > 1: connections = [(c.point.id, c.point.group.identity) for c in self.connections] - # if self.oriented: - # connections = self.permute_entities(self.oriented, dim - 1) # if self.dimension == 2: # connections = [connections[-1]] + connections[:-1] - # print([self.get_node(c[0]).id - min_ids[1] for c in connections]) - # print([c.point.id - min_ids[1] for c in self.connections]) for e, o in connections: p = self.get_node(e).orient(o) p_dim = p.get_spatial_dimension() diff --git a/fuse/dof.py b/fuse/dof.py index 4e3cc9bb..bf35d7a0 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -152,7 +152,7 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + def evaluate(self, Qpts, Qwts, immersed, dim): return np.array([self.pt for _ in Qpts]).astype(np.float64), np.ones_like(Qwts), [[tuple()] for pt in Qpts] def _to_dict(self): @@ -189,12 +189,12 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + def evaluate(self, Qpts, Qwts, immersed, dim): 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] 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*self.g(self.pt) for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + return Qpts, np.array([wt*immersed(self.g(self.pt))for wt in Qwts]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] def _to_dict(self): o_dict = {"pt": self.pt} @@ -246,8 +246,8 @@ def __call__(self, *args): res += [sympy_to_numpy(self.fn[i], self.syms, args[:len(self.syms)])] return res - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + def evaluate(self, Qpts, Qwts, immersed, dim): + return Qpts, np.array([wt*self(*self.g(pt)) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] def _to_dict(self): o_dict = {"fn": self.fn} @@ -279,9 +279,8 @@ def __call__(self, *args): return tuple(args[i] if i in self.comp else 0 for i in range(len(args))) # return tuple(args[c] for c in self.comp) - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + def evaluate(self, Qpts, Qwts, immersed, dim): return Qpts, Qwts, [[self.comp] for pt in Qpts] - # return Qpts, np.array([self(*pt) for pt in Qpts]).astype(np.float64) def _to_dict(self): o_dict = {"comp": self.comp} @@ -348,13 +347,7 @@ def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): def to_quadrature(self, arg_degree): Qpts, Qwts = self.cell_defined_on.quadrature(self.kernel.degree(arg_degree)) Qwts = Qwts.reshape(Qwts.shape + (1,)) - dim = self.cell_defined_on.get_spatial_dimension() - if dim > 0: - bvs = np.array(self.cell_defined_on.basis_vectors()) - new_bvs = np.array(self.cell_defined_on.orient(self.pairing.orientation).basis_vectors()) - basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) - else: - basis_change = np.eye(dim) + if self.immersed and isinstance(self.kernel, VectorKernel): def immersed(pt): basis = np.array(self.cell_defined_on.basis_vectors()).T @@ -363,7 +356,7 @@ def immersed(pt): return np.matmul(basis_coeffs, immersed_basis) else: immersed = self.immersed - pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) + pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, immersed, self.cell.dimension) if self.immersed: # need to compute jacobian from attachment. diff --git a/fuse/groups.py b/fuse/groups.py index e63a9d32..173ea4b2 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -123,7 +123,8 @@ def __init__(self, perm_list, cell=None): if cell is not None: self.cell = cell - vertices = cell.vertices(return_coords=True) + verts = cell.ordered_vertices() + vertices = [cell.get_node(v, return_coords=True) for v in verts] self._members = [] counter = 0 @@ -221,7 +222,6 @@ def __init__(self, base_group, cell=None): self.generators = [] if cell is not None: self.cell = cell - # vertices = cell.vertices(return_coords=True) verts = cell.ordered_vertices() vertices = [cell.get_node(v, return_coords=True) for v in verts] @@ -272,29 +272,6 @@ def size(self): assert len(self._members) == self.base_group.order() return self.base_group.order() - def members(self, perm=False): - if self.cell is None: - raise ValueError("Group does not have a domain - members have not been calculated") - if perm: - return [m.perm for m in self._members] - return self._members - - def transform_between_perms(self, perm1, perm2): - member_perms = self.members(perm=True) - perm1 = Permutation.from_sequence(perm1) - perm2 = Permutation.from_sequence(perm2) - assert perm1 in member_perms - assert perm2 in member_perms - return ~self.get_member(Permutation(perm1)) * self.get_member(Permutation(perm2)) - - # def get_member(self, perm): - # if not isinstance(perm, Permutation): - # perm = Permutation.from_sequence(perm) - # for m in self.members(): - # if m.perm == perm: - # return m - # raise ValueError("Permutation not a member of group") - # def compute_reps(self, g, path, remaining_members): # # breadth first search to find generator representations of all members # if len(remaining_members) == 0: diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 5bc87be1..a4926763 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -178,14 +178,8 @@ def construct_tet_rt2(cell=None, perm=None): im_xs = [immerse(cell, face_vec, TrHDiv)] faces = DOFGenerator(im_xs, tet_faces, S1) - v_0 = np.array(cell.get_node(cell.ordered_vertices()[0], return_coords=True)) - v_1 = np.array(cell.get_node(cell.ordered_vertices()[1], return_coords=True)) - v_2 = np.array(cell.get_node(cell.ordered_vertices()[2], return_coords=True)) - v_3 = np.array(cell.get_node(cell.ordered_vertices()[3], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)), - DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2)), - DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] - interior = DOFGenerator(xs, S1, S4) + xs = [DOF(L2Pairing(), VectorKernel(cell.basis_vectors()[0]))] + interior = DOFGenerator(xs, cell.basis_group, S4) rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [faces, interior]) @@ -289,12 +283,9 @@ def construct_tet_ned2(tet=None, perm=None): dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) - v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) - # v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) - v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] + xs = [DOF(L2Pairing(), VectorKernel(cell.basis_vectors()[0]))] - center_dofs = DOFGenerator(xs, S2, S3) + center_dofs = DOFGenerator(xs, cell.basis_group, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) diff --git a/test/test_cells.py b/test/test_cells.py index ced8e36d..522905db 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -35,6 +35,22 @@ def test_basis_vectors(C): assert len(bv_ids) == len(bv_coords) +def test_basis_group(C): + if C.dimension == 0: + assert C.basis_group.size() == 1 + else: + bv_coords = C.basis_vectors(return_coords=True) + bv_0 = bv_coords[0] + for i, g in enumerate(C.basis_group.members()): + assert np.allclose(np.array(bv_coords[i]), np.array(g(bv_0))) + if C.dimension == 2: + for i, g in enumerate(C.basis_group.members()): + bvs = np.array(C.basis_vectors()) + new_bvs = np.array(C.orient(g).basis_vectors()) + basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) + assert np.allclose(np.array(bv_coords[i]), np.array(np.matmul(basis_change, bv_0))) + + def test_sub_basis_vectors(): cell = polygon(3) diff --git a/test/test_orientations.py b/test/test_orientations.py index b82edbe3..c7125ae7 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -22,14 +22,12 @@ def construct_nd2(tri=None): dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) - v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) - v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] - - center_dofs = DOFGenerator(xs, S2, S3) xs = [immerse(tri, int_ned1, TrHCurl)] tri_dofs = DOFGenerator(xs, C3, S1) + xs = [DOF(L2Pairing(), VectorKernel(tri.basis_vectors()[0]))] + center_dofs = DOFGenerator(xs, tri.basis_group, S3) + vec_Pk = PolynomialSpace(deg - 1, set_shape=True) Pk = PolynomialSpace(deg - 1) M = sp.Matrix([[y, -x]]) @@ -60,10 +58,8 @@ def construct_rt2(tri=None): xs = [immerse(tri, int_rt2, TrHDiv)] tri_dofs = DOFGenerator(xs, C3, S1) - v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) - v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) - i_xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] - i_dofs = DOFGenerator(i_xs, S2, S3) + i_xs = [DOF(L2Pairing(), VectorKernel(tri.basis_vectors()[0]))] + i_dofs = DOFGenerator(i_xs, tri.basis_group, S3) vec_Pk = PolynomialSpace(deg - 1, set_shape=True) Pk = PolynomialSpace(deg - 1) From ec1e3ddc252144cfca03f0eb6a9fdc98b54b91fa Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 9 Mar 2026 19:26:35 +0000 Subject: [PATCH 71/98] first stab at using cosets in orientations --- fuse/cells.py | 2 - fuse/dof.py | 15 +++-- fuse/groups.py | 40 +++++++++---- fuse/triples.py | 49 ++++++++++++---- test/test_2d_examples_docs.py | 1 + test/test_3d_examples_docs.py | 103 +++++++++++++++++++++++++++++----- test/test_cells.py | 7 +++ test/test_convert_to_fiat.py | 6 +- test/test_orientations.py | 14 ----- 9 files changed, 176 insertions(+), 61 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index cef4e0c4..0ca41d59 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -13,10 +13,8 @@ from fuse.utils import sympy_to_numpy, fold_reduce, numpy_to_str_tuple, orientation_value from FIAT.reference_element import Simplex, TensorProductCell as FiatTensorProductCell, Hypercube from FIAT.quadrature_schemes import create_quadrature -from FIAT.quadrature_schemes import create_quadrature from ufl.cell import Cell, TensorProductCell from functools import cache -from functools import cache class Arrow3D(FancyArrowPatch): diff --git a/fuse/dof.py b/fuse/dof.py index 22fe7a06..4fa6549e 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -193,7 +193,11 @@ def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): 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] if not immersed: + if not np.allclose(np.matmul(basis_change, self.pt), self.g(self.pt)): + breakpoint() 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] + if not np.allclose(np.matmul(basis_change, self.pt), self.g(self.pt)): + breakpoint() 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] def _to_dict(self): @@ -247,6 +251,9 @@ def __call__(self, *args): return res def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + for pt in Qpts: + if not np.allclose(self(*(np.matmul(basis_change, pt))), self(*self.g(pt))): + breakpoint() return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] def _to_dict(self): @@ -382,10 +389,10 @@ def immersed(pt): new_wts = wts # pt dict is { pt: [(weight, component)]} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} - if self.cell_defined_on.dimension == 2: - np.set_printoptions(linewidth=90, precision=4, suppress=True) - for key, val in pt_dict.items(): - print(np.array(key), ":", np.array([v[0] for v in val])) + # if self.cell_defined_on.dimension == 2: + # np.set_printoptions(linewidth=90, precision=4, suppress=True) + # for key, val in pt_dict.items(): + # print(np.array(key), ":", np.array([v[0] for v in val])) return pt_dict def __repr__(self, fn="v"): diff --git a/fuse/groups.py b/fuse/groups.py index 173ea4b2..4d787748 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -148,6 +148,31 @@ def __init__(self, perm_list, cell=None): def add_cell(self, cell): return PermutationSetRepresentation(self.perm_list, cell=cell) + def conjugacy_class(self, g): + conj_class = set() + for x in self.members(): + res = ~x * g * x + conj_class.add(res) + return conj_class + + def cosets(self, subset): + # Divides current group by given subset + # can be modified to allow members of given subset not to exist in group self + seen = self.members().copy() + cosets = [] + while len(seen) > 0: + g = seen[0] + coset = [] + for h in subset.members(): + # try: + coset += [g*h] + seen.remove(g*h) + # except ValueError: + # # member of subset not a member of superset + # pass + cosets += [coset] + return cosets + def members(self, perm=False): if self.cell is None: raise ValueError("Group does not have a domain - members have not been calculated") @@ -229,10 +254,8 @@ def __init__(self, base_group, cell=None): counter = 0 for g in self.base_group.elements: if len(vertices) > g.size: - temp_perm = Permutation(g, size=len(vertices)) - reordered = temp_perm(vertices) - else: - reordered = g(vertices) + g = Permutation(g, size=len(vertices)) + reordered = g(vertices) A = np.c_[np.array(vertices, dtype=float), np.ones(len(vertices))] b = np.array(reordered, dtype=float) @@ -256,14 +279,7 @@ def __init__(self, base_group, cell=None): # self._members = sorted(self._members, key=lambda g: g.numeric_rep()) else: self.cell = None - - def conjugacy_class(self, g): - conj_class = set() - for x in self.members(): - res = ~x * g * x - conj_class.add(res) - return conj_class - + def add_cell(self, cell): return GroupRepresentation(self.base_group, cell=cell) diff --git a/fuse/triples.py b/fuse/triples.py index 32a72208..4d0c66bb 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -85,6 +85,13 @@ def setup_ids_and_nodes(self): def setup_matrices(self): # self.matrices_by_entity = self.make_entity_dense_matrices(self.ref_el, self.entity_ids, self.nodes, self.poly_set) matrices, entity_perms, pure_perm = self.make_dof_perms(self.ref_el, self.entity_ids, self.nodes, self.poly_set) + # new_matrices = matrices.copy() + # if not pure_perm: + # for j in range(4): + # for i, k in zip([0, 3, 4], [0, 3, 4]): + # new_matrices[2][j][i] = matrices[2][j][k] + # # new_matrices[2][j][k] = matrices[2][j][i] + # matrices = new_matrices reversed_matrices = self.reverse_dof_perms(matrices) if self.perm: self.pure_perm = pure_perm @@ -424,6 +431,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if ed.id not in total_ent_dof_ids] # dof_idx = [total_ent_dof_ids.index(id) for id in ent_dofs_ids] dof_gen_class = ent_dofs[0].generation + g_to_ent_id = {str(sub_g.perm.array_form): ent_id for ent_id, sub_g in zip(ent_dofs_ids, dof_gen_class[dim].g1.members())} if not len(dof_gen_class[dim].g2.members()) == 1 and dim == min(dof_gen_class.keys()): # if DOFs on entity are not perms, get the matrix @@ -431,36 +439,53 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): bvs = np.array(e.basis_vectors()) new_bvs = np.array(e.orient(~g).basis_vectors()) basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) - if len(ent_dofs_ids) == basis_change.shape[0]: - sub_mat = basis_change - elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: - # equivalently g1 trivial - sub_mat = ent_dofs[0].target_space.manipulate_basis(basis_change) - else: - # len(dof_gen_class[dim].g2.members()) == 2: - # case where value change is a restriction of the full transformation of the basis + cosets = dof_gen_class[dim].g1.cosets(e.basis_group) + if len(cosets[0]) < len(bvs): value_change = ent_dofs[0].target_space.manipulate_basis(basis_change) + else: + value_change = basis_change + if len(cosets) == 1: + sub_mat = value_change + else: + # check if this should be the cyclic variant sub_mat = np.kron((~g).matrix_form(), value_change) + new_ent_dofs_ids = [int(g_to_ent_id[str(sub_g.perm.array_form)]) for coset in cosets for sub_g in coset] + if not np.allclose(new_ent_dofs_ids, ent_dofs_ids): + print("original", sub_mat) + # ent_dofs_ids = new_ent_dofs_ids + oriented_mats_by_entity[dim][e_id][val][np.ix_(new_ent_dofs_ids, new_ent_dofs_ids)] = sub_mat.copy() + else: + oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + print("added", oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)]) + # if len(ent_dofs_ids) == basis_change.shape[0]: + # sub_mat = basis_change + # elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: + # # equivalently g1 trivial + # sub_mat = ent_dofs[0].target_space.manipulate_basis(basis_change) + # else: + # # len(dof_gen_class[dim].g2.members()) == 2: + # # case where value change is a restriction of the full transformation of the basis + # value_change = ent_dofs[0].target_space.manipulate_basis(basis_change) + # sub_mat = np.kron((~g).matrix_form(), value_change) # sub_mat = (~g).matrix_form() # elif len(ent_dofs_ids) != 1:# more dofs than dimension of g? # case for transforms where the basis vector is already included in the dof # sub_mat = np.kron((~g).matrix_form(), basis_change) # else: - # raise NotImplementedError("Unconsidered permuation case") - oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - + # raise NotImplementedError("Unconsidered permutation case") elif g.perm.is_Identity or (pure_perm and len(ent_dofs_ids) == 1): oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) elif dim < self.cell.dim(): # g in dof_gen_class[dim].g1.members() and # Permutation of DOF on the entity they are defined on sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim(): + elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim() and len(ent_dofs_ids) == len(g.perm.array_form): # case for dofs defined on the cell and not immersed sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() else: # TODO what if an orientation is not in G1 + # also the case of 3 dofs inside a 3d shape warnings.warn("FUSE: orientation case not covered") # sub_mat = g.matrix_form() # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index a7117416..a030fe83 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -271,6 +271,7 @@ def test_nd_example(): for dof in ned.generate(): assert [np.allclose(1, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 1 assert [np.allclose(0, dof.eval(basis_func).flatten()) for basis_func in basis_funcs].count(True) == 2 + ned.to_fiat() def construct_rt(tri=None): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 803c3619..896cad17 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -178,14 +178,16 @@ def construct_tet_rt2(cell=None, perm=None): im_xs = [immerse(cell, face_vec, TrHDiv)] faces = DOFGenerator(im_xs, tet_faces, S1) - v_0 = np.array(cell.get_node(cell.ordered_vertices()[0], return_coords=True)) - v_1 = np.array(cell.get_node(cell.ordered_vertices()[1], return_coords=True)) - v_2 = np.array(cell.get_node(cell.ordered_vertices()[2], return_coords=True)) - v_3 = np.array(cell.get_node(cell.ordered_vertices()[3], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)), - DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2)), - DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] - interior = DOFGenerator(xs, S1, S4) + # v_0 = np.array(cell.get_node(cell.ordered_vertices()[0], return_coords=True)) + # v_1 = np.array(cell.get_node(cell.ordered_vertices()[1], return_coords=True)) + # v_2 = np.array(cell.get_node(cell.ordered_vertices()[2], return_coords=True)) + # v_3 = np.array(cell.get_node(cell.ordered_vertices()[3], return_coords=True)) + # xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)), + # DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2)), + # DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] + # interior = DOFGenerator(xs, S1, S4) + xs = [DOF(L2Pairing(), VectorKernel(cell.basis_vectors()[0]))] + interior = DOFGenerator(xs, cell.basis_group, S4) rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [faces, interior]) @@ -290,24 +292,85 @@ def construct_tet_ned2(tet=None, perm=None): int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) - # v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) + v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) - xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2))] - # breakpoint() - # xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] - # xs = [DOF(L2Pairing(), VectorKernel((v_1 - v_0)/2)), DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)),] + # xs = [DOF(L2Pairing(), VectorKernel(face.basis_vectors()[1]))] + # # breakpoint() + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] + print((v_2 - v_1)/2) center_dofs = DOFGenerator(xs, S2, S3) + xs1 = [DOF(L2Pairing(), VectorKernel(face.basis_vectors()[0]))] + print(face.basis_vectors()[0]) + center_dofs1 = DOFGenerator(xs1, face.basis_group, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) - + face_vec1 = ElementTriple(face, (P1, CellHCurl, C0), center_dofs1) + im_xs1 = [immerse(tet, face_vec1, TrH1)] + face_dofs1 = DOFGenerator(im_xs1, tet_faces, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [face_dofs]) + ned1 = ElementTriple(tet, (nd_space, CellHCurl, C0), [face_dofs1]) + + dofs = ned.generate() + print("S2") + for d in dofs: + print(d) + pt_dict = d.to_quadrature(1) + for key, val in pt_dict.items(): + print(key, ":", val) + dofs1 = ned1.generate() + print("basis_group") + for d in dofs1: + print(d) + pt_dict = d.to_quadrature(1) + for key, val in pt_dict.items(): + print(key, ":", val) xs = [immerse(tet, int_ned1, TrHCurl)] tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) - ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs1]) + return ned + + +def construct_tet_ned3(tet=None, perm=None): + if tet is None: + tet = make_tetrahedron() + deg = 3 + edge = tet.edges()[0] + face = tet.d_entities(2, get_class=True)[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) + + phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] + # phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] + xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2))] + face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S3, S3)) + face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) + + xs = [DOF(L2Pairing(), VectorKernel([1, 0, 0])), + DOF(L2Pairing(), VectorKernel([0, 1, 0])), + DOF(L2Pairing(), VectorKernel([0, 0, 1]))] + int_dofs = DOFGenerator(xs, S1, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs, int_dofs]) return ned @@ -352,6 +415,16 @@ def test_tet_nd(): nd1.to_fiat() +def test_tet_nd3(): + nd3 = construct_tet_ned3() + ls = nd3.generate() + for dof in ls: + # dof_dict = dof.to_quadrature(1) + # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) + print(dof) + nd3.to_fiat() + + def plot_tet_ned(): ned = construct_tet_ned() ned.plot(filename="tet_nd2_fiat.png") diff --git a/test/test_cells.py b/test/test_cells.py index 522905db..173c15f5 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -50,6 +50,13 @@ def test_basis_group(C): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) assert np.allclose(np.array(bv_coords[i]), np.array(np.matmul(basis_change, bv_0))) +def test_cosets(): + cell = polygon(3) + g = cell.basis_group.members()[1] + conj = cell.group.cosets(cell.basis_group) + print(conj) + breakpoint() + def test_sub_basis_vectors(): cell = polygon(3) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index bee86500..de277e84 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -732,17 +732,19 @@ def vec(mesh, array=False): error_gs = [] error_row_lists = [] for g in group: + # mesh = UnitTetrahedronMesh() mesh = TwoTetMesh(perm=g) print(mesh.entity_orientations) if bool(os.environ.get("FIREDRAKE_USE_FUSE", 0)): V2 = FunctionSpace(mesh, elem.to_ufl()) + # print(elem.matrices[2][0][mesh.entity_orientations[1][10]][np.ix_([12, 13], [12, 13])]) res2 = assemble(interpolate(vec(mesh), V2)) CG3 = VectorFunctionSpace(mesh, create_cg3_tet(cell).to_ufl()) res3 = assemble(interpolate(res2, CG3)) error_rows = [] for i in range(res3.dat.data.shape[0]): if not np.allclose(res3.dat.data[i], vec(mesh, array=True)): - print(res3.dat.data[i]) + # print(res3.dat.data[i]) error_gs += [g] error_rows += [i] error_row_lists += [error_rows] @@ -760,7 +762,7 @@ def vec(mesh, array=False): (construct_tet_cg4, "CG", 4, 1e-13), (construct_tet_rt2, "RT", 2, 1e-13), (construct_tet_ned2, "N1curl", 2, 1e-13)]) -def test_const_two_tet(elem_gen, elem_code, deg, max_err): +def test_project_two_tet(elem_gen, elem_code, deg, max_err): cell = make_tetrahedron() # elem_perms = elem_gen(cell, perm=True) elem_mats = elem_gen(cell, perm=False) diff --git a/test/test_orientations.py b/test/test_orientations.py index 07104cbd..b82edbe3 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -22,14 +22,6 @@ def construct_nd2(tri=None): dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) -<<<<<<< HEAD - xs = [immerse(tri, int_ned1, TrHCurl)] - tri_dofs = DOFGenerator(xs, C3, S1) - - xs = [DOF(L2Pairing(), VectorKernel(tri.basis_vectors()[0]))] - center_dofs = DOFGenerator(xs, tri.basis_group, S3) - -======= v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] @@ -38,7 +30,6 @@ def construct_nd2(tri=None): xs = [immerse(tri, int_ned1, TrHCurl)] tri_dofs = DOFGenerator(xs, C3, S1) ->>>>>>> main vec_Pk = PolynomialSpace(deg - 1, set_shape=True) Pk = PolynomialSpace(deg - 1) M = sp.Matrix([[y, -x]]) @@ -69,15 +60,10 @@ def construct_rt2(tri=None): xs = [immerse(tri, int_rt2, TrHDiv)] tri_dofs = DOFGenerator(xs, C3, S1) -<<<<<<< HEAD - i_xs = [DOF(L2Pairing(), VectorKernel(tri.basis_vectors()[0]))] - i_dofs = DOFGenerator(i_xs, tri.basis_group, S3) -======= v_2 = np.array(tri.get_node(tri.ordered_vertices()[2], return_coords=True)) v_1 = np.array(tri.get_node(tri.ordered_vertices()[1], return_coords=True)) i_xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] i_dofs = DOFGenerator(i_xs, S2, S3) ->>>>>>> main vec_Pk = PolynomialSpace(deg - 1, set_shape=True) Pk = PolynomialSpace(deg - 1) From 2c961755a9cccbb0fac72da045be5007ded0aa28 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 10 Mar 2026 16:53:32 +0000 Subject: [PATCH 72/98] fiddling with nd3 no progress --- fuse/dof.py | 20 ++++++++++-------- fuse/triples.py | 4 ++-- test/test_3d_examples_docs.py | 40 ++++++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 4fa6549e..6fc26107 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -251,11 +251,13 @@ def __call__(self, *args): return res def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): - for pt in Qpts: - if not np.allclose(self(*(np.matmul(basis_change, pt))), self(*self.g(pt))): - breakpoint() - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] - + # for pt in Qpts: + # if not np.allclose(self(*(np.matmul(basis_change, pt))), self(*self.g(pt))): + # breakpoint() + # print("normal", self(*self.g(pt))) + # print("double rotate", self.g(self(*((~self.g)(pt))))) + return Qpts, np.array([wt*self(*(self.g(pt))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + def _to_dict(self): o_dict = {"fn": self.fn} return o_dict @@ -389,10 +391,10 @@ def immersed(pt): new_wts = wts # pt dict is { pt: [(weight, component)]} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} - # if self.cell_defined_on.dimension == 2: - # np.set_printoptions(linewidth=90, precision=4, suppress=True) - # for key, val in pt_dict.items(): - # print(np.array(key), ":", np.array([v[0] for v in val])) + if self.cell_defined_on.dimension == 2: + np.set_printoptions(linewidth=90, precision=4, suppress=True) + for key, val in pt_dict.items(): + print(np.array(key), ":", np.array([v[0] for v in val])) return pt_dict def __repr__(self, fn="v"): diff --git a/fuse/triples.py b/fuse/triples.py index 4d0c66bb..47323d86 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -451,12 +451,12 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): sub_mat = np.kron((~g).matrix_form(), value_change) new_ent_dofs_ids = [int(g_to_ent_id[str(sub_g.perm.array_form)]) for coset in cosets for sub_g in coset] if not np.allclose(new_ent_dofs_ids, ent_dofs_ids): - print("original", sub_mat) + # print("original", sub_mat) # ent_dofs_ids = new_ent_dofs_ids oriented_mats_by_entity[dim][e_id][val][np.ix_(new_ent_dofs_ids, new_ent_dofs_ids)] = sub_mat.copy() else: oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - print("added", oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)]) + # print("added", oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)]) # if len(ent_dofs_ids) == basis_change.shape[0]: # sub_mat = basis_change # elif len(dof_gen_class[dim].g2.members()) == 2 and len(ent_dofs_ids) == 1: diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 896cad17..5b504700 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -336,7 +336,7 @@ def construct_tet_ned2(tet=None, perm=None): return ned -def construct_tet_ned3(tet=None, perm=None): +def construct_tet_ned3(tet=None, both=False): if tet is None: tet = make_tetrahedron() deg = 3 @@ -359,11 +359,24 @@ def construct_tet_ned3(tet=None, perm=None): int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) - phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] - # phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] - xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2))] - face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S3, S3)) - face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) + phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] + xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y), shape=2)), + DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y), shape=2)), + DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y), shape=2))] + # face_vec = DOFGenerator(xs, S1, S1) + face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S1, S1)) + # phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] + # xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2))] + # if both: + # phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] + # xs += [DOF(L2Pairing(), PolynomialKernel(phi1, symbols=(x, y), shape=2))] + # if both: + # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S3)) + # else: + # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S3, S3)) + face_dofs = DOFGenerator([immerse(tet, face_vec, TrHCurl)], tet_faces, S1) xs = [DOF(L2Pairing(), VectorKernel([1, 0, 0])), DOF(L2Pairing(), VectorKernel([0, 1, 0])), @@ -418,10 +431,21 @@ def test_tet_nd(): def test_tet_nd3(): nd3 = construct_tet_ned3() ls = nd3.generate() + # for dof in ls: + # dof_dict = dof.to_quadrature(1) + # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) + # print(dof) + # print("both") + # nd3 = construct_tet_ned3() + # ls1 = nd3.generate() for dof in ls: - # dof_dict = dof.to_quadrature(1) + print("A", dof) + dof.to_quadrature(1) + # print("B", dof1) + # dof1.to_quadrature(1) # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) - print(dof) + # print(dof) + # breakpoint() nd3.to_fiat() From 80304b7d7ebe8d8b5962ea1dee56eb93b6d60ecb Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 1 Apr 2025 14:09:27 +0100 Subject: [PATCH 73/98] add equality to cells --- fuse/__init__.py | 3 ++- fuse/cells.py | 22 +++++++++++++++++++++- test/test_cells.py | 10 ++++++++++ test/test_convert_to_fiat.py | 6 ++++-- test/test_tensor_prod.py | 12 ++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index e7ed0618..9b68648d 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,4 +1,5 @@ -from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex + +from fuse.cells import Point, Edge, polygon, 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, PolynomialKernel, ComponentKernel from fuse.triples import ElementTriple, DOFGenerator, immerse diff --git a/fuse/cells.py b/fuse/cells.py index ab06eb98..340b1035 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -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 @@ -544,6 +551,9 @@ def ordered_vertices(self, get_class=False): return self.oriented.permute(verts) return verts + def ordered_vertex_coords(self): + return [self.get_node(o, return_coords=True) for o in self.ordered_vertices()] + def d_entities_ids(self, d): return self.d_entities(d, get_class=False) @@ -645,7 +655,6 @@ def basis_vectors(self, return_coords=True, entity=None, order=False): 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 @@ -874,6 +883,16 @@ 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 __eq__(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() + + def __hash__(self): + return hash(self.id) + class Edge(): """ @@ -971,6 +990,7 @@ def to_fiat(self, name=None): return CellComplexToFiatTensorProduct(self, name) def flatten(self): + assert self.A == self.B return TensorProductPoint(self.A, self.B, True) diff --git a/test/test_cells.py b/test/test_cells.py index ced8e36d..54b9aa5a 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -179,6 +179,16 @@ def test_comparison(): # print(tensor_product1 >= tensor_product1) +def test_self_equality(C): + assert C == C + + +@pytest.mark.parametrize(["A", "B", "res"], [(firedrake_triangle(), polygon(3), False), + (line(), line(), True),]) +def test_equality(A, B, res): + assert (A == B) == res + + @pytest.mark.parametrize(["cell"], [(ufc_triangle(),), (polygon(3),)]) def test_connectivity(cell): cell = cell.to_fiat() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index bee86500..af195fb0 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -542,8 +542,10 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -def test_non_tensor_quad(): - create_cg1_quad() +# def test_non_tensor_quad(): +# elem = create_cg1_quad() +# ufl_elem = elem.to_ufl() +# assert (run_test(0, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) def project(U, mesh, func): diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index 7c527203..67232963 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -117,3 +117,15 @@ def test_quad_mesh_helmholtz(): conv = np.log2(res[:-1] / res[1:]) print("convergence order:", conv) assert (np.array(conv) > 1.8).all() + + +@pytest.mark.parametrize(["A", "B", "res"], [(Point(0), line(), False), + (line(), line(), True), + (polygon(3), line(), False),]) +def test_flattening(A, B, res): + tensor_cell = TensorProductPoint(A, B) + if not res: + with pytest.raises(AssertionError): + tensor_cell.flatten() + else: + tensor_cell.flatten() From d881c4a89f9bd696726d5f04cea5dd0411470d9f Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 1 Apr 2025 16:16:58 +0100 Subject: [PATCH 74/98] refactor --- fuse/cells.py | 29 ++++++++++++++++++++++------- test/test_convert_to_fiat.py | 8 ++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 340b1035..10d850d5 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -948,11 +948,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() @@ -980,18 +980,33 @@ 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): assert self.A == self.B - return TensorProductPoint(self.A, self.B, True) + return FlattenedPoint(self.A, self.B) + + +class FlattenedPoint(TensorProductPoint): + + def __init__(self, A, B): + self.A = A + self.B = B + self.dimension = self.A.dimension + self.B.dimension + self.flat = True + + 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 + return CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) + + def flatten(self): + return self class CellComplexToFiatSimplex(Simplex): diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index af195fb0..598bb9dd 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -542,10 +542,10 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -# def test_non_tensor_quad(): -# elem = create_cg1_quad() -# ufl_elem = elem.to_ufl() -# assert (run_test(0, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) +def test_non_tensor_quad(): + elem = create_cg1_quad() + ufl_elem = elem.to_ufl() + assert (run_test(0, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) def project(U, mesh, func): From e49c7034544b06c7921b7b235cfc9b75b6896075 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 1 Apr 2025 17:10:00 +0100 Subject: [PATCH 75/98] first steps to making a fuse element from a flattened tensor prod --- fuse/cells.py | 24 ++++++++++++++++++++++-- test/test_tensor_prod.py | 3 ++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 10d850d5..bc43781a 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -964,8 +964,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) @@ -1005,6 +1005,26 @@ def to_fiat(self, name=None): # TODO this should check if it actually is a hypercube return CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) + def construct_fuse_rep(self): + sub_cells = [self.A, self.B] + dims = self.dim() + points = {i: [] for i in range(max(dims))} + + for d in range(max(dims)): + points = [] + attachments = [] + for cell in sub_cells: + if d <= cell.dimension: + points.append(cell.d_entities(d)) + attachments.append(cell.edges()) + for i in itertools.product(*points): + print(i) + print(attachments) + new_attachments = [] + # for a in attachments: + # old_attach = a.attachment + # new_attachments.append() + def flatten(self): return self diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index 67232963..d8b2bba8 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -128,4 +128,5 @@ def test_flattening(A, B, res): with pytest.raises(AssertionError): tensor_cell.flatten() else: - tensor_cell.flatten() + cell = tensor_cell.flatten() + cell.construct_fuse_rep() \ No newline at end of file From 2fb6b88abd5ffff5bee0813b79733f2c6ff53037 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 3 Apr 2025 14:14:08 +0100 Subject: [PATCH 76/98] first level of generated hasse diagram --- fuse/cells.py | 94 ++++++++++++++++++++++++++++------- test/test_cells.py | 120 +-------------------------------------------- 2 files changed, 79 insertions(+), 135 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index bc43781a..c7f5a536 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -509,6 +509,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 @@ -882,16 +891,14 @@ 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 __eq__(self, other): + + 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() - def __hash__(self): - return hash(self.id) class Edge(): @@ -986,7 +993,7 @@ def to_fiat(self, name=None): return CellComplexToFiatTensorProduct(self, name) def flatten(self): - assert self.A == self.B + assert self.A.equivalent(self.B) return FlattenedPoint(self.A, self.B) @@ -1008,22 +1015,75 @@ def to_fiat(self, name=None): def construct_fuse_rep(self): sub_cells = [self.A, self.B] dims = self.dim() - points = {i: [] for i in range(max(dims))} + 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)): - points = [] - attachments = [] + for d in range(max(dims) + 1): for cell in sub_cells: + + # if d <= cell.dimension: + # points[d].extend([cell.get_node(e, return_coords=True) for e in cell.d_entities(d, get_class=False)]) if d <= cell.dimension: - points.append(cell.d_entities(d)) - attachments.append(cell.edges()) - for i in itertools.product(*points): - print(i) + sub_ent = cell.d_entities(d, get_class=True) + + # points[d].extend([cell.get_node(e, return_coords=True) for e in cell.d_entities(d, get_class=False)]) + points[cell][d].extend(sub_ent) + for s in sub_ent: + attachments[cell][d].extend(s.connections) + + new_attachments = [] + for a in attachments[cell][d]: + print(cell) + print(a, " res: ", a.attachment) print(attachments) - new_attachments = [] - # for a in attachments: - # old_attach = a.attachment - # new_attachments.append() + print(points) + + + # old_attach = a.attachment + # new_attachments.append() + prod_points = list(itertools.product(*[points[cell][0] for cell in sub_cells])) + print(len(prod_points)) + 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: [] for e in edges} + print(prod_points) + 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)].append(Point(1, [Edge(point_cls[a_idx], a_edge.attachment, a_edge.o), + Edge(point_cls[b_idx], b_edge.attachment, b_edge.o)])) + print(edge_cls1) + # hasse level 2 + # for i in range(len(sub_cells)): + # for (a, b) in edges: + # a_idx = prod_points.index(a) + # b_idx = prod_points.index(b) + # print(a, a_idx) + # print(b, b_idx) + # # breakpoint() + # 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.append(Point(1, [Edge(point_cls[a_idx], a_edge.attachment, a_edge.o), + # Edge(point_cls[b_idx], b_edge.attachment, b_edge.o)])) + # print(attachments) + # for i in itertools.product(attachments, repeat=2): + # print(i) def flatten(self): return self diff --git a/test/test_cells.py b/test/test_cells.py index 54b9aa5a..cac57755 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -185,121 +185,5 @@ def test_self_equality(C): @pytest.mark.parametrize(["A", "B", "res"], [(firedrake_triangle(), polygon(3), False), (line(), line(), True),]) -def test_equality(A, B, res): - assert (A == B) == res - - -@pytest.mark.parametrize(["cell"], [(ufc_triangle(),), (polygon(3),)]) -def test_connectivity(cell): - cell = cell.to_fiat() - for dim0 in range(cell.get_spatial_dimension()+1): - connectivity = cell.get_connectivity()[(dim0, 0)] - topology = cell.get_topology()[dim0] - assert len(connectivity) == len(topology) - - assert all(connectivity[i] == t for i, t in topology.items()) - - -def test_tensor_connectivity(): - from test_2d_examples_docs import construct_cg1 - A = construct_cg1() - B = construct_cg1() - cell = tensor_product(A, B).cell - cell = cell.to_fiat() - for dim0 in [(0, 0), (1, 0), (0, 1), (1, 1)]: - connectivity = cell.get_connectivity()[(dim0, (0, 0))] - topology = cell.get_topology()[dim0] - assert len(connectivity) == len(topology) - - assert all(connectivity[i] == t for i, t in topology.items()) - - -@pytest.mark.parametrize(["cell"], [(ufc_triangle(),), (polygon(3),), (make_tetrahedron(), ), (make_tetrahedron(), )]) -def test_new_connectivity(cell): - cell = cell.to_fiat() - for dim0 in range(cell.get_dimension() + 1): - connectivity = cell.get_connectivity()[(dim0, 0)] - topology = cell.get_topology()[dim0] - assert len(connectivity) == len(topology) - for i, t in topology.items(): - print(connectivity[i]) - print(t) - assert all(connectivity[i] == t for i, t in topology.items()) - - -def test_compare_tris(): - fuse_tet = polygon(3) - ufc_tet = ufc_triangle() - fiat_tet = ufc_simplex(2) - - print(fiat_tet.get_topology()) - print(fuse_tet.get_topology()) - print(ufc_tet.get_topology()) - fiat_connectivity = fiat_tet.get_connectivity() - fuse_connectivity = fuse_tet.to_fiat().get_connectivity() - ufc_connectivity = ufc_tet.to_fiat().get_connectivity() - _dim = fiat_tet.get_dimension() - print("fiat") - print(make_entity_cone_lists(fiat_tet)) - for dim0 in range(_dim): - connectivity = fiat_connectivity[(dim0+1, dim0)] - print(connectivity) - print("fuse") - print(make_entity_cone_lists(fuse_tet.to_fiat())) - for dim0 in range(_dim): - connectivity = fuse_connectivity[(dim0+1, dim0)] - print(connectivity) - print("fuse ufc") - print(make_entity_cone_lists(ufc_tet.to_fiat())) - for dim0 in range(_dim): - connectivity = ufc_connectivity[(dim0+1, dim0)] - print(connectivity) - - -def test_compare_tets(): - tet = make_tetrahedron() - # perm = tet.group.get_member([1, 2, 0, 3]) - fuse_tet = tet - ufc_tet = ufc_tetrahedron() - fiat_tet = ufc_simplex(3) - # breakpoint() - print(fiat_tet.get_topology()) - print(fuse_tet.get_topology()) - print(ufc_tet.get_topology()) - fiat_connectivity = fiat_tet.get_connectivity() - fuse_connectivity = fuse_tet.to_fiat().get_connectivity() - ufc_connectivity = ufc_tet.to_fiat().get_connectivity() - _dim = fiat_tet.get_dimension() - print("fiat") - print(make_entity_cone_lists(fiat_tet)) - for dim0 in range(_dim): - connectivity = fiat_connectivity[(dim0+1, dim0)] - print(connectivity) - print("fuse") - print(make_entity_cone_lists(fuse_tet.to_fiat())) - for dim0 in range(_dim): - connectivity = fuse_connectivity[(dim0+1, dim0)] - print(connectivity) - print("fuse ufc") - print(make_entity_cone_lists(ufc_tet.to_fiat())) - for dim0 in range(_dim): - connectivity = ufc_connectivity[(dim0+1, dim0)] - print(connectivity) - - -def make_entity_cone_lists(fiat_cell): - _dim = fiat_cell.get_dimension() - _connectivity = fiat_cell.connectivity - _list = [] - _offset_list = [0 for _ in _connectivity[(0, 0)]] # vertices have no cones - _offset = 0 - _n = 0 # num. of entities up to dimension = _d - for _d in range(_dim): - _n1 = len(_offset_list) - for _conn in _connectivity[(_d + 1, _d)]: - _list += [_c + _n for _c in _conn] # These are indices into cell_closure[some_cell] - _offset_list.append(_offset) - _offset += len(_conn) - _n = _n1 - _offset_list.append(_offset) - return _list, _offset_list +def test_equivalence(A, B, res): + assert A.equivalent(B) == res From e6341b9e7a0996dd493233471bda2991d8ab92ba Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 7 Apr 2025 12:25:50 +0100 Subject: [PATCH 77/98] fix recursion issue --- fuse/cells.py | 56 +++++++++++++++++++----------------- test/test_convert_to_fiat.py | 3 +- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index c7f5a536..732b41ce 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -380,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()] @@ -923,7 +924,11 @@ 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.append(attach_comp.subs({syms[i]: x[i] for i in range(len(x))})) + return tuple(res) return sympy_to_numpy(self.attachment, syms, x) return x @@ -997,13 +1002,15 @@ def flatten(self): return FlattenedPoint(self.A, self.B) -class FlattenedPoint(TensorProductPoint): +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") @@ -1014,7 +1021,8 @@ def to_fiat(self, name=None): def construct_fuse_rep(self): sub_cells = [self.A, self.B] - dims = self.dim() + 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} @@ -1038,7 +1046,6 @@ def construct_fuse_rep(self): print(attachments) print(points) - # old_attach = a.attachment # new_attachments.append() prod_points = list(itertools.product(*[points[cell][0] for cell in sub_cells])) @@ -1056,7 +1063,7 @@ def construct_fuse_rep(self): edges.append((a, b)) # hasse level 1 - edge_cls1 = {e: [] for e in edges} + edge_cls1 = {e: None for e in edges} print(prod_points) for i in range(len(sub_cells)): for (a, b) in edges: @@ -1065,25 +1072,23 @@ def construct_fuse_rep(self): 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)].append(Point(1, [Edge(point_cls[a_idx], a_edge.attachment, a_edge.o), - Edge(point_cls[b_idx], b_edge.attachment, b_edge.o)])) - print(edge_cls1) + 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: - # a_idx = prod_points.index(a) - # b_idx = prod_points.index(b) - # print(a, a_idx) - # print(b, b_idx) - # # breakpoint() - # 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.append(Point(1, [Edge(point_cls[a_idx], a_edge.attachment, a_edge.o), - # Edge(point_cls[b_idx], b_edge.attachment, b_edge.o)])) - # print(attachments) - # for i in itertools.product(attachments, repeat=2): - # print(i) + 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,) + print(edge_cls1[(a, b)].ordered_vertices()) + edge_cls2.append(Edge(edge_cls1[(a, b)], attach, a_edge.o)) + print(edge_cls2) + return edge_cls2 def flatten(self): return self @@ -1276,9 +1281,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) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 598bb9dd..3caf5ee2 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -117,6 +117,7 @@ def create_cg1_quad(): xs = [immerse(cell, vert_dg, TrH1)] Pk = PolynomialSpace(deg, deg + 1) + breakpoint() cg = ElementTriple(cell, (Pk, CellL2, C0), DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1)) return cg @@ -545,7 +546,7 @@ def test_quad(elem_gen): def test_non_tensor_quad(): elem = create_cg1_quad() ufl_elem = elem.to_ufl() - assert (run_test(0, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) + assert (run_test(1, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) def project(U, mesh, func): From 208d64a77fb98cc47db3969d1be571f17bee9c39 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 7 Apr 2025 12:27:17 +0100 Subject: [PATCH 78/98] lint --- fuse/cells.py | 15 ++++++--------- test/test_tensor_prod.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index 732b41ce..e0bed57b 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -380,7 +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()] @@ -892,7 +892,7 @@ 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 @@ -901,7 +901,6 @@ def equivalent(self, other): return self.get_topology() == other.get_topology() - class Edge(): """ Representation of the connections in a cell complex. @@ -928,7 +927,6 @@ def __call__(self, *x): res.append(sympy_to_numpy(attach_comp, syms, x)) else: res.append(attach_comp.subs({syms[i]: x[i] for i in range(len(x))})) - return tuple(res) return sympy_to_numpy(self.attachment, syms, x) return x @@ -1039,7 +1037,6 @@ def construct_fuse_rep(self): for s in sub_ent: attachments[cell][d].extend(s.connections) - new_attachments = [] for a in attachments[cell][d]: print(cell) print(a, " res: ", a.attachment) @@ -1059,9 +1056,9 @@ def construct_fuse_rep(self): # 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))]): + 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} print(prod_points) @@ -1072,8 +1069,8 @@ def construct_fuse_rep(self): 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_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)): diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index d8b2bba8..8fa82f27 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -129,4 +129,4 @@ def test_flattening(A, B, res): tensor_cell.flatten() else: cell = tensor_cell.flatten() - cell.construct_fuse_rep() \ No newline at end of file + cell.construct_fuse_rep() From 3f06f030f3ed9509d2c826e87a0ae539fb46fd22 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 7 Apr 2025 17:59:55 +0100 Subject: [PATCH 79/98] refactor work on poly spaces and trying to figure out how to make quad spaces --- fuse/spaces/polynomial_spaces.py | 57 ++++++++++++++++++++++++-------- fuse/utils.py | 21 +++++++----- test/test_convert_to_fiat.py | 20 +++++++++-- test/test_polynomial_space.py | 12 +++++++ 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 422cf053..0b74783b 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -3,7 +3,7 @@ from FIAT.reference_element import cell_to_simplex from FIAT import expansions, polynomial_set, reference_element from itertools import chain -from fuse.utils import tabulate_sympy, max_deg_sp_mat +from fuse.utils import tabulate_sympy, max_deg_sp_expr import sympy as sp import numpy as np from functools import total_ordering @@ -56,9 +56,27 @@ def to_ON_polynomial_set(self, ref_el, k=None): shape = (sd,) else: shape = tuple() + base_ON = ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") + + # if self.contains: + # expansion_set = expansions.ExpansionSet(ref_el) + # num_exp_functions = expansion_set.get_num_members(self.maxdegree) + # dimPmin = expansions.polynomial_dimension(ref_el, self.mindegree) + # dimPmax = expansions.polynomial_dimension(ref_el, self.maxdegree) + # indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) + # from math import comb + # print(list(chain(*[range(0, 0), range(2, 3)]))) + # base_ON.expansion_set._tabulate_on_cell(3, np.array([[0,0]])) + # if self.set_shape: + # indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) + # else: + # indices = list(range(dimPmin, dimPmax)) + # restricted_ON = base_ON.take(indices) + # return restricted_ON + + # breakpoint() if self.mindegree > 0: - base_ON = ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") dimPmin = expansions.polynomial_dimension(ref_el, self.mindegree) dimPmax = expansions.polynomial_dimension(ref_el, self.maxdegree) if self.set_shape: @@ -67,7 +85,8 @@ def to_ON_polynomial_set(self, ref_el, k=None): indices = list(range(dimPmin, dimPmax)) restricted_ON = base_ON.take(indices) return restricted_ON - return ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") + + return base_ON def __repr__(self): res = "" @@ -163,37 +182,49 @@ def to_ON_polynomial_set(self, ref_el): if not isinstance(ref_el, reference_element.Cell): ref_el = ref_el.to_fiat() k = max([s.maxdegree for s in self.spaces]) - space_poly_sets = [s.to_ON_polynomial_set(ref_el) for s in self.spaces] sd = ref_el.get_spatial_dimension() ref_el = cell_to_simplex(ref_el) - if all([w == 1 for w in self.weights]): - weighted_sets = space_poly_sets - # otherwise have to work on this through tabulation Q = create_quadrature(ref_el, 2 * (k + 1)) Qpts, Qwts = Q.get_points(), Q.get_weights() weighted_sets = [] - for (space, w) in zip(space_poly_sets, self.weights): + for (s, w) in zip(self.spaces, self.weights): + space = s.to_ON_polynomial_set(ref_el) + if s.set_shape: + shape = (sd,) + else: + shape = tuple() if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)): weighted_sets.append(space) else: - w_deg = max_deg_sp_mat(w) - Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, scale="orthonormal") - vec_Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, (sd,), scale="orthonormal") + if isinstance(w, sp.Expr): + w = sp.Matrix([[w]]) + vec = False + else: + vec = True + w_deg = max_deg_sp_expr(w) + Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, shape, scale="orthonormal") + # vec_Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, (sd,), scale="orthonormal") space_at_Qpts = space.tabulate(Qpts)[(0,) * sd] Pkpw_at_Qpts = Pkpw.tabulate(Qpts)[(0,) * sd] tabulated_expr = tabulate_sympy(w, Qpts).T - scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + if s.set_shape or vec: + scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + else: + # breakpoint() + scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] + scaled_at_Qpts = scaled_at_Qpts.squeeze() PkHw_coeffs = np.dot(np.multiply(scaled_at_Qpts, Qwts), Pkpw_at_Qpts.T) + # breakpoint() weighted_sets.append(polynomial_set.PolynomialSet(ref_el, space.degree + w_deg, space.degree + w_deg, - vec_Pkpw.get_expansion_set(), + Pkpw.get_expansion_set(), PkHw_coeffs)) combined_sets = weighted_sets[0] for i in range(1, len(weighted_sets)): diff --git a/fuse/utils.py b/fuse/utils.py index fcfa3d9e..3a91c6ba 100644 --- a/fuse/utils.py +++ b/fuse/utils.py @@ -47,7 +47,7 @@ def tabulate_sympy(expr, pts): # expr: sp matrix expression in x,y,z for components of R^d # pts: n values in R^d # returns: evaluation of expr at pts - res = np.array(pts) + res = np.zeros((pts.shape[0],) + (expr.shape[-1],)) i = 0 syms = ["x", "y", "z"] for pt in pts: @@ -57,16 +57,21 @@ def tabulate_sympy(expr, pts): subbed = np.array(subbed).astype(np.float64) res[i] = subbed[0] i += 1 - final = res.squeeze() - return final + # final = res.squeeze() + return res -def max_deg_sp_mat(sp_mat): +def max_deg_sp_expr(sp_expr): degs = [] - for comp in sp_mat: - # only compute degree if component is a polynomial - if sp.sympify(comp).as_poly(): - degs += [sp.sympify(comp).as_poly().degree()] + if isinstance(sp_expr, sp.Matrix): + for comp in sp_expr: + # only compute degree if component is a polynomial + if sp.sympify(comp).as_poly(): + degs += [sp.sympify(comp).as_poly().degree()] + else: + if sp.sympify(sp_expr).as_poly(): + degs += [sp.sympify(sp_expr).as_poly().degree()] + return max(degs) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 3caf5ee2..cdf0dd5d 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -115,9 +115,9 @@ def create_cg1_quad(): vert_dg = create_dg0(cell.vertices()[0]) xs = [immerse(cell, vert_dg, TrH1)] + x = sp.Symbol("y") - Pk = PolynomialSpace(deg, deg + 1) - breakpoint() + Pk = PolynomialSpace(deg) + P2.restrict(1, 1)*x cg = ElementTriple(cell, (Pk, CellL2, C0), DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1)) return cg @@ -535,7 +535,7 @@ def test_poisson_analytic(params, elem_gen): @pytest.mark.parametrize(['elem_gen'], - [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='Need to allow generation on tensor product quads'))]) + [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='How to make quad poly set correctly'))]) def test_quad(elem_gen): elem = elem_gen() r = 0 @@ -563,6 +563,20 @@ def project(U, mesh, func): return res +def project(U, mesh, func): + f = assemble(interpolate(func, U)) + + out = Function(U) + u = TrialFunction(U) + v = TestFunction(U) + a = inner(u, v)*dx + L = inner(f, v)*dx + solve(a == L, out) + + res = sqrt(assemble(dot(out - func, out - func) * dx)) + return res + + @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), (create_cg1, "CG", 1), (create_dg1, "DG", 1), diff --git a/test/test_polynomial_space.py b/test/test_polynomial_space.py index e7dad66d..d22ebd54 100644 --- a/test/test_polynomial_space.py +++ b/test/test_polynomial_space.py @@ -41,6 +41,7 @@ def test_restriction(): res_on_set = restricted.to_ON_polynomial_set(cell) P3_on_set = P3.to_ON_polynomial_set(cell) + assert res_on_set.get_num_members() < P3_on_set.get_num_members() not_restricted = P3.restrict(0, 3) @@ -48,6 +49,17 @@ def test_restriction(): assert not_restricted.mindegree == 0 +@pytest.mark.xfail(reason="Quad space WIP") +def test_square_space(): + cell = polygon(3) + q2 = PolynomialSpace(3, 1) + + q2_on_set = q2.to_ON_polynomial_set(cell) + P3_on_set = P3.to_ON_polynomial_set(cell) + + assert q2_on_set.get_num_members() < P3_on_set.get_num_members() + + @pytest.mark.parametrize("deg", [1, 2, 3, 4]) def test_complete_space(deg): cell = polygon(3) From 01ac541c93c68278c4a18abb0e8bd114db04f94a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 10 Apr 2025 12:19:33 +0100 Subject: [PATCH 80/98] fix contains in polynomial sets --- fuse/spaces/polynomial_spaces.py | 34 +++++++++++--------------------- test/test_convert_to_fiat.py | 34 ++------------------------------ test/test_polynomial_space.py | 1 - 3 files changed, 14 insertions(+), 55 deletions(-) diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 0b74783b..3d4618fd 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -1,4 +1,5 @@ from FIAT.polynomial_set import ONPolynomialSet +from FIAT.expansions import morton_index2, morton_index3 from FIAT.quadrature_schemes import create_quadrature from FIAT.reference_element import cell_to_simplex from FIAT import expansions, polynomial_set, reference_element @@ -8,6 +9,8 @@ import numpy as np from functools import total_ordering +morton_index = {2: morton_index2, 3: morton_index3} + @total_ordering class PolynomialSpace(object): @@ -47,7 +50,6 @@ def degree(self): return self.maxdegree def to_ON_polynomial_set(self, ref_el, k=None): - # how does super/sub degrees work here if not isinstance(ref_el, reference_element.Cell): ref_el = ref_el.to_fiat() sd = ref_el.get_spatial_dimension() @@ -57,24 +59,7 @@ def to_ON_polynomial_set(self, ref_el, k=None): else: shape = tuple() base_ON = ONPolynomialSet(ref_el, self.maxdegree, shape, scale="orthonormal") - - # if self.contains: - # expansion_set = expansions.ExpansionSet(ref_el) - # num_exp_functions = expansion_set.get_num_members(self.maxdegree) - # dimPmin = expansions.polynomial_dimension(ref_el, self.mindegree) - # dimPmax = expansions.polynomial_dimension(ref_el, self.maxdegree) - # indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) - # from math import comb - # print(list(chain(*[range(0, 0), range(2, 3)]))) - # base_ON.expansion_set._tabulate_on_cell(3, np.array([[0,0]])) - # if self.set_shape: - # indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) - # else: - # indices = list(range(dimPmin, dimPmax)) - # restricted_ON = base_ON.take(indices) - # return restricted_ON - - # breakpoint() + indices = None if self.mindegree > 0: dimPmin = expansions.polynomial_dimension(ref_el, self.mindegree) @@ -83,10 +68,15 @@ def to_ON_polynomial_set(self, ref_el, k=None): indices = list(chain(*(range(i * dimPmin, i * dimPmax) for i in range(sd)))) else: indices = list(range(dimPmin, dimPmax)) - restricted_ON = base_ON.take(indices) - return restricted_ON - return base_ON + if self.contains != self.maxdegree and self.contains != -1: + indices = [morton_index[sd](p, q) for p in range(self.contains + 1) for q in range(self.contains + 1)] + + if indices is None: + return base_ON + + restricted_ON = base_ON.take(indices) + return restricted_ON def __repr__(self): res = "" diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index cdf0dd5d..d0a5f641 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -1,6 +1,5 @@ import pytest import numpy as np -import sympy as sp from fuse import * from firedrake import * from sympy.combinatorics import Permutation @@ -115,9 +114,7 @@ def create_cg1_quad(): vert_dg = create_dg0(cell.vertices()[0]) xs = [immerse(cell, vert_dg, TrH1)] - x = sp.Symbol("y") - - Pk = PolynomialSpace(deg) + P2.restrict(1, 1)*x + Pk = PolynomialSpace(deg + 1, deg) cg = ElementTriple(cell, (Pk, CellL2, C0), DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1)) return cg @@ -543,40 +540,13 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) +@pytest.mark.xfail(reason="Issue with quad cell") def test_non_tensor_quad(): elem = create_cg1_quad() ufl_elem = elem.to_ufl() assert (run_test(1, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -def project(U, mesh, func): - f = assemble(interpolate(func, U)) - - out = Function(U) - u = TrialFunction(U) - v = TestFunction(U) - a = inner(u, v)*dx - L = inner(f, v)*dx - solve(a == L, out) - - res = sqrt(assemble(dot(out - func, out - func) * dx)) - return res - - -def project(U, mesh, func): - f = assemble(interpolate(func, U)) - - out = Function(U) - u = TrialFunction(U) - v = TestFunction(U) - a = inner(u, v)*dx - L = inner(f, v)*dx - solve(a == L, out) - - res = sqrt(assemble(dot(out - func, out - func) * dx)) - return res - - @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), (create_cg1, "CG", 1), (create_dg1, "DG", 1), diff --git a/test/test_polynomial_space.py b/test/test_polynomial_space.py index d22ebd54..baf81206 100644 --- a/test/test_polynomial_space.py +++ b/test/test_polynomial_space.py @@ -49,7 +49,6 @@ def test_restriction(): assert not_restricted.mindegree == 0 -@pytest.mark.xfail(reason="Quad space WIP") def test_square_space(): cell = polygon(3) q2 = PolynomialSpace(3, 1) From 78e0861ddcf8b4588816e5609b23c70f04ed7ae7 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 14 Apr 2025 17:20:58 +0100 Subject: [PATCH 81/98] working on cell --- fuse/cells.py | 24 +++++------------------- test/test_convert_to_fiat.py | 11 +++++------ 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index e0bed57b..5b4c436b 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -1015,7 +1015,8 @@ def to_ufl(self, name=None): def to_fiat(self, name=None): # TODO this should check if it actually is a hypercube - return CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) + fiat = CellComplexToFiatHypercube(self, CellComplexToFiatTensorProduct(self, name)) + return fiat def construct_fuse_rep(self): sub_cells = [self.A, self.B] @@ -1026,27 +1027,16 @@ def construct_fuse_rep(self): for d in range(max(dims) + 1): for cell in sub_cells: - - # if d <= cell.dimension: - # points[d].extend([cell.get_node(e, return_coords=True) for e in cell.d_entities(d, get_class=False)]) if d <= cell.dimension: sub_ent = cell.d_entities(d, get_class=True) - - # points[d].extend([cell.get_node(e, return_coords=True) for e in cell.d_entities(d, get_class=False)]) points[cell][d].extend(sub_ent) for s in sub_ent: attachments[cell][d].extend(s.connections) - for a in attachments[cell][d]: - print(cell) - print(a, " res: ", a.attachment) - print(attachments) - print(points) - - # old_attach = a.attachment - # new_attachments.append() prod_points = list(itertools.product(*[points[cell][0] for cell in sub_cells])) - print(len(prod_points)) + # 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 = [] @@ -1058,10 +1048,8 @@ def construct_fuse_rep(self): # 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} - print(prod_points) for i in range(len(sub_cells)): for (a, b) in edges: a_idx = prod_points.index(a) @@ -1082,9 +1070,7 @@ def construct_fuse_rep(self): attach = (x,) + a_edge.attachment else: attach = a_edge.attachment + (x,) - print(edge_cls1[(a, b)].ordered_vertices()) edge_cls2.append(Edge(edge_cls1[(a, b)], attach, a_edge.o)) - print(edge_cls2) return edge_cls2 def flatten(self): diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index d0a5f641..07316d47 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -109,10 +109,9 @@ def create_cg1(cell): def create_cg1_quad(): deg = 1 - cell = polygon(4) - # cell = constructCellComplex("quadrilateral").cell_complex - - vert_dg = create_dg0(cell.vertices()[0]) + cell = TensorProductPoint(line(), line()).flatten() + print(cell, type(cell)) + vert_dg = create_dg1(cell.vertices()[0]) xs = [immerse(cell, vert_dg, TrH1)] Pk = PolynomialSpace(deg + 1, deg) cg = ElementTriple(cell, (Pk, CellL2, C0), DOFGenerator(xs, get_cyc_group(len(cell.vertices())), S1)) @@ -532,7 +531,7 @@ def test_poisson_analytic(params, elem_gen): @pytest.mark.parametrize(['elem_gen'], - [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='How to make quad poly set correctly'))]) + [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='Issue with cell/mesh'))]) def test_quad(elem_gen): elem = elem_gen() r = 0 @@ -540,7 +539,7 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -@pytest.mark.xfail(reason="Issue with quad cell") +# @pytest.mark.xfail(reason="Issue with quad cell") def test_non_tensor_quad(): elem = create_cg1_quad() ufl_elem = elem.to_ufl() From e9560f9c4d227516526568e26255569b17443291 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 23 Apr 2025 12:34:51 +0100 Subject: [PATCH 82/98] Ensure all numeric reps are 0..n over the whole group --- fuse/groups.py | 18 +++++++++++++++++- test/test_convert_to_fiat.py | 13 ++++++++----- test/test_groups.py | 7 +++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/fuse/groups.py b/fuse/groups.py index e63a9d32..1ec5275c 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -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) @@ -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) @@ -243,6 +254,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() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 07316d47..efca5daa 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -500,6 +500,7 @@ def helmholtz_solve(V, mesh): def poisson_solve(r, elem, parameters={}, quadrilateral=False): # Create mesh and define function space m = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=quadrilateral) + x = SpatialCoordinate(m) V = FunctionSpace(m, elem) @@ -539,11 +540,13 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -# @pytest.mark.xfail(reason="Issue with quad cell") -def test_non_tensor_quad(): - elem = create_cg1_quad() - ufl_elem = elem.to_ufl() - assert (run_test(1, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) +# # @pytest.mark.xfail(reason="Issue with quad cell") +# def test_non_tensor_quad(): +# elem = create_cg1_quad() +# ufl_elem = elem.to_ufl() +# print(elem.to_fiat().entity_permutations()) +# # elem.cell.hasse_diagram(filename="cg1quad.png") +# assert (run_test(1, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), diff --git a/test/test_groups.py b/test/test_groups.py index 1b26787e..0c207039 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -117,3 +117,10 @@ def test_perm_mat_conversion(): mat_form = g.matrix_form() array_form = perm_matrix_to_perm_array(mat_form) assert np.allclose(g.perm.array_form, array_form) + + +def test_numeric_reps(): + cell = polygon(4) + rot4 = get_cyc_group(4).add_cell(cell) + + assert sorted([m.numeric_rep() for m in rot4.members()]) == list(range(len(rot4.members()))) From 37b6124c629399abb9eca29b4291340527e3ff76 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 24 Apr 2025 09:56:52 +0100 Subject: [PATCH 83/98] comparision to existing --- test/test_convert_to_fiat.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index efca5daa..7eb7a2e3 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -521,6 +521,29 @@ def poisson_solve(r, elem, parameters={}, quadrilateral=False): return sqrt(assemble(inner(u - f, u - f) * dx)) +def run_test_original(r, elem_code, deg, parameters={}, quadrilateral=False): + # Create mesh and define function space + m = UnitSquareMesh(2 ** r, 2 ** r, quadrilateral=quadrilateral) + + x = SpatialCoordinate(m) + V = FunctionSpace(m, elem_code, deg) + # Define variational problem + u = Function(V) + v = TestFunction(V) + a = inner(grad(u), grad(v)) * dx + + bcs = [DirichletBC(V, Constant(0), 3), + DirichletBC(V, Constant(42), 4)] + + # Compute solution + solve(a == 0, u, solver_parameters=parameters, bcs=bcs) + + f = Function(V) + f.interpolate(42*x[1]) + + return sqrt(assemble(inner(u - f, u - f) * dx)) + + @pytest.mark.parametrize(['params', 'elem_gen'], [(p, d) for p in [{}, {'snes_type': 'ksponly', 'ksp_type': 'preonly', 'pc_type': 'lu'}] @@ -546,7 +569,7 @@ def test_quad(elem_gen): # ufl_elem = elem.to_ufl() # print(elem.to_fiat().entity_permutations()) # # elem.cell.hasse_diagram(filename="cg1quad.png") -# assert (run_test(1, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) +# assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), From d2d2d4bb18960795ea1b1b1bfc77b743f53489d4 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 10 Mar 2026 18:06:49 +0000 Subject: [PATCH 84/98] tidy up --- fuse/__init__.py | 2 +- fuse/cells.py | 6 ++++-- fuse/spaces/polynomial_spaces.py | 2 +- fuse/utils.py | 2 +- test/test_convert_to_fiat.py | 28 +++++++++++++++++++++------- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index 9b68648d..0db1c3d7 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,5 +1,5 @@ -from fuse.cells import Point, Edge, polygon, make_tetrahedron, constructCellComplex, TensorProductPoint +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, PolynomialKernel, ComponentKernel from fuse.triples import ElementTriple, DOFGenerator, immerse diff --git a/fuse/cells.py b/fuse/cells.py index 5b4c436b..b7b25833 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -926,7 +926,9 @@ def __call__(self, *x): if len(attach_comp.atoms(sp.Symbol)) <= len(x): res.append(sympy_to_numpy(attach_comp, syms, x)) else: - res.append(attach_comp.subs({syms[i]: x[i] for i in range(len(x))})) + 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 @@ -1046,7 +1048,7 @@ def construct_fuse_rep(self): # 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))]): + 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} diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 3d4618fd..3502a6ea 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -157,7 +157,7 @@ def __init__(self, weights, spaces): self.weights = weights self.spaces = spaces - weight_degrees = [0 if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)) else max_deg_sp_mat(w) for w in self.weights] + weight_degrees = [0 if not (isinstance(w, sp.Expr) or isinstance(w, sp.Matrix)) else max_deg_sp_expr(w) for w in self.weights] maxdegree = max([space.maxdegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) mindegree = min([space.mindegree + w_deg for space, w_deg in zip(spaces, weight_degrees)]) diff --git a/fuse/utils.py b/fuse/utils.py index 3a91c6ba..6592e117 100644 --- a/fuse/utils.py +++ b/fuse/utils.py @@ -29,7 +29,7 @@ def sympy_to_numpy(array, symbols, values): """ substituted = array.subs({symbols[i]: values[i] for i in range(len(values))}) - if len(array.atoms(sp.Symbol)) == len(values) and all(not isinstance(v, sp.Expr) for v in values): + if len(array.atoms(sp.Symbol)) <= len(values) and all(not isinstance(v, sp.Expr) for v in values): nparray = np.array(substituted).astype(np.float64) if len(nparray.shape) > 1: diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 7eb7a2e3..2cd8885d 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -563,13 +563,27 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -# # @pytest.mark.xfail(reason="Issue with quad cell") -# def test_non_tensor_quad(): -# elem = create_cg1_quad() -# ufl_elem = elem.to_ufl() -# print(elem.to_fiat().entity_permutations()) -# # elem.cell.hasse_diagram(filename="cg1quad.png") -# assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) +@pytest.mark.xfail(reason="Issue with quad cell") +def test_non_tensor_quad(): + elem = create_cg1_quad() + ufl_elem = elem.to_ufl() + print(elem.to_fiat().entity_permutations()) + # elem.cell.hasse_diagram(filename="cg1quad.png") + assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) + + +def project(U, mesh, func): + f = assemble(interpolate(func, U)) + + out = Function(U) + u = TrialFunction(U) + v = TestFunction(U) + a = inner(u, v)*dx + L = inner(f, v)*dx + solve(a == L, out) + + res = sqrt(assemble(dot(out - func, out - func) * dx)) + return res @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg2_tri, "CG", 2), From 06a50d0f489267085c81c941480292a121f58f7a Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 10 Mar 2026 18:08:11 +0000 Subject: [PATCH 85/98] lint --- test/test_cells.py | 4 ++-- test/test_convert_to_fiat.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_cells.py b/test/test_cells.py index cac57755..0fdbe0e1 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -1,9 +1,9 @@ from fuse import * from firedrake import * -from fuse.cells import ufc_triangle, ufc_tetrahedron +from fuse.cells import ufc_triangle import pytest import numpy as np -from FIAT.reference_element import default_simplex, ufc_simplex +from FIAT.reference_element import default_simplex from test_convert_to_fiat import helmholtz_solve diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 2cd8885d..f0004b32 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -566,7 +566,7 @@ def test_quad(elem_gen): @pytest.mark.xfail(reason="Issue with quad cell") def test_non_tensor_quad(): elem = create_cg1_quad() - ufl_elem = elem.to_ufl() + # ufl_elem = elem.to_ufl() print(elem.to_fiat().entity_permutations()) # elem.cell.hasse_diagram(filename="cg1quad.png") assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) From 52fa438d12c2106e21ab024fb8a3661a08204e19 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 10 Mar 2026 18:58:38 +0000 Subject: [PATCH 86/98] small fixes --- test/test_cells.py | 2 +- test/test_convert_to_fiat.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_cells.py b/test/test_cells.py index 0fdbe0e1..c4da9859 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -183,7 +183,7 @@ def test_self_equality(C): assert C == C -@pytest.mark.parametrize(["A", "B", "res"], [(firedrake_triangle(), polygon(3), False), +@pytest.mark.parametrize(["A", "B", "res"], [(ufc_triangle(), polygon(3), False), (line(), line(), True),]) def test_equivalence(A, B, res): assert A.equivalent(B) == res diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index f0004b32..6213fd1d 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -1,5 +1,6 @@ import pytest import numpy as np +import sympy as sp from fuse import * from firedrake import * from sympy.combinatorics import Permutation From 0e197b45e7f987044467a0c32e25c71e71aa81f5 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 10:36:48 +0000 Subject: [PATCH 87/98] modifications to polynomial kernel to take into account value shape --- fuse/dof.py | 37 +++++++++++++++++++++-------------- test/test_2d_examples_docs.py | 32 +++++++++++++++++++++++++++--- test/test_convert_to_fiat.py | 4 +++- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 7c07f1c9..c576ee48 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -152,7 +152,7 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): return np.array([self.pt for _ in Qpts]).astype(np.float64), np.ones_like(Qwts), [[tuple()] for pt in Qpts] def _to_dict(self): @@ -189,12 +189,13 @@ def permute(self, g): def __call__(self, *args): return self.pt - def evaluate(self, Qpts, Qwts, basis_change, immersed, dim): + def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): + comps = [(i,) for v in value_shape for i in range(v)] 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} @@ -209,7 +210,11 @@ def _from_dict(obj_dict): class PolynomialKernel(BaseKernel): - def __init__(self, fn, g=None, symbols=[], shape=0): + def __init__(self, fn, g=None, symbols=[]): + if hasattr(fn, "__iter__"): + shape = len(fn) + else: + shape = 0 if len(symbols) != 0 and (shape != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape))) and not sp.sympify(fn).as_poly(): raise ValueError("Function argument or its components must be able to be interpreted as a sympy polynomial") if shape != 0: @@ -235,19 +240,21 @@ def degree(self, interpolant_degree): def permute(self, g): # new_fn = self.fn.subs({self.syms[i]: g(self.syms)[i] for i in range(len(self.syms))}) new_fn = self.fn - return PolynomialKernel(new_fn, g=g, symbols=self.syms, shape=self.shape) + return PolynomialKernel(new_fn, g=g, symbols=self.syms) 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): - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + 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] + return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps def _to_dict(self): o_dict = {"fn": self.fn} @@ -343,9 +350,9 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): # TODO deriv dict needs implementing (currently {}) - return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree), {}, str(self)) + return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree, value_shape), {}, str(self)) - def to_quadrature(self, arg_degree): + def to_quadrature(self, arg_degree, value_shape): Qpts, Qwts = self.cell_defined_on.quadrature(self.kernel.degree(arg_degree)) Qwts = Qwts.reshape(Qwts.shape + (1,)) dim = self.cell_defined_on.get_spatial_dimension() @@ -363,7 +370,7 @@ def immersed(pt): return np.matmul(basis_coeffs, immersed_basis) else: immersed = self.immersed - pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension) + 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. diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index a7117416..06da1eea 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -30,6 +30,32 @@ def construct_dg1(): return dg1 +def construct_dg0_integral(cell=None): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + xs = [DOF(L2Pairing(), PolynomialKernel(1))] + dg0 = ElementTriple(edge, (P0, CellL2, C0), DOFGenerator(xs, S1, S1)) + return dg0 + + +def construct_dg1_integral(cell=None): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + x = sp.Symbol("x") + xs = [DOF(L2Pairing(), PolynomialKernel((1/2)*(x + 1), symbols=(x,)))] + dg1 = ElementTriple(edge, (P1, CellL2, C0), DOFGenerator(xs, S2, S1)) + return dg1 + + +def construct_dg2_integral(cell=None): + edge = Point(1, [Point(0), Point(0)], vertex_num=2) + x = sp.Symbol("x") + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + + dofs = [DOFGenerator(xs, S2, S1), DOFGenerator(centre, S1, S1)] + dg2 = ElementTriple(edge, (PolynomialSpace(2), CellL2, C0), dofs) + return dg2 + + def plot_dg1(): dg1 = construct_dg1() dg1.plot() @@ -243,9 +269,9 @@ def construct_bdm2(tri=None): phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] - xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y), shape=2)), - DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y), shape=2)), - DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y), shape=2))] + xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y))), + DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y))), + DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y)))] interior = DOFGenerator(xs, S1, S1) nd = PolynomialSpace(deg, set_shape=True) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 6213fd1d..9bc7d3dd 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -5,7 +5,7 @@ from firedrake import * from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature -from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3 +from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg1_integral, construct_dg2_integral from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n @@ -358,6 +358,8 @@ def test_entity_perms(elem_gen, cell): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), + (construct_dg1_integral, "DG", 1), + (construct_dg2_integral, "DG", 2), pytest.param(create_dg2, "DG", 2, marks=pytest.mark.xfail(reason='Need to update TSFC in CI')), (create_cg2, "CG", 2) ]) From 87dfe977a13e16d38fe1a9ac3998f8db85366096 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 11:31:44 +0000 Subject: [PATCH 88/98] fix mistake in components --- fuse/dof.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fuse/dof.py b/fuse/dof.py index c576ee48..97e07d50 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -190,7 +190,7 @@ def __call__(self, *args): return self.pt def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): - comps = [(i,) for v in value_shape for i in range(v)] + 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), comps if not immersed: @@ -350,6 +350,7 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): # TODO deriv dict needs implementing (currently {}) + print(value_shape) return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree, value_shape), {}, str(self)) def to_quadrature(self, arg_degree, value_shape): From ea8265b66d0ad7db60150c51c22a359ce23b65c1 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 11:32:26 +0000 Subject: [PATCH 89/98] add jacobian --- fuse/cells.py | 9 +++++++++ fuse/dof.py | 12 ++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/fuse/cells.py b/fuse/cells.py index b7b25833..83d426e9 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -835,6 +835,15 @@ 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 = [] + 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 quadrature(self, degree): fiat_el = self.to_fiat() Q = create_quadrature(fiat_el, degree) diff --git a/fuse/dof.py b/fuse/dof.py index 97e07d50..3953ae33 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -375,18 +375,22 @@ def immersed(pt): if self.immersed: # need to compute jacobian from attachment. + old_pts = pts 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) + 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) # Special case - force evaluation on different orientation of entity for construction of matrix transforms - if self.entity_o: - immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) + # if self.entity_o: + # immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) if isinstance(self.target_space, TrH1): - new_wts = wts + new_wts = wts * J_det else: - new_wts = np.outer(wts, immersion) + new_wts = np.outer(wts * J_det, immersion) else: new_wts = wts # pt dict is { pt: [(weight, component)]} From 6ad49d347b52b727399b3846da0b8c86725cd267 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 11:43:12 +0000 Subject: [PATCH 90/98] ensure vector kernel and scalar poly kernel behave the same --- fuse/dof.py | 8 +++++--- test/test_2d_examples_docs.py | 2 +- test/test_convert_to_fiat.py | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 3953ae33..fa02b1fb 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -190,7 +190,11 @@ def __call__(self, *args): return self.pt def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): - comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] + 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), comps if not immersed: @@ -374,8 +378,6 @@ 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. - old_pts = pts 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) if not np.allclose(J_det, 1): diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 06da1eea..1f9f1ca0 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -32,7 +32,7 @@ def construct_dg1(): def construct_dg0_integral(cell=None): edge = Point(1, [Point(0), Point(0)], vertex_num=2) - xs = [DOF(L2Pairing(), PolynomialKernel(1))] + xs = [DOF(L2Pairing(), VectorKernel(1))] dg0 = ElementTriple(edge, (P0, CellL2, C0), DOFGenerator(xs, S1, S1)) return dg0 diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 9bc7d3dd..9057815c 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -5,7 +5,7 @@ from firedrake import * from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature -from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg1_integral, construct_dg2_integral +from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg0_integral, construct_dg1_integral, construct_dg2_integral from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n @@ -358,6 +358,7 @@ def test_entity_perms(elem_gen, cell): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), + (construct_dg0_integral, "DG", 0), (construct_dg1_integral, "DG", 1), (construct_dg2_integral, "DG", 2), pytest.param(create_dg2, "DG", 2, marks=pytest.mark.xfail(reason='Need to update TSFC in CI')), From 3eaf4c72788fe58a6c293acbdd92b84046866e9c Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 12:16:45 +0000 Subject: [PATCH 91/98] bug fix --- fuse/cells.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuse/cells.py b/fuse/cells.py index 83d426e9..d5cbd37d 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -839,6 +839,8 @@ 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)) From fdf00cc99ffaf34af66d964f1a5a762f2cc77b23 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 15:36:42 +0000 Subject: [PATCH 92/98] use ufl sobolev spaces --- fuse/spaces/element_sobolev_spaces.py | 17 ++++++++++++++++- fuse/tensor_products.py | 2 -- test/test_tensor_prod.py | 25 ++++++++++++++++--------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/fuse/spaces/element_sobolev_spaces.py b/fuse/spaces/element_sobolev_spaces.py index 2a3aa4fb..07c9fdd9 100644 --- a/fuse/spaces/element_sobolev_spaces.py +++ b/fuse/spaces/element_sobolev_spaces.py @@ -1,5 +1,5 @@ from functools import total_ordering - +from ufl.sobolevspace import H1, HDiv, HCurl, L2, H2 @total_ordering class ElementSobolevSpace(object): @@ -54,6 +54,9 @@ def __init__(self, cell): def __repr__(self): return "H1" + + def to_ufl(self): + return H1 class CellHDiv(ElementSobolevSpace): @@ -64,6 +67,9 @@ def __init__(self, cell): def __repr__(self): return "HDiv" + def to_ufl(self): + return HDiv + class CellHCurl(ElementSobolevSpace): @@ -73,6 +79,9 @@ def __init__(self, cell): def __repr__(self): return "HCurl" + def to_ufl(self): + return HCurl + class CellH2(ElementSobolevSpace): @@ -82,6 +91,9 @@ def __init__(self, cell): def __repr__(self): return "H2" + def to_ufl(self): + return H2 + class CellL2(ElementSobolevSpace): @@ -90,3 +102,6 @@ def __init__(self, cell): def __repr__(self): return "L2" + + def to_ufl(self): + return L2 diff --git a/fuse/tensor_products.py b/fuse/tensor_products.py index cf16e247..4e0a486b 100644 --- a/fuse/tensor_products.py +++ b/fuse/tensor_products.py @@ -43,8 +43,6 @@ def to_ufl(self): if self.flat: return FuseElement(self, self.cell.flatten().to_ufl()) ufl_sub_elements = [e.to_ufl() for e in self.sub_elements()] - # self.setup_matrices() - # breakpoint() return TensorProductElement(*ufl_sub_elements, cell=self.cell.to_ufl()) def flatten(self): diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index 8fa82f27..a90e258c 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -2,7 +2,7 @@ import numpy as np from fuse import * from firedrake import * -from test_2d_examples_docs import construct_cg1, construct_dg1 +from test_2d_examples_docs import construct_cg1, construct_dg1, construct_dg1_integral # from test_convert_to_fiat import create_cg1 @@ -32,27 +32,34 @@ def mass_solve(U): assemble(L) solve(a == L, out) assert np.allclose(out.dat.data, f.dat.data, rtol=1e-5) + return out.dat.data -@pytest.mark.parametrize("generator, code, deg", [(construct_cg1, "CG", 1), (construct_dg1, "DG", 1)]) -def test_tensor_product_ext_mesh(generator, code, deg): +@pytest.mark.parametrize("generator1, generator2, code1, code2, deg1, deg2", + [(construct_cg1, construct_cg1, "CG", "CG", 1, 1), + (construct_dg1, construct_dg1, "DG", "DG", 1, 1), + (construct_dg1, construct_cg1, "DG", "CG", 1, 1), + (construct_dg1_integral, construct_cg1, "DG", "CG", 1, 1)]) +def test_ext_mesh(generator1, generator2, code1, code2, deg1, deg2): m = UnitIntervalMesh(2) mesh = ExtrudedMesh(m, 2) # manual method of creating tensor product elements - horiz_elt = FiniteElement(code, as_cell("interval"), deg) - vert_elt = FiniteElement(code, as_cell("interval"), deg) + horiz_elt = FiniteElement(code1, as_cell("interval"), deg1) + vert_elt = FiniteElement(code2, as_cell("interval"), deg2) elt = TensorProductElement(horiz_elt, vert_elt) U = FunctionSpace(mesh, elt) - mass_solve(U) + res1 = mass_solve(U) # fuseonic way of creating tensor product elements - A = generator() - B = generator() + A = generator1() + B = generator2() elem = tensor_product(A, B) U = FunctionSpace(mesh, elem.to_ufl()) - mass_solve(U) + res2 = mass_solve(U) + + assert np.allclose(res1, res2) def test_helmholtz(): From f7f27099a9c46cc79577dd560533d37fb1568b0e Mon Sep 17 00:00:00 2001 From: India Marsden Date: Wed, 11 Mar 2026 18:04:27 +0000 Subject: [PATCH 93/98] first stab at addition --- fuse/dof.py | 1 - fuse/spaces/polynomial_spaces.py | 4 +-- fuse/triples.py | 15 ++++++++++++ test/test_algebra.py | 42 ++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 test/test_algebra.py diff --git a/fuse/dof.py b/fuse/dof.py index fa02b1fb..fb04e427 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -354,7 +354,6 @@ def add_context(self, dof_gen, cell, space, g, overall_id=None, generator_id=Non def convert_to_fiat(self, ref_el, interpolant_degree, value_shape=tuple()): # TODO deriv dict needs implementing (currently {}) - print(value_shape) return Functional(ref_el, value_shape, self.to_quadrature(interpolant_degree, value_shape), {}, str(self)) def to_quadrature(self, arg_degree, value_shape): diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 3502a6ea..fa17c6ac 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -96,9 +96,7 @@ def __mul__(self, x): the sympy object on the right. This is due to Sympy's implementation of __mul__ not passing to this handler as it should. """ - if isinstance(x, sp.Symbol): - return ConstructedPolynomialSpace([x], [self]) - elif isinstance(x, sp.Matrix): + if isinstance(x, sp.Symbol) or isinstance(x, sp.Expr) or isinstance(x, sp.Matrix): return ConstructedPolynomialSpace([x], [self]) else: raise TypeError(f'Cannot multiply a PolySpace with {type(x)}') diff --git a/fuse/triples.py b/fuse/triples.py index 32a72208..6709eb58 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -537,6 +537,21 @@ def reverse_dof_perms(self, matrices): reversed_mats[dim][e_id] = perms_copy return reversed_mats + def __add__(self, other): + """ Construct a new element triple by combining the degrees of freedom + This implementation does not make assertions about the properties + of the resulting element. + + Elements being adding must be defined over the same cell and have the same + value shape and mapping""" + assert self.cell == other.cell + assert self.spaces[0].set_shape == other.spaces[0].set_shape + assert str(self.spaces[1]) == str(other.spaces[1]) + + spaces = (self.spaces[0] + other.spaces[0], self.spaces[1], max([self.spaces[2], other.spaces[2]])) + + return ElementTriple(self.cell, spaces, self.DOFGenerator + other.DOFGenerator) + def _to_dict(self): o_dict = {"cell": self.cell, "spaces": self.spaces, "dofs": self.DOFGenerator} return o_dict diff --git a/test/test_algebra.py b/test/test_algebra.py new file mode 100644 index 00000000..93385cab --- /dev/null +++ b/test/test_algebra.py @@ -0,0 +1,42 @@ +from fuse import * +from firedrake import * +import sympy as sp +from test_convert_to_fiat import create_cg2_tri, create_cg1, create_cr + + + +def test_bubble(): + mesh = UnitTriangleMesh() + x = SpatialCoordinate(mesh) + P2 = FiniteElement("CG", "triangle", 2) + Bubble = FiniteElement("Bubble", "triangle", 3) + P2B3 = P2 + Bubble + V = FunctionSpace(mesh, P2B3) + W = FunctionSpace(mesh, "CG", 3) + u = project(27*x[0]*x[1]*(1-x[0]-x[1]), V) + exact = Function(W) + exact.interpolate(27*x[0]*x[1]*(1-x[0]-x[1])) + # make sure that these are the same + assert sqrt(assemble((u-exact)*(u-exact)*dx)) < 1e-14 + + +def construct_bubble(cell=None): + if cell is None: + cell = polygon(3) + x = sp.Symbol("x") + y = sp.Symbol("y") + space = PolynomialSpace(0)*(x*y*(-x-y+1)) + breakpoint() + xs = [DOF(DeltaPairing(), PointKernel((0, 0)))] + bubble = ElementTriple(cell, (space, CellL2, L2), DOFGenerator(xs, S1, S1)) + return bubble + + + +def test_temp(): + tri = polygon(3) + cg1 = create_cg1(tri) + cr1 = create_cr(tri) + cg2 = cg1 + cr1 + cg2.to_fiat() + breakpoint() \ No newline at end of file From 860f11e84854163b552774828bac15f2e0814466 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 12 Mar 2026 15:59:08 +0000 Subject: [PATCH 94/98] fix bubble - addition working --- Makefile | 6 ++++ fuse/dof.py | 4 +-- fuse/spaces/element_sobolev_spaces.py | 3 +- fuse/spaces/polynomial_spaces.py | 10 +++--- fuse/triples.py | 31 +++++++++---------- fuse/utils.py | 5 ++- test/test_algebra.py | 44 ++++++++++++--------------- 7 files changed, 51 insertions(+), 52 deletions(-) diff --git a/Makefile b/Makefile index 734a8d34..e21d433a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/fuse/dof.py b/fuse/dof.py index fb04e427..4c6546f5 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -347,9 +347,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()): diff --git a/fuse/spaces/element_sobolev_spaces.py b/fuse/spaces/element_sobolev_spaces.py index 07c9fdd9..b5964d9d 100644 --- a/fuse/spaces/element_sobolev_spaces.py +++ b/fuse/spaces/element_sobolev_spaces.py @@ -1,6 +1,7 @@ from functools import total_ordering from ufl.sobolevspace import H1, HDiv, HCurl, L2, H2 + @total_ordering class ElementSobolevSpace(object): """ @@ -54,7 +55,7 @@ def __init__(self, cell): def __repr__(self): return "H1" - + def to_ufl(self): return H1 diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index fa17c6ac..7097c5f0 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -174,9 +174,7 @@ def to_ON_polynomial_set(self, ref_el): ref_el = cell_to_simplex(ref_el) # otherwise have to work on this through tabulation - - Q = create_quadrature(ref_el, 2 * (k + 1)) - Qpts, Qwts = Q.get_points(), Q.get_weights() + weighted_sets = [] for (s, w) in zip(self.spaces, self.weights): @@ -194,6 +192,8 @@ def to_ON_polynomial_set(self, ref_el): else: vec = True w_deg = max_deg_sp_expr(w) + Q = create_quadrature(ref_el, 2 * (k + w_deg + 1)) + Qpts, Qwts = Q.get_points(), Q.get_weights() Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, shape, scale="orthonormal") # vec_Pkpw = ONPolynomialSet(ref_el, space.degree + w_deg, (sd,), scale="orthonormal") @@ -204,11 +204,11 @@ def to_ON_polynomial_set(self, ref_el): if s.set_shape or vec: scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] else: - # breakpoint() scaled_at_Qpts = space_at_Qpts[:, None, :] * tabulated_expr[None, :, :] scaled_at_Qpts = scaled_at_Qpts.squeeze() PkHw_coeffs = np.dot(np.multiply(scaled_at_Qpts, Qwts), Pkpw_at_Qpts.T) - # breakpoint() + if len(PkHw_coeffs.shape) == 1: + PkHw_coeffs = PkHw_coeffs.reshape(1, -1) weighted_sets.append(polynomial_set.PolynomialSet(ref_el, space.degree + w_deg, space.degree + w_deg, diff --git a/fuse/triples.py b/fuse/triples.py index 6709eb58..2d297336 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -421,7 +421,7 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): ent_dofs = entity_associations[dim][e_id][dof_gen] ent_dofs_ids = np.array([self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs], dtype=int) # (dof_gen, ent_dofs) - total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if ed.id not in total_ent_dof_ids] + total_ent_dof_ids += [self.dof_id_to_fiat_id[ed.id] for ed in ent_dofs if self.dof_id_to_fiat_id[ed.id] not in total_ent_dof_ids] # dof_idx = [total_ent_dof_ids.index(id) for id in ent_dofs_ids] dof_gen_class = ent_dofs[0].generation @@ -589,21 +589,20 @@ def num_dofs(self): return self.dof_numbers def generate(self, cell, space, id_counter): - if self.ls is None: - self.ls = [] - for l_g in self.x: - i = 0 - for g in self.g1.members(): - generated = l_g(g) - if not isinstance(generated, list): - generated = [generated] - for dof in generated: - dof.add_context(self, cell, space, g, id_counter, i) - id_counter += 1 - i += 1 - self.ls.extend(generated) - self.dof_numbers = len(self.ls) - self.dof_ids = [dof.id for dof in self.ls] + self.ls = [] + for l_g in self.x: + i = 0 + for g in self.g1.members(): + generated = l_g(g) + if not isinstance(generated, list): + generated = [generated] + for dof in generated: + dof.add_context(self, cell, space, g, id_counter, i) + id_counter += 1 + i += 1 + self.ls.extend(generated) + self.dof_numbers = len(self.ls) + self.dof_ids = [dof.id for dof in self.ls] return self.ls def make_entity_ids(self): diff --git a/fuse/utils.py b/fuse/utils.py index 6592e117..0d0ee9a9 100644 --- a/fuse/utils.py +++ b/fuse/utils.py @@ -67,11 +67,10 @@ def max_deg_sp_expr(sp_expr): for comp in sp_expr: # only compute degree if component is a polynomial if sp.sympify(comp).as_poly(): - degs += [sp.sympify(comp).as_poly().degree()] + degs += [sp.sympify(comp).as_poly().total_degree()] else: if sp.sympify(sp_expr).as_poly(): - degs += [sp.sympify(sp_expr).as_poly().degree()] - + degs += [sp.sympify(sp_expr).as_poly().total_degree()] return max(degs) diff --git a/test/test_algebra.py b/test/test_algebra.py index 93385cab..53049276 100644 --- a/test/test_algebra.py +++ b/test/test_algebra.py @@ -1,23 +1,8 @@ from fuse import * from firedrake import * +import numpy as np import sympy as sp -from test_convert_to_fiat import create_cg2_tri, create_cg1, create_cr - - - -def test_bubble(): - mesh = UnitTriangleMesh() - x = SpatialCoordinate(mesh) - P2 = FiniteElement("CG", "triangle", 2) - Bubble = FiniteElement("Bubble", "triangle", 3) - P2B3 = P2 + Bubble - V = FunctionSpace(mesh, P2B3) - W = FunctionSpace(mesh, "CG", 3) - u = project(27*x[0]*x[1]*(1-x[0]-x[1]), V) - exact = Function(W) - exact.interpolate(27*x[0]*x[1]*(1-x[0]-x[1])) - # make sure that these are the same - assert sqrt(assemble((u-exact)*(u-exact)*dx)) < 1e-14 +from test_convert_to_fiat import create_cg2_tri, construct_cg3 def construct_bubble(cell=None): @@ -25,18 +10,27 @@ def construct_bubble(cell=None): cell = polygon(3) x = sp.Symbol("x") y = sp.Symbol("y") - space = PolynomialSpace(0)*(x*y*(-x-y+1)) - breakpoint() + f = (3*np.sqrt(3)/4)*(y + np.sqrt(3)/3)*(np.sqrt(3)*x + y - 2*np.sqrt(3)/3)*(-np.sqrt(3)*x + y - 2*np.sqrt(3)/3) + space = PolynomialSpace(3).restrict(0, 0)*f xs = [DOF(DeltaPairing(), PointKernel((0, 0)))] bubble = ElementTriple(cell, (space, CellL2, L2), DOFGenerator(xs, S1, S1)) return bubble +def test_bubble(): + mesh = UnitTriangleMesh() + x = SpatialCoordinate(mesh) -def test_temp(): tri = polygon(3) - cg1 = create_cg1(tri) - cr1 = create_cr(tri) - cg2 = cg1 + cr1 - cg2.to_fiat() - breakpoint() \ No newline at end of file + bub = construct_bubble(tri) + cg2 = create_cg2_tri(tri) + p2b3 = bub + cg2 + V = FunctionSpace(mesh, p2b3.to_ufl()) + W = FunctionSpace(mesh, construct_cg3().to_ufl()) + + bubble_func = 27*x[0]*x[1]*(1-x[0]-x[1]) + u = project(bubble_func, V) + exact = Function(W) + exact.interpolate(bubble_func, W) + # make sure that these are the same + assert sqrt(assemble((u-exact)*(u-exact)*dx)) < 1e-14 From d19632043bf8ce22123dfd1257abcbda96fd9b34 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Thu, 12 Mar 2026 17:29:57 +0000 Subject: [PATCH 95/98] fix tests --- fuse/spaces/polynomial_spaces.py | 2 +- test/test_3d_examples_docs.py | 2 +- test/test_convert_to_fiat.py | 2 +- test/test_dofs.py | 2 +- test/test_perms.py | 22 +---------- test/test_tensor_prod.py | 64 ++++++++++++++++++++++++++++++-- 6 files changed, 66 insertions(+), 28 deletions(-) diff --git a/fuse/spaces/polynomial_spaces.py b/fuse/spaces/polynomial_spaces.py index 7097c5f0..c7f8d369 100644 --- a/fuse/spaces/polynomial_spaces.py +++ b/fuse/spaces/polynomial_spaces.py @@ -174,7 +174,7 @@ def to_ON_polynomial_set(self, ref_el): ref_el = cell_to_simplex(ref_el) # otherwise have to work on this through tabulation - + weighted_sets = [] for (s, w) in zip(self.spaces, self.weights): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 803c3619..75729430 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -327,7 +327,7 @@ def test_tet_rt2(): ls = rt2.generate() # TODO make this a proper test for dof in ls: - print(dof.to_quadrature(1)) + print(dof.to_quadrature(1, value_shape=(2,))) rt2.to_fiat() diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 9057815c..ad1db363 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -358,7 +358,7 @@ def test_entity_perms(elem_gen, cell): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(create_cg1, "CG", 1), (create_dg1, "DG", 1), - (construct_dg0_integral, "DG", 0), + pytest.param(construct_dg0_integral, "DG", 0, marks=pytest.mark.xfail(reason='Passes locally, fails in CI, probably same as dg2')), (construct_dg1_integral, "DG", 1), (construct_dg2_integral, "DG", 2), pytest.param(create_dg2, "DG", 2, marks=pytest.mark.xfail(reason='Need to update TSFC in CI')), diff --git a/test/test_dofs.py b/test/test_dofs.py index ec7424f3..bf29ad45 100644 --- a/test/test_dofs.py +++ b/test/test_dofs.py @@ -220,6 +220,6 @@ def test_generate_quadrature(): print("fiat", d.pt_dict) print() for d in elem.generate(): - print("fuse", d.to_quadrature(degree)) + print("fuse", d.to_quadrature(degree, value_shape=(2,))) elem.to_fiat() diff --git a/test/test_perms.py b/test/test_perms.py index f00aed6e..82a3cb90 100644 --- a/test/test_perms.py +++ b/test/test_perms.py @@ -1,9 +1,8 @@ from fuse import * -from test_convert_to_fiat import create_cg1, create_dg1, create_cg2 +from test_convert_to_fiat import create_cg1, create_dg1, create_cg2, construct_nd from test_2d_examples_docs import construct_cg3 import pytest import numpy as np -import sympy as sp vert = Point(0) edge = Point(1, [Point(0), Point(0)], vertex_num=2) @@ -48,24 +47,7 @@ def test_basic_perms(cell): @pytest.mark.parametrize("cell", [tri]) def test_nd_perms(cell): - deg = 1 - edge = cell.edges(get_class=True)[0] - x = sp.Symbol("x") - y = sp.Symbol("y") - - xs = [DOF(L2Pairing(), PolynomialKernel(edge.basis_vectors()[0]))] - dofs = DOFGenerator(xs, S1, S2) - int_ned = ElementTriple(edge, (P1, CellHCurl, C0), dofs) - - xs = [immerse(cell, int_ned, TrHCurl)] - tri_dofs = DOFGenerator(xs, C3, S3) - - M = sp.Matrix([[y, -x]]) - vec_Pk = PolynomialSpace(deg - 1, set_shape=True) - Pk = PolynomialSpace(deg - 1) - nd = vec_Pk + (Pk.restrict(deg - 2, deg - 1))*M - - ned = ElementTriple(cell, (nd, CellHCurl, C0), [tri_dofs]) + ned = construct_nd(cell) ned.to_fiat() for i, mat in ned.matrices[2][0].items(): print(i) diff --git a/test/test_tensor_prod.py b/test/test_tensor_prod.py index a90e258c..15e15a71 100644 --- a/test/test_tensor_prod.py +++ b/test/test_tensor_prod.py @@ -36,10 +36,10 @@ def mass_solve(U): @pytest.mark.parametrize("generator1, generator2, code1, code2, deg1, deg2", - [(construct_cg1, construct_cg1, "CG", "CG", 1, 1), - (construct_dg1, construct_dg1, "DG", "DG", 1, 1), - (construct_dg1, construct_cg1, "DG", "CG", 1, 1), - (construct_dg1_integral, construct_cg1, "DG", "CG", 1, 1)]) + [(construct_cg1, construct_cg1, "CG", "CG", 1, 1), + (construct_dg1, construct_dg1, "DG", "DG", 1, 1), + (construct_dg1, construct_cg1, "DG", "CG", 1, 1), + (construct_dg1_integral, construct_cg1, "DG", "CG", 1, 1)]) def test_ext_mesh(generator1, generator2, code1, code2, deg1, deg2): m = UnitIntervalMesh(2) mesh = ExtrudedMesh(m, 2) @@ -137,3 +137,59 @@ def test_flattening(A, B, res): else: cell = tensor_cell.flatten() cell.construct_fuse_rep() + + +# def test_trace_galerkin_projection(): +# mesh = UnitSquareMesh(10, 10, quadrilateral=True) + +# x, y = SpatialCoordinate(mesh) +# A = construct_cg1() +# B = construct_dg0_integral() +# elem = tensor_product(A, B) +# elem = elem.flatten() + +# # Define the Trace Space +# T = FunctionSpace(mesh, elem.to_ufl()) + +# # Define trial and test functions +# lambdar = TrialFunction(T) +# gammar = TestFunction(T) + +# # Define right hand side function + +# V = FunctionSpace(mesh, "CG", 1) +# f = Function(V) +# f.interpolate(cos(x*pi*2)*cos(y*pi*2)) + +# # Construct bilinear form +# a = inner(lambdar, gammar) * ds + inner(lambdar('+'), gammar('+')) * dS + +# # Construct linear form +# l = inner(f, gammar) * ds + inner(f('+'), gammar('+')) * dS + +# # Compute the solution +# t = Function(T) +# solve(a == l, t, solver_parameters={'ksp_rtol': 1e-14}) + +# # Compute error in trace norm +# trace_error = sqrt(assemble(FacetArea(mesh)*inner((t - f)('+'), (t - f)('+')) * dS)) + +# assert trace_error < 1e-13 + +# def test_hdiv(): +# np.set_printoptions(linewidth=90, precision=4, suppress=True) +# m = UnitIntervalMesh(2) +# mesh = ExtrudedMesh(m, 2) +# CG_1 = FiniteElement("CG", interval, 1) +# DG_0 = FiniteElement("DG", interval, 0) +# P1P0 = TensorProductElement(CG_1, DG_0) +# RT_horiz = HDivElement(P1P0) +# P0P1 = TensorProductElement(DG_0, CG_1) +# RT_vert = HDivElement(P0P1) +# elt = RT_horiz + RT_vert +# V = FunctionSpace(mesh, elt) +# tabulation = V.finat_element.fiat_equivalent.tabulate(0, [(0, 0), (1, 0)]) +# for ent, arr in tabulation.items(): +# print(ent) +# for comp in arr: +# print(comp[0], comp[1]) From 3bad4ca2d4aafa6aa4901befee987ac2bf01ce91 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 16 Mar 2026 08:49:44 +0000 Subject: [PATCH 96/98] merge stash --- fuse/dof.py | 73 ++++++++++++++++++++++++++++++----- fuse/triples.py | 9 +++-- test/test_2d_examples_docs.py | 15 +++---- test/test_3d_examples_docs.py | 48 ++++++++++++++++++++--- test/test_convert_to_fiat.py | 18 +++------ 5 files changed, 126 insertions(+), 37 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 7fcdfcb3..aaec2cc7 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -215,19 +215,74 @@ def _from_dict(obj_dict): return VectorKernel(tuple(obj_dict["pt"])) -class PolynomialKernel(BaseKernel): +class BarycentricPolynomialKernel(BaseKernel): def __init__(self, fn, g=None, symbols=[]): if hasattr(fn, "__iter__"): - shape = len(fn) + if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape)): + raise ValueError("Function components must be able to be interpreted as a sympy polynomial") + self.fn = [sp.sympify(fn[i]).as_poly() for i in range(len(fn))] + self.shape = len(fn) else: - shape = 0 - if len(symbols) != 0 and (shape != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape))) and not sp.sympify(fn).as_poly(): - raise ValueError("Function argument or its components must be able to be interpreted as a sympy polynomial") - if shape != 0: - self.fn = [sp.sympify(fn[i]).as_poly() for i in range(shape)] - self.shape = shape + if len(symbols) != 0 and not sp.sympify(fn).as_poly(): + raise ValueError("Function must be able to be interpreted as a sympy polynomial") + self.fn = sp.sympify(fn) + self.shape = 0 + self.g = g + self.syms = symbols + super(PolynomialKernel, self).__init__() + + def __repr__(self): + return str(self.fn) + + def degree(self, interpolant_degree): + if self.shape != 0: + return max([self.fn[i].as_poly().total_degree() for i in range(self.shape)]) + interpolant_degree + if len(self.fn.free_symbols) == 0: # this should probably be removed + return interpolant_degree + return self.fn.as_poly().total_degree() + interpolant_degree + + def permute(self, g): + new_fn = self.fn + return PolynomialKernel(new_fn, g=g, symbols=self.syms) + + def __call__(self, *args): + if self.shape == 0: + res = sympy_to_numpy(self.fn, self.syms, args[:len(self.syms)]) else: + 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): + 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 not np.allclose(np.matmul(basis_change, self.pt), self.g(self.pt)): + breakpoint() + return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps + + def _to_dict(self): + o_dict = {"fn": self.fn} + return o_dict + + def dict_id(self): + return "PolynomialKernel" + + def _from_dict(obj_dict): + return PolynomialKernel(obj_dict["fn"]) + +class PolynomialKernel(BaseKernel): + + def __init__(self, fn, g=None, symbols=[]): + if hasattr(fn, "__iter__"): + if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape)): + raise ValueError("Function components must be able to be interpreted as a sympy polynomial") + self.fn = [sp.sympify(fn[i]).as_poly() for i in range(len(fn))] + self.shape = len(fn) + else: + if len(symbols) != 0 and not sp.sympify(fn).as_poly(): + raise ValueError("Function must be able to be interpreted as a sympy polynomial") self.fn = sp.sympify(fn) self.shape = 0 self.g = g @@ -262,7 +317,7 @@ def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): else: comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps - + def _to_dict(self): o_dict = {"fn": self.fn} return o_dict diff --git a/fuse/triples.py b/fuse/triples.py index 2891331d..9bf9aa44 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -479,10 +479,11 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): # Permutation of DOF on the entity they are defined on sub_mat = (~g).matrix_form() oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() - elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim() and len(ent_dofs_ids) == len(g.perm.array_form): - # case for dofs defined on the cell and not immersed - sub_mat = (~g).matrix_form() - oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + elif len(dof_gen_class.keys()) == 1 and dim == self.cell.dim() and len(ent_dofs_ids) == g.perm.size: + # case for dofs defined on the cell and not immersed - since they are interior the orientations don't matter + # sub_mat = (~g).matrix_form() + # oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = sub_mat.copy() + oriented_mats_by_entity[dim][e_id][val][np.ix_(ent_dofs_ids, ent_dofs_ids)] = np.eye(len(ent_dofs_ids)) else: # TODO what if an orientation is not in G1 # also the case of 3 dofs inside a 3d shape diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 6d674573..0a53ae3c 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -269,15 +269,16 @@ def construct_bdm2(tri=None): phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] - xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y))), - DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y))), - DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y)))] - interior = DOFGenerator(xs, S1, S1) + xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y)))] + # DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y))), + # DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y)))] + interior = DOFGenerator(xs, C3, S1) - nd = PolynomialSpace(deg, set_shape=True) + space = PolynomialSpace(deg, set_shape=True) - rt = ElementTriple(tri, (nd, CellHDiv, C0), [tri_dofs, interior]) - return rt + bdm2 = ElementTriple(tri, (space, CellHDiv, C0), [tri_dofs, interior]) + breakpoint() + return bdm2 def test_nd_example(): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index db9d52c5..dc5c8a62 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -172,7 +172,7 @@ def construct_tet_rt2(cell=None, perm=None): rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M xs = [DOF(L2Pairing(), PolynomialKernel(1/3 - (1/2)*x + y/(2*np.sqrt(3)), symbols=(x, y)))] - dofs = DOFGenerator(xs, C3, S2) + dofs = DOFGenerator(xs, C3, S3) face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] @@ -202,16 +202,54 @@ def construct_tet_bdm(cell=None, perm=None): x = sp.Symbol("x") y = sp.Symbol("y") - rt_space = PolynomialSpace(deg, set_shape=True) + space = PolynomialSpace(deg, set_shape=True) xs = [DOF(L2Pairing(), PolynomialKernel(1/3 - (1/2)*x + y/(2*np.sqrt(3)), symbols=(x, y)))] dofs = DOFGenerator(xs, C3, S2) - face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) + face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] faces = DOFGenerator(im_xs, tet_faces, S1) - rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), [faces]) - return rt2 + bdm = ElementTriple(cell, (space, CellHDiv, "C0"), [faces]) + return bdm + + +def construct_tet_bdm2(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 2 + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + + space = PolynomialSpace(deg, set_shape=True) + + s = np.sqrt(3) + vertex_basis = (1/9)*(-1 + s*y + (2/3)*y**2) + edge_basis = (4/9)*(1 - 4*s*y) - x**2 + (1/3)*(y**2) + xs = [DOF(L2Pairing(), PolynomialKernel(vertex_basis, symbols=(x, y)))] + xs1 = [DOF(L2Pairing(), PolynomialKernel(edge_basis, symbols=(x, y)))] + dofs = [DOFGenerator(xs, C3, S3), DOFGenerator(xs1, C3, S3)] + face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + s2 = np.sqrt(2) + N = {} + N[(1, 2)] = [y/4 + z/4, -x/4 - s2/8, -x/4 - s2/8] + N[(1, 3)] = [-y/4 - s2/8, x/4 + z/4, -y/4 - s2/8] + N[(1, 4)] = [-z/4 - s2/8, -z/4 - s2/8, x/4 + y/4] + N[(2, 3)] = [z/4 - s2/8, -z/4 + s2/8, -x/4 + y/4] + N[(2, 4)] = [y/4 - s2/8, -x/4 + z/4, -y/4 + s2/8] + N[(3, 4)] = [-y/4 + z/4, x/4 - s2/8, -x/4 + s2/8] + xs = [DOF(L2Pairing(), PolynomialKernel(N[phi], symbols=(x, y, z))) for phi in N.keys()] + interior = DOFGenerator(xs, S1, S1) + + bdm2 = ElementTriple(cell, (space, CellHDiv, "C0"), [faces, interior]) + breakpoint() + return bdm2 + def construct_tet_ned(cell=None): diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 86380b35..96f875ca 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg0_integral, construct_dg1_integral, construct_dg2_integral -from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_ned2, construct_tet_cg4 +from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_bdm2, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -559,7 +559,9 @@ def test_poisson_analytic(params, elem_gen): @pytest.mark.parametrize(['elem_gen'], - [(create_cg1_quad_tensor,), pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='Issue with cell/mesh'))]) + [(create_cg1_quad_tensor,), + pytest.param(create_cg1_quad, marks=pytest.mark.xfail(reason='Issue with cell/mesh')) + ]) def test_quad(elem_gen): elem = elem_gen() r = 0 @@ -567,15 +569,6 @@ def test_quad(elem_gen): assert (poisson_solve(r, ufl_elem, parameters={}, quadrilateral=True) < 1.e-9) -@pytest.mark.xfail(reason="Issue with quad cell") -def test_non_tensor_quad(): - elem = create_cg1_quad() - # ufl_elem = elem.to_ufl() - print(elem.to_fiat().entity_permutations()) - # elem.cell.hasse_diagram(filename="cg1quad.png") - assert (run_test_original(1, "CG", 1, parameters={}, quadrilateral=True) < 1.e-9) - - def project(U, mesh, func): f = assemble(interpolate(func, U)) @@ -634,7 +627,8 @@ def test_project_3d(elem_gen, elem_code, deg): (construct_tet_ned, "N1curl", 1, 0.8), (construct_tet_ned2, "N1curl", 2, 1.8), (construct_tet_ned_2nd_kind, "N2curl", 1, 1.8), - (construct_tet_bdm, "BDM", 1, 1.8)]) + (construct_tet_bdm, "BDM", 1, 1.8), + (construct_tet_bdm2, "BDM", 2, 2.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): cell = make_tetrahedron() elem = elem_gen(cell) From 7b2a0cce8862074516101f45182c5c938e194d52 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Mon, 16 Mar 2026 17:59:57 +0000 Subject: [PATCH 97/98] add barycentric polynomials --- fuse/__init__.py | 2 +- fuse/cells.py | 19 ++- fuse/dof.py | 43 +++--- fuse/groups.py | 14 +- fuse/traces.py | 1 + fuse/triples.py | 18 ++- test/test_2d_examples_docs.py | 60 ++++++-- test/test_3d_examples_docs.py | 262 +++++++++++++++++++--------------- test/test_cells.py | 4 +- test/test_orientations.py | 6 +- 10 files changed, 261 insertions(+), 168 deletions(-) diff --git a/fuse/__init__.py b/fuse/__init__.py index 0db1c3d7..f7e89d39 100644 --- a/fuse/__init__.py +++ b/fuse/__init__.py @@ -1,7 +1,7 @@ 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, PolynomialKernel, ComponentKernel +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 diff --git a/fuse/cells.py b/fuse/cells.py index 620282bc..8b1942c6 100644 --- a/fuse/cells.py +++ b/fuse/cells.py @@ -668,7 +668,7 @@ def permute_entities(self, g, d): return reordered_entities - def basis_vectors(self, return_coords=True, entity=None, order=False): + def basis_vectors(self, return_coords=True, entity=None, order=False, norm=True): if not entity: entity = self self_levels = [generation for generation in nx.topological_generations(self.G)] @@ -687,7 +687,10 @@ def basis_vectors(self, return_coords=True, entity=None, order=False): for v in vertices[1:]: if return_coords: v_coords = self.attachment(top_level_node, v)() - sub = normalise(np.subtract(v_coords, v_0_coords)) + if norm: + sub = normalise(np.subtract(v_coords, v_0_coords)) + else: + sub = np.subtract(v_coords, v_0_coords) if not hasattr(sub, "__iter__"): basis_vecs.append((sub,)) else: @@ -861,6 +864,18 @@ def quadrature(self, degree): pts, wts = Q.get_points(), Q.get_weights() return pts, wts + def cartesian_to_barycentric(self, pts): + verts = np.array(self.ordered_vertex_coords()) + v_0 = self.ordered_vertex_coords()[0] + bvs = np.array(self.basis_vectors(norm=False)) + bary_coords = [] + for pt in pts: + res = np.matmul(np.linalg.inv(bvs.T), np.array(pt - v_0)) + assert np.allclose(sum(bvs[i]*res[i] for i in range(len(bvs))), np.array(pt - v_0)) + bary_coords += [(1 - sum(res),) + tuple(res[i] for i in range(len(res)))] + assert np.allclose(np.array(sum(bary_coords[-1][i]*verts[i] for i in range(len(verts)))), pt) + return bary_coords + def cell_attachment(self, dst): if not isinstance(dst, int): raise ValueError diff --git a/fuse/dof.py b/fuse/dof.py index aaec2cc7..0b5dc1cf 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -219,8 +219,8 @@ class BarycentricPolynomialKernel(BaseKernel): def __init__(self, fn, g=None, symbols=[]): if hasattr(fn, "__iter__"): - if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape)): - raise ValueError("Function components must be able to be interpreted as a sympy polynomial") + # if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(len(fn))): + # raise ValueError("Function components must be able to be interpreted as a sympy polynomial") self.fn = [sp.sympify(fn[i]).as_poly() for i in range(len(fn))] self.shape = len(fn) else: @@ -230,7 +230,7 @@ def __init__(self, fn, g=None, symbols=[]): self.shape = 0 self.g = g self.syms = symbols - super(PolynomialKernel, self).__init__() + super(BarycentricPolynomialKernel, self).__init__() def __repr__(self): return str(self.fn) @@ -244,7 +244,7 @@ def degree(self, interpolant_degree): def permute(self, g): new_fn = self.fn - return PolynomialKernel(new_fn, g=g, symbols=self.syms) + return BarycentricPolynomialKernel(new_fn, g=g, symbols=self.syms) def __call__(self, *args): if self.shape == 0: @@ -253,30 +253,29 @@ def __call__(self, *args): 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): + def evaluate(self, Qpts, bary_pts, 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 not np.allclose(np.matmul(basis_change, self.pt), self.g(self.pt)): - breakpoint() - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps + return Qpts, np.array([wt*self(*self.g.permute(pt)) for pt, wt in zip(bary_pts, Qwts)]).astype(np.float64), comps def _to_dict(self): o_dict = {"fn": self.fn} return o_dict def dict_id(self): - return "PolynomialKernel" + return "BarycentricPolynomialKernel" def _from_dict(obj_dict): - return PolynomialKernel(obj_dict["fn"]) + return BarycentricPolynomialKernel(obj_dict["fn"]) + class PolynomialKernel(BaseKernel): - def __init__(self, fn, g=None, symbols=[]): + def __init__(self, fn, g=None, symbols=[]): if hasattr(fn, "__iter__"): - if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(shape)): + if len(symbols) != 0 and any(not sp.sympify(fn[i]).as_poly() for i in range(len(fn))): raise ValueError("Function components must be able to be interpreted as a sympy polynomial") self.fn = [sp.sympify(fn[i]).as_poly() for i in range(len(fn))] self.shape = len(fn) @@ -317,7 +316,7 @@ def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): else: comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps - + def _to_dict(self): o_dict = {"fn": self.fn} return o_dict @@ -346,7 +345,6 @@ def permute(self, g): def __call__(self, *args): return tuple(args[i] if i in self.comp else 0 for i in range(len(args))) -# return tuple(args[c] for c in self.comp) def evaluate(self, Qpts, Qwts, immersed, dim): return Qpts, Qwts, [[self.comp] for pt in Qpts] @@ -423,6 +421,7 @@ def to_quadrature(self, arg_degree, value_shape): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) else: basis_change = np.eye(dim) + if self.immersed and isinstance(self.kernel, VectorKernel): def immersed(pt): basis = np.array(self.cell_defined_on.basis_vectors()).T @@ -431,20 +430,19 @@ def immersed(pt): return np.matmul(basis_coeffs, immersed_basis) else: immersed = self.immersed - pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension, value_shape) + + if isinstance(self.kernel, BarycentricPolynomialKernel): + bary_pts = self.cell_defined_on.cartesian_to_barycentric(Qpts) + pts, wts, comps = self.kernel.evaluate(Qpts, bary_pts, Qwts, basis_change, immersed, self.cell.dimension, value_shape) + else: + pts, wts, comps = self.kernel.evaluate(Qpts, Qwts, basis_change, immersed, self.cell.dimension, value_shape) if self.immersed: 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) 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) - # Special case - force evaluation on different orientation of entity for construction of matrix transforms - # if self.entity_o: - # immersion = self.target_space.tabulate(wts, self.pairing.entity.orient(self.entity_o)) if isinstance(self.target_space, TrH1): new_wts = wts * J_det else: @@ -453,7 +451,8 @@ def immersed(pt): new_wts = wts # pt dict is { pt: [(weight, component)]} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} - if self.cell_defined_on.dimension == 2: + if self.cell_defined_on.dimension >= 2: + print(self) np.set_printoptions(linewidth=90, precision=4, suppress=True) for key, val in pt_dict.items(): print(np.array(key), ":", np.array([v[0] for v in val])) diff --git a/fuse/groups.py b/fuse/groups.py index c5f77661..fd8217e6 100644 --- a/fuse/groups.py +++ b/fuse/groups.py @@ -175,12 +175,12 @@ def cosets(self, subset): g = seen[0] coset = [] for h in subset.members(): - # try: - coset += [g*h] - seen.remove(g*h) - # except ValueError: - # # member of subset not a member of superset - # pass + try: + coset += [g*h] + seen.remove(g*h) + except ValueError: + # member of subset not a member of superset + pass cosets += [coset] return cosets @@ -295,7 +295,7 @@ def __init__(self, base_group, cell=None): # self._members = sorted(self._members, key=lambda g: g.numeric_rep()) else: self.cell = None - + def add_cell(self, cell): return GroupRepresentation(self.base_group, cell=cell) diff --git a/fuse/traces.py b/fuse/traces.py index b5f97225..69e5f737 100644 --- a/fuse/traces.py +++ b/fuse/traces.py @@ -139,6 +139,7 @@ def tabulate(self, Qwts, trace_entity): # return result def manipulate_basis(self, basis): + breakpoint() return basis[0] def plot(self, ax, coord, trace_entity, **kwargs): diff --git a/fuse/triples.py b/fuse/triples.py index 9bf9aa44..3444ff5d 100644 --- a/fuse/triples.py +++ b/fuse/triples.py @@ -439,12 +439,24 @@ def make_dof_perms(self, ref_el, entity_ids, nodes, poly_set): bvs = np.array(e.basis_vectors()) new_bvs = np.array(e.orient(~g).basis_vectors()) basis_change = np.matmul(new_bvs, np.linalg.inv(bvs)) + cosets = dof_gen_class[dim].g1.cosets(e.basis_group) - if len(cosets[0]) < len(bvs): - value_change = ent_dofs[0].target_space.manipulate_basis(basis_change) + if len(ent_dofs_ids) == len(basis_change): + value_change = basis_change + elif len(cosets[0]) < len(bvs): + if e.basis_group.size() > 2: + raise NotImplementedError("This logic is only valid for facets of 2 dimensions or less") + value_change = None + for coset in cosets: + if g in coset: + value_change = 1 + elif g*e.basis_group.members()[1] in coset: + value_change = -1 + if value_change is None: + raise ValueError("Basis group and generation group do not form the full symmetry group") else: value_change = basis_change - if len(cosets) == 1: + if len(cosets) == 1 or len(ent_dofs_ids) == len(basis_change): sub_mat = value_change else: # check if this should be the cyclic variant diff --git a/test/test_2d_examples_docs.py b/test/test_2d_examples_docs.py index 0a53ae3c..11aa3f1f 100644 --- a/test/test_2d_examples_docs.py +++ b/test/test_2d_examples_docs.py @@ -190,8 +190,6 @@ def construct_nd(tri=None): x = sp.Symbol("x") y = sp.Symbol("y") - # xs = [DOF(L2Pairing(), PointKernel(edge.basis_vectors()[0]))] - # xs = [DOF(L2Pairing(), PointKernel((1,)))] xs = [DOF(L2Pairing(), VectorKernel(1))] dofs = DOFGenerator(xs, S1, S2) @@ -229,6 +227,31 @@ def construct_nd_2nd_kind(tri=None): return ned +def construct_nd2_2nd_kind(tri=None): + if tri is None: + tri = polygon(3) + deg = 2 + edge = tri.edges()[0] + + s_0 = sp.Symbol("s_0") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(s_0*(2*s_0 - 1), symbols=(s_0,)))] + centre = [DOF(L2Pairing(), BarycentricPolynomialKernel(4*s_0*(1 - s_0), symbols=(s_0,)))] + dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] + int_ned = ElementTriple(edge, (PolynomialSpace(deg, set_shape=True), CellHCurl, C0), dofs) + + xs = [immerse(tri, int_ned, TrHCurl)] + edge_dofs = DOFGenerator(xs, C3, S1) + + s_1 = sp.Symbol("s_1") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_0, 1 - s_1], symbols=(s_0, s_1)))] + face_dofs = DOFGenerator(xs, C3, S2) + + nd = PolynomialSpace(deg, set_shape=True) + + ned = ElementTriple(tri, (nd, CellHCurl, C0), [edge_dofs, face_dofs]) + return ned + + def construct_bdm(tri=None): if tri is None: tri = polygon(3) @@ -249,13 +272,32 @@ def construct_bdm(tri=None): return rt +def construct_bdm_bary(tri=None): + if tri is None: + tri = polygon(3) + deg = 1 + edge = tri.edges()[0] + s_0 = sp.Symbol("s_0") + + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(s_0, symbols=(s_0,)))] + dofs = DOFGenerator(xs, S2, S2) + int_rt = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHDiv, C0), dofs) + + xs = [immerse(tri, int_rt, TrHDiv)] + tri_dofs = DOFGenerator(xs, C3, S1) + + nd = PolynomialSpace(deg, set_shape=True) + + rt = ElementTriple(tri, (nd, CellHDiv, C0), [tri_dofs]) + return rt + + def construct_bdm2(tri=None): if tri is None: tri = polygon(3) deg = 2 edge = tri.edges()[0] x = sp.Symbol("x") - y = sp.Symbol("y") xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] @@ -266,18 +308,16 @@ def construct_bdm2(tri=None): xs = [immerse(tri, int_rt, TrHDiv)] tri_dofs = DOFGenerator(xs, C3, S1) - phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] - phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] - phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] - xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y)))] - # DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y))), - # DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y)))] + s_1 = sp.Symbol("s_1") + s_0 = sp.Symbol("s_0") + phi_0 = [1 - s_1, s_0] + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(phi_0, symbols=(s_0, s_1)))] interior = DOFGenerator(xs, C3, S1) space = PolynomialSpace(deg, set_shape=True) bdm2 = ElementTriple(tri, (space, CellHDiv, C0), [tri_dofs, interior]) - breakpoint() + dofs = bdm2.generate() return bdm2 diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index dc5c8a62..78d2826f 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -178,14 +178,6 @@ def construct_tet_rt2(cell=None, perm=None): im_xs = [immerse(cell, face_vec, TrHDiv)] faces = DOFGenerator(im_xs, tet_faces, S1) - # v_0 = np.array(cell.get_node(cell.ordered_vertices()[0], return_coords=True)) - # v_1 = np.array(cell.get_node(cell.ordered_vertices()[1], return_coords=True)) - # v_2 = np.array(cell.get_node(cell.ordered_vertices()[2], return_coords=True)) - # v_3 = np.array(cell.get_node(cell.ordered_vertices()[3], return_coords=True)) - # xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_0)/2)), - # DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2)), - # DOF(L2Pairing(), VectorKernel((v_2 - v_3)/2))] - # interior = DOFGenerator(xs, S1, S4) xs = [DOF(L2Pairing(), VectorKernel(cell.basis_vectors()[0]))] interior = DOFGenerator(xs, cell.basis_group, S4) @@ -194,7 +186,44 @@ def construct_tet_rt2(cell=None, perm=None): return rt2 -def construct_tet_bdm(cell=None, perm=None): +def construct_tet_rt3(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 3 + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M = sp.Matrix([[x, y, z]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + rt_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M + + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + vertex_dofs = [DOF(L2Pairing(), BarycentricPolynomialKernel(2*s_0**2 + 4*s_0*s_1 - 3*s_0 + 2*s_1**2 - 3*s_1 + 1, symbols=(s_0, s_1)))] + edge_dofs = [DOF(L2Pairing(), BarycentricPolynomialKernel(4*s_0*(1 - s_0 - s_1), symbols=(s_0, s_1)))] + dofs = [DOFGenerator(vertex_dofs, C3, S3), DOFGenerator(edge_dofs, C3, S3)] + face_vec = ElementTriple(face, (rt_space, CellHDiv, "C0"), dofs) + + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([1 - s_0 - s_1, sp.Poly(0, s_0)], symbols=(s_0, s_1))), + DOF(L2Pairing(), BarycentricPolynomialKernel([sp.Poly(0, s_0), 1 - s_0 - s_1], symbols=(s_0, s_1)))] + interior = DOFGenerator(xs, C3, S4) + + rt2 = ElementTriple(cell, (rt_space, CellHDiv, "C0"), + [faces, interior]) + return rt2 + +# def test_rt3(): +# rt3 = construct_tet_rt3() +# breakpoint() + + +def construct_tet_bdm_old(cell=None, perm=None): if cell is None: cell = make_tetrahedron() face = cell.d_entities(2, get_class=True)[0] @@ -214,44 +243,64 @@ def construct_tet_bdm(cell=None, perm=None): return bdm +def construct_tet_bdm(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 1 + + space = PolynomialSpace(deg, set_shape=True) + + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(1 - s_0 - s_1, symbols=(s_0, s_1)))] + dofs = DOFGenerator(xs, C3, S2) + face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + bdm = ElementTriple(cell, (space, CellHDiv, "C0"), [faces]) + return bdm + +# def test_compare_bdms(): +# print("NORMAL") +# bdm1 = construct_tet_bdm() +# bdm1.to_fiat() +# print("BARY") +# bdm2 = construct_tet_bdm_bary() +# bdm2.to_fiat() + + def construct_tet_bdm2(cell=None, perm=None): if cell is None: cell = make_tetrahedron() face = cell.d_entities(2, get_class=True)[0] deg = 2 - x = sp.Symbol("x") - y = sp.Symbol("y") - z = sp.Symbol("z") space = PolynomialSpace(deg, set_shape=True) - s = np.sqrt(3) - vertex_basis = (1/9)*(-1 + s*y + (2/3)*y**2) - edge_basis = (4/9)*(1 - 4*s*y) - x**2 + (1/3)*(y**2) - xs = [DOF(L2Pairing(), PolynomialKernel(vertex_basis, symbols=(x, y)))] - xs1 = [DOF(L2Pairing(), PolynomialKernel(edge_basis, symbols=(x, y)))] + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + symbols = (s_0, s_1) + vertex_basis = 2*s_0**2 + 4*s_0*s_1 - 3*s_0 + 2*s_1**2 - 3*s_1 + 1 + edge_basis = 4*s_0*(-s_0 - s_1 + 1) + xs = [DOF(L2Pairing(), PolynomialKernel(vertex_basis, symbols=symbols))] + xs1 = [DOF(L2Pairing(), PolynomialKernel(edge_basis, symbols=symbols))] dofs = [DOFGenerator(xs, C3, S3), DOFGenerator(xs1, C3, S3)] face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) im_xs = [immerse(cell, face_vec, TrHDiv)] faces = DOFGenerator(im_xs, tet_faces, S1) - s2 = np.sqrt(2) - N = {} - N[(1, 2)] = [y/4 + z/4, -x/4 - s2/8, -x/4 - s2/8] - N[(1, 3)] = [-y/4 - s2/8, x/4 + z/4, -y/4 - s2/8] - N[(1, 4)] = [-z/4 - s2/8, -z/4 - s2/8, x/4 + y/4] - N[(2, 3)] = [z/4 - s2/8, -z/4 + s2/8, -x/4 + y/4] - N[(2, 4)] = [y/4 - s2/8, -x/4 + z/4, -y/4 + s2/8] - N[(3, 4)] = [-y/4 + z/4, x/4 - s2/8, -x/4 + s2/8] - xs = [DOF(L2Pairing(), PolynomialKernel(N[phi], symbols=(x, y, z))) for phi in N.keys()] - interior = DOFGenerator(xs, S1, S1) + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + s_2 = sp.Symbol("s_2") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_1 - s_2 + 1, s_0, s_0], symbols=(s_0, s_1, s_2)))] + interior = DOFGenerator(xs, tet_edges, S1) bdm2 = ElementTriple(cell, (space, CellHDiv, "C0"), [faces, interior]) - breakpoint() return bdm2 - def construct_tet_ned(cell=None): deg = 1 tet = make_tetrahedron() @@ -284,7 +333,7 @@ def construct_tet_ned(cell=None): return ElementTriple(tet, (nd_space, CellHCurl, L2), [edge_dofs]) -def construct_tet_ned_2nd_kind(tet=None, perm=None): +def construct_tet_ned_2nd_kind_old(tet=None, perm=None): if tet is None: tet = make_tetrahedron() deg = 1 @@ -299,9 +348,27 @@ def construct_tet_ned_2nd_kind(tet=None, perm=None): int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) xs = [immerse(tet, int_ned1, TrHCurl)] - tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), - Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), - Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) + edge_dofs = DOFGenerator(xs, tet_edges, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs]) + return ned + + +def construct_tet_ned_2nd_kind(tet=None, perm=None): + if tet is None: + tet = make_tetrahedron() + deg = 1 + edge = tet.edges()[0] + + vec_Pd = PolynomialSpace(deg, set_shape=True) + nd_space = vec_Pd + + s_0 = sp.Symbol("s_0") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(1 - s_0, symbols=(s_0,)))] + dofs = DOFGenerator(xs, S2, S2) + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + + xs = [immerse(tet, int_ned1, TrHCurl)] edge_dofs = DOFGenerator(xs, tet_edges, S1) ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs]) @@ -329,99 +396,49 @@ def construct_tet_ned2(tet=None, perm=None): dofs = DOFGenerator(xs, S2, S2) int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) - v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) + # v_0 = np.array(face.get_node(face.ordered_vertices()[0], return_coords=True)) v_1 = np.array(face.get_node(face.ordered_vertices()[1], return_coords=True)) v_2 = np.array(face.get_node(face.ordered_vertices()[2], return_coords=True)) - # xs = [DOF(L2Pairing(), VectorKernel(face.basis_vectors()[1]))] - # # breakpoint() + xs = [DOF(L2Pairing(), VectorKernel((v_2 - v_1)/2))] - print((v_2 - v_1)/2) center_dofs = DOFGenerator(xs, S2, S3) - xs1 = [DOF(L2Pairing(), VectorKernel(face.basis_vectors()[0]))] - print(face.basis_vectors()[0]) - center_dofs1 = DOFGenerator(xs1, face.basis_group, S3) face_vec = ElementTriple(face, (P1, CellHCurl, C0), center_dofs) im_xs = [immerse(tet, face_vec, TrH1)] face_dofs = DOFGenerator(im_xs, tet_faces, S1) - face_vec1 = ElementTriple(face, (P1, CellHCurl, C0), center_dofs1) - im_xs1 = [immerse(tet, face_vec1, TrH1)] - face_dofs1 = DOFGenerator(im_xs1, tet_faces, S1) - - ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [face_dofs]) - ned1 = ElementTriple(tet, (nd_space, CellHCurl, C0), [face_dofs1]) - - dofs = ned.generate() - print("S2") - for d in dofs: - print(d) - pt_dict = d.to_quadrature(1) - for key, val in pt_dict.items(): - print(key, ":", val) - dofs1 = ned1.generate() - print("basis_group") - for d in dofs1: - print(d) - pt_dict = d.to_quadrature(1) - for key, val in pt_dict.items(): - print(key, ":", val) + xs = [immerse(tet, int_ned1, TrHCurl)] tet_edges = PermutationSetRepresentation([Permutation([0, 1, 2, 3]), Permutation([1, 2, 3, 0]), Permutation([2, 3, 0, 1]), Permutation([1, 3, 0, 2]), Permutation([2, 0, 1, 3]), Permutation([3, 0, 1, 2])]) edge_dofs = DOFGenerator(xs, tet_edges, S1) - ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs1]) + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) return ned -def construct_tet_ned3(tet=None, both=False): +def construct_tet_ned_2nd_kind_2(tet=None, both=False): if tet is None: tet = make_tetrahedron() - deg = 3 + deg = 2 edge = tet.edges()[0] face = tet.d_entities(2, get_class=True)[0] - x = sp.Symbol("x") - y = sp.Symbol("y") - z = sp.Symbol("z") - M1 = sp.Matrix([[0, z, -y]]) - M2 = sp.Matrix([[z, 0, -x]]) - M3 = sp.Matrix([[y, -x, 0]]) - - vec_Pd = PolynomialSpace(deg - 1, set_shape=True) - Pd = PolynomialSpace(deg - 1) - nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + vec_Pd = PolynomialSpace(deg, set_shape=True) + nd_space = vec_Pd - xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] - centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + s_0 = sp.Symbol("s_0") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(s_0*(2*s_0 - 1), symbols=(s_0,)))] + centre = [DOF(L2Pairing(), BarycentricPolynomialKernel(4*s_0*(1 - s_0), symbols=(s_0,)))] dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) - phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] - phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] - phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] - xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y), shape=2)), - DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y), shape=2)), - DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y), shape=2))] - # face_vec = DOFGenerator(xs, S1, S1) - face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S1, S1)) - # phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] - # xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2))] - # if both: - # phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] - # xs += [DOF(L2Pairing(), PolynomialKernel(phi1, symbols=(x, y), shape=2))] - # if both: - # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S3)) - # else: - # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S3, S3)) - face_dofs = DOFGenerator([immerse(tet, face_vec, TrHCurl)], tet_faces, S1) - - xs = [DOF(L2Pairing(), VectorKernel([1, 0, 0])), - DOF(L2Pairing(), VectorKernel([0, 1, 0])), - DOF(L2Pairing(), VectorKernel([0, 0, 1]))] - int_dofs = DOFGenerator(xs, S1, S1) - - ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs, int_dofs]) + s_1 = sp.Symbol("s_1") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_0, 1 - s_1], symbols=(s_0, s_1)))] + face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S2)) + + face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) return ned @@ -466,25 +483,32 @@ def test_tet_nd(): nd1.to_fiat() +def construct_V(elem): + nodes = elem.nodes + ref_el = elem.ref_el + entity_ids = elem.entity_ids + poly_set = elem.poly_set + from FIAT.dual_set import DualSet + + dual = DualSet(nodes, ref_el, entity_ids) + + old_coeffs = poly_set.get_coeffs() + dualmat = dual.to_riesz(poly_set) + + shp = dualmat.shape + A = dualmat.reshape((shp[0], -1)) + B = old_coeffs.reshape((shp[0], -1)) + V = np.dot(A, np.transpose(B)) + return V + + def test_tet_nd3(): - nd3 = construct_tet_ned3() - ls = nd3.generate() - # for dof in ls: - # dof_dict = dof.to_quadrature(1) - # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) - # print(dof) - # print("both") - # nd3 = construct_tet_ned3() - # ls1 = nd3.generate() - for dof in ls: - print("A", dof) - dof.to_quadrature(1) - # print("B", dof1) - # dof1.to_quadrature(1) - # print(np.array(list(dof_dict.keys())[0]), list(dof_dict.values())) - # print(dof) - # breakpoint() - nd3.to_fiat() + nd3 = construct_tet_ned_2nd_kind_2() + bdm2 = construct_tet_bdm2() + nd3.to_ufl() + # V_nd = construct_V(nd3) + bdm2.to_ufl() + # V_bdm = construct_V(bdm2) def plot_tet_ned(): diff --git a/test/test_cells.py b/test/test_cells.py index 54a2516f..10ce8316 100644 --- a/test/test_cells.py +++ b/test/test_cells.py @@ -50,12 +50,12 @@ def test_basis_group(C): basis_change = np.matmul(np.linalg.inv(new_bvs), bvs) assert np.allclose(np.array(bv_coords[i]), np.array(np.matmul(basis_change, bv_0))) + def test_cosets(): cell = polygon(3) - g = cell.basis_group.members()[1] + # g = cell.basis_group.members()[1] conj = cell.group.cosets(cell.basis_group) print(conj) - breakpoint() def test_sub_basis_vectors(): diff --git a/test/test_orientations.py b/test/test_orientations.py index b82edbe3..319b7c37 100644 --- a/test/test_orientations.py +++ b/test/test_orientations.py @@ -3,7 +3,7 @@ from fuse import * import numpy as np import sympy as sp -from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt, construct_nd_2nd_kind, construct_bdm, construct_bdm2 +from test_2d_examples_docs import construct_cg3, construct_nd, construct_rt, construct_nd_2nd_kind, construct_nd2_2nd_kind, construct_bdm, construct_bdm2, construct_bdm_bary from test_convert_to_fiat import create_cg1, create_cg2_tri, create_dg1 import os @@ -280,8 +280,10 @@ def test_convergence(elem_gen, elem_code, deg, conv_rate): (construct_nd_2nd_kind, "N2curl", 1, 1.8), (construct_rt2, "RT", 2, 1.8), (construct_bdm, "BDM", 1, 1.8), + (construct_bdm_bary, "BDM", 1, 1.8), (construct_bdm2, "BDM", 2, 2.8), - (construct_nd2, "N1curl", 2, 1.8)]) + (construct_nd2, "N1curl", 2, 1.8), + (construct_nd2_2nd_kind, "N2curl", 2, 2.8),]) def test_convergence_vector(elem_gen, elem_code, deg, conv_rate): cell = polygon(3) elem = elem_gen(cell) From 61785ff0ba7434223e302f18840b51b4ffaf7b47 Mon Sep 17 00:00:00 2001 From: India Marsden Date: Tue, 17 Mar 2026 12:57:10 +0000 Subject: [PATCH 98/98] immerse polynomial vectors --- fuse/dof.py | 23 +++--- test/test_3d_examples_docs.py | 127 +++++++++++++++++++++++++++++++--- test/test_convert_to_fiat.py | 11 ++- 3 files changed, 142 insertions(+), 19 deletions(-) diff --git a/fuse/dof.py b/fuse/dof.py index 0b5dc1cf..3f0b6fbe 100644 --- a/fuse/dof.py +++ b/fuse/dof.py @@ -258,7 +258,9 @@ def evaluate(self, Qpts, bary_pts, Qwts, basis_change, immersed, dim, value_shap comps = [[tuple()] for pt in Qpts] else: comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] - return Qpts, np.array([wt*self(*self.g.permute(pt)) for pt, wt in zip(bary_pts, Qwts)]).astype(np.float64), comps + if not immersed or self.shape == 0: + return Qpts, np.array([wt*self(*self.g.permute(pt)) for pt, wt in zip(bary_pts, Qwts)]).astype(np.float64), [[(i,) for i in range(dim)] for pt in Qpts] + return Qpts, np.array([wt*immersed(self(*self.g.permute(pt))) for pt, wt in zip(bary_pts, Qwts)]).astype(np.float64), comps def _to_dict(self): o_dict = {"fn": self.fn} @@ -315,7 +317,9 @@ def evaluate(self, Qpts, Qwts, basis_change, immersed, dim, value_shape): comps = [[tuple()] for pt in Qpts] else: comps = [[(i,) for v in value_shape for i in range(v)] for pt in Qpts] - return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps + if not immersed or self.shape == 0: + return Qpts, np.array([wt*self(*(np.matmul(pt, basis_change))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps + return Qpts, np.array([wt*immersed(self(*(np.matmul(pt, basis_change)))) for pt, wt in zip(Qpts, Qwts)]).astype(np.float64), comps def _to_dict(self): o_dict = {"fn": self.fn} @@ -422,7 +426,7 @@ def to_quadrature(self, arg_degree, value_shape): else: basis_change = np.eye(dim) - if self.immersed and isinstance(self.kernel, VectorKernel): + if self.immersed and (isinstance(self.kernel, VectorKernel) or isinstance(self.kernel, BarycentricPolynomialKernel) or isinstance(self.kernel, PolynomialKernel)): def immersed(pt): basis = np.array(self.cell_defined_on.basis_vectors()).T basis_coeffs = np.matmul(np.linalg.inv(basis), np.array(pt)) @@ -447,15 +451,18 @@ def immersed(pt): 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)]} pt_dict = {tuple(pt): [(w, c) for w, c in zip(wt, cp)] for pt, wt, cp in zip(pts, new_wts, comps)} - if self.cell_defined_on.dimension >= 2: - print(self) - np.set_printoptions(linewidth=90, precision=4, suppress=True) - for key, val in pt_dict.items(): - print(np.array(key), ":", np.array([v[0] for v in val])) + # if self.cell_defined_on.dimension >= 2: + # print(self) + # np.set_printoptions(linewidth=90, precision=4, suppress=True) + # for key, val in pt_dict.items(): + # print(np.array(key), ":", np.array([v[0] for v in val])) return pt_dict def __repr__(self, fn="v"): diff --git a/test/test_3d_examples_docs.py b/test/test_3d_examples_docs.py index 78d2826f..d1739afd 100644 --- a/test/test_3d_examples_docs.py +++ b/test/test_3d_examples_docs.py @@ -271,6 +271,58 @@ def construct_tet_bdm(cell=None, perm=None): # bdm2.to_fiat() +def construct_tet_bdm2(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 2 + + space = PolynomialSpace(deg, set_shape=True) + + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + symbols = (s_0, s_1) + vertex_basis = 2*s_0**2 + 4*s_0*s_1 - 3*s_0 + 2*s_1**2 - 3*s_1 + 1 + edge_basis = 4*s_0*(-s_0 - s_1 + 1) + xs = [DOF(L2Pairing(), PolynomialKernel(vertex_basis, symbols=symbols))] + xs1 = [DOF(L2Pairing(), PolynomialKernel(edge_basis, symbols=symbols))] + dofs = [DOFGenerator(xs, C3, S3), DOFGenerator(xs1, C3, S3)] + face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + bdm = ElementTriple(cell, (space, CellHDiv, "C0"), [faces]) + return bdm + + +def construct_tet_bdm(cell=None, perm=None): + if cell is None: + cell = make_tetrahedron() + face = cell.d_entities(2, get_class=True)[0] + deg = 1 + + space = PolynomialSpace(deg, set_shape=True) + + s_0 = sp.Symbol("s_0") + s_1 = sp.Symbol("s_1") + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel(1 - s_0 - s_1, symbols=(s_0, s_1)))] + dofs = DOFGenerator(xs, C3, S2) + face_vec = ElementTriple(face, (space, CellHDiv, "C0"), dofs) + im_xs = [immerse(cell, face_vec, TrHDiv)] + faces = DOFGenerator(im_xs, tet_faces, S1) + + bdm = ElementTriple(cell, (space, CellHDiv, "C0"), [faces]) + return bdm + +# def test_compare_bdms(): +# print("NORMAL") +# bdm1 = construct_tet_bdm() +# bdm1.to_fiat() +# print("BARY") +# bdm2 = construct_tet_bdm_bary() +# bdm2.to_fiat() + + def construct_tet_bdm2(cell=None, perm=None): if cell is None: cell = make_tetrahedron() @@ -294,8 +346,14 @@ def construct_tet_bdm2(cell=None, perm=None): s_0 = sp.Symbol("s_0") s_1 = sp.Symbol("s_1") s_2 = sp.Symbol("s_2") - xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_1 - s_2 + 1, s_0, s_0], symbols=(s_0, s_1, s_2)))] - interior = DOFGenerator(xs, tet_edges, S1) + zero = sp.Poly(0, (s_0, s_1, s_2)) + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_1 - s_2 + 1, s_0, s_0], symbols=(s_0, s_1, s_2))), + DOF(L2Pairing(), BarycentricPolynomialKernel([s_1, -s_0 - s_2 + 1, s_1], symbols=(s_0, s_1, s_2))), + DOF(L2Pairing(), BarycentricPolynomialKernel([s_2, s_2, -s_0 - s_1 + 1], symbols=(s_0, s_1, s_2))), + DOF(L2Pairing(), BarycentricPolynomialKernel([-s_1, s_0, zero], symbols=(s_0, s_1, s_2))), + DOF(L2Pairing(), BarycentricPolynomialKernel([-s_2, zero, s_0], symbols=(s_0, s_1, s_2))), + DOF(L2Pairing(), BarycentricPolynomialKernel([zero, - s_2, s_1], symbols=(s_0, s_1, s_2)))] + interior = DOFGenerator(xs, S1, S1) bdm2 = ElementTriple(cell, (space, CellHDiv, "C0"), [faces, interior]) return bdm2 @@ -415,7 +473,6 @@ def construct_tet_ned2(tet=None, perm=None): ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs]) return ned - def construct_tet_ned_2nd_kind_2(tet=None, both=False): if tet is None: tet = make_tetrahedron() @@ -433,7 +490,7 @@ def construct_tet_ned_2nd_kind_2(tet=None, both=False): edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) s_1 = sp.Symbol("s_1") - xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_0, 1 - s_1], symbols=(s_0, s_1)))] + xs = [DOF(L2Pairing(), BarycentricPolynomialKernel([-s_0, -s_1], symbols=(s_0, s_1)))] face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S2)) face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) @@ -442,6 +499,56 @@ def construct_tet_ned_2nd_kind_2(tet=None, both=False): return ned +def construct_tet_ned3(tet=None, both=False): + if tet is None: + tet = make_tetrahedron() + deg = 3 + edge = tet.edges()[0] + face = tet.d_entities(2, get_class=True)[0] + x = sp.Symbol("x") + y = sp.Symbol("y") + z = sp.Symbol("z") + M1 = sp.Matrix([[0, z, -y]]) + M2 = sp.Matrix([[z, 0, -x]]) + M3 = sp.Matrix([[y, -x, 0]]) + + vec_Pd = PolynomialSpace(deg - 1, set_shape=True) + Pd = PolynomialSpace(deg - 1) + nd_space = vec_Pd + (Pd.restrict(deg - 2, deg - 1))*M1 + (Pd.restrict(deg - 2, deg - 1))*M2 + (Pd.restrict(deg - 2, deg - 1))*M3 + + xs = [DOF(L2Pairing(), PolynomialKernel((x/2)*(x + 1), symbols=(x,)))] + centre = [DOF(L2Pairing(), PolynomialKernel((1 - x**2), symbols=(x,)))] + dofs = [DOFGenerator(xs, S2, S2), DOFGenerator(centre, S1, S2)] + int_ned1 = ElementTriple(edge, (PolynomialSpace(1, set_shape=True), CellHCurl, C0), dofs) + edge_dofs = DOFGenerator([immerse(tet, int_ned1, TrHCurl)], tet_edges, S1) + + phi_0 = [-1/6 - (np.sqrt(3)/6)*y, (-np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + + # phi_1 = [-1/6 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6) + (np.sqrt(3)/6)*x] + # phi_2 = [1/3 - (np.sqrt(3)/6)*y, (np.sqrt(3)/6)*x] + xs = [DOF(L2Pairing(), PolynomialKernel(phi_0, symbols=(x, y)))] + # DOF(L2Pairing(), PolynomialKernel(phi_1, symbols=(x, y), shape=2)), + # DOF(L2Pairing(), PolynomialKernel(phi_2, symbols=(x, y), shape=2))] + face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S1)) + # phi0 = [1/3 - (1/2)*x + y/(2*np.sqrt(3)), sp.Poly(0, (x, y))] + # xs = [DOF(L2Pairing(), PolynomialKernel(phi0, symbols=(x, y), shape=2))] + # if both: + # phi1 = [sp.Poly(0, (x, y)), 1/3 - (1/2)*x + y/(2*np.sqrt(3))] + # xs += [DOF(L2Pairing(), PolynomialKernel(phi1, symbols=(x, y), shape=2))] + # if both: + # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, C3, S3)) + # else: + # face_vec = ElementTriple(face, (P1, CellHCurl, C0), DOFGenerator(xs, S3, S3)) + face_dofs = DOFGenerator([immerse(tet, face_vec, TrH1)], tet_faces, S1) + + xs = [DOF(L2Pairing(), VectorKernel([1, 0, 0])), + DOF(L2Pairing(), VectorKernel([0, 1, 0])), + DOF(L2Pairing(), VectorKernel([0, 0, 1]))] + int_dofs = DOFGenerator(xs, S1, S1) + + ned = ElementTriple(tet, (nd_space, CellHCurl, C0), [edge_dofs, face_dofs, int_dofs]) + return ned + def test_plot_tet_ned2(): nd = construct_tet_ned2() # nd.plot(filename="new.png") @@ -503,11 +610,15 @@ def construct_V(elem): def test_tet_nd3(): - nd3 = construct_tet_ned_2nd_kind_2() - bdm2 = construct_tet_bdm2() - nd3.to_ufl() + nd3 = construct_tet_ned3() + nd3.to_fiat() + # nd3 = construct_tet_ned_2nd_kind_2() + # bdm2 = construct_tet_bdm2() + # nd3.to_ufl() + # nd3.to_fiat() # V_nd = construct_V(nd3) - bdm2.to_ufl() + # bdm2.to_ufl() + # bdm2.to_fiat() # V_bdm = construct_V(bdm2) diff --git a/test/test_convert_to_fiat.py b/test/test_convert_to_fiat.py index 96f875ca..07fde7be 100644 --- a/test/test_convert_to_fiat.py +++ b/test/test_convert_to_fiat.py @@ -6,7 +6,7 @@ from sympy.combinatorics import Permutation from FIAT.quadrature_schemes import create_quadrature from test_2d_examples_docs import construct_cg1, construct_nd, construct_rt, construct_cg3, construct_dg0_integral, construct_dg1_integral, construct_dg2_integral -from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_bdm, construct_tet_bdm2, construct_tet_ned2, construct_tet_cg4 +from test_3d_examples_docs import construct_tet_rt, construct_tet_rt2, construct_tet_ned, construct_tet_ned_2nd_kind, construct_tet_ned_2nd_kind_2, construct_tet_bdm, construct_tet_bdm2, construct_tet_ned2, construct_tet_cg4 from test_polynomial_space import flatten from element_examples import CR_n import os @@ -627,6 +627,7 @@ def test_project_3d(elem_gen, elem_code, deg): (construct_tet_ned, "N1curl", 1, 0.8), (construct_tet_ned2, "N1curl", 2, 1.8), (construct_tet_ned_2nd_kind, "N2curl", 1, 1.8), + (construct_tet_ned_2nd_kind_2, "N2curl", 2, 2.8), (construct_tet_bdm, "BDM", 1, 1.8), (construct_tet_bdm2, "BDM", 2, 2.8)]) def test_projection_convergence_3d(elem_gen, elem_code, deg, conv_rate): @@ -699,7 +700,9 @@ def test_const_vec(elem_gen, elem_code, deg, conv_rate): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_ned2, "N1curl", 2), - (construct_tet_rt2, "RT", 2)]) + (construct_tet_rt2, "RT", 2), + (construct_tet_bdm2, "BDM", 2), + (construct_tet_ned_2nd_kind_2, "N2curl", 2)]) def test_linear_vec(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell) @@ -735,7 +738,9 @@ def test_linear_vec(elem_gen, elem_code, deg): @pytest.mark.parametrize("elem_gen,elem_code,deg", [(construct_tet_rt, "RT", 1), (construct_tet_rt2, "RT", 2), (construct_tet_ned, "N1curl", 1), - (construct_tet_ned2, "N1curl", 2)]) + (construct_tet_ned2, "N1curl", 2), + (construct_tet_ned_2nd_kind_2, "N2curl", 2) + ]) def test_vec_two_tet(elem_gen, elem_code, deg): cell = make_tetrahedron() elem = elem_gen(cell)