def argument(self, o): from ufl import split from firedrake import MixedFunctionSpace, FunctionSpace V = o.function_space() if len(V) == 1: # Not on a mixed space, just return ourselves. return o if o in self._arg_cache: return self._arg_cache[o] V_is = V.split() indices = self.blocks[o.number()] try: indices = tuple(indices) nidx = len(indices) except TypeError: # Only one index provided. indices = (indices, ) nidx = 1 if nidx == 1: W = V_is[indices[0]] W = FunctionSpace(W.mesh(), W.ufl_element()) a = (Argument(W, o.number(), part=o.part()), ) else: W = MixedFunctionSpace([V_is[i] for i in indices]) a = split(Argument(W, o.number(), part=o.part())) args = [] for i in range(len(V_is)): if i in indices: c = indices.index(i) a_ = a[c] if len(a_.ufl_shape) == 0: args += [a_] else: args += [a_[j] for j in numpy.ndindex(a_.ufl_shape)] else: args += [ Zero() for j in numpy.ndindex(V_is[i].ufl_element().value_shape()) ] return self._arg_cache.setdefault(o, as_vector(args))
def argument(self, o): from ufl import split from firedrake import MixedFunctionSpace, FunctionSpace V = o.function_space() if len(V) == 1: # Not on a mixed space, just return ourselves. return o if o in self._arg_cache: return self._arg_cache[o] V_is = V.split() indices = self.blocks[o.number()] try: indices = tuple(indices) nidx = len(indices) except TypeError: # Only one index provided. indices = (indices, ) nidx = 1 if nidx == 1: W = V_is[indices[0]] W = FunctionSpace(W.mesh(), W.ufl_element()) a = (Argument(W, o.number(), part=o.part()), ) else: W = MixedFunctionSpace([V_is[i] for i in indices]) a = split(Argument(W, o.number(), part=o.part())) args = [] for i in range(len(V_is)): if i in indices: c = indices.index(i) a_ = a[c] if len(a_.ufl_shape) == 0: args += [a_] else: args += [a_[j] for j in numpy.ndindex(a_.ufl_shape)] else: args += [Zero() for j in numpy.ndindex( V_is[i].ufl_element().value_shape())] return self._arg_cache.setdefault(o, as_vector(args))
def build_connection_to_firedrake(discr, group_nr=None, comm=None): """ Create a connection from a meshmode discretization into firedrake. Create a corresponding "DG" function space and allow for conversion back and forth by resampling at the nodes. :param discr: A :class:`~meshmode.discretization.Discretization` to intialize the connection with :param group_nr: The group number of the discretization to convert. If *None* there must be only one group. The selected group must be of type :class:`~meshmode.discretization.poly_element.\ InterpolatoryQuadratureSimplexElementGroup`. :param comm: Communicator to build a dmplex object on for the created firedrake mesh """ if group_nr is None: if len(discr.groups) != 1: raise ValueError("'group_nr' is *None*, but 'discr' has '%s' " "!= 1 groups." % len(discr.groups)) group_nr = 0 el_group = discr.groups[group_nr] from firedrake.functionspace import FunctionSpace fd_mesh, fd_cell_order, perm2cells = \ export_mesh_to_firedrake(discr.mesh, group_nr, comm) fspace = FunctionSpace(fd_mesh, "DG", el_group.order) # get firedrake unit nodes and map onto meshmode reference element dim = fspace.mesh().topological_dimension() fd_ref_cell_to_mm = get_affine_reference_simplex_mapping(dim, True) fd_unit_nodes = get_finat_element_unit_nodes(fspace.finat_element) fd_unit_nodes = fd_ref_cell_to_mm(fd_unit_nodes) # **_cell_node holds the node nrs in shape *(ncells, nunit_nodes)* fd_cell_node = fspace.cell_node_list # To get the meshmode to firedrake node assocation, we need to handle # local vertex reordering and cell reordering. from pyop2.datatypes import IntType mm2fd_node_mapping = np.ndarray((el_group.nelements, el_group.nunit_dofs), dtype=IntType) for perm, cells in perm2cells.items(): # reordering_arr[i] should be the fd node corresponding to meshmode # node i # # The jth meshmode cell corresponds to the fd_cell_order[j]th # firedrake cell. If *nodeperm* is the permutation of local nodes # applied to the *j*\ th meshmode cell, the firedrake node # fd_cell_node[fd_cell_order[j]][k] corresponds to the # mm_cell_node[j, nodeperm[k]]th meshmode node. # # Note that the permutation on the unit nodes may not be the # same as the permutation on the barycentric coordinates (*perm*). # Importantly, the permutation is derived from getting a flip # matrix from the Firedrake unit nodes, not necessarily the meshmode # unit nodes # flip_mat = get_simplex_element_flip_matrix(el_group.order, fd_unit_nodes, np.argsort(perm)) flip_mat = np.rint(flip_mat).astype(IntType) fd_permuted_cell_node = np.matmul(fd_cell_node[fd_cell_order[cells]], flip_mat.T) mm2fd_node_mapping[cells] = fd_permuted_cell_node assert np.size(np.unique(mm2fd_node_mapping)) == \ np.size(mm2fd_node_mapping), \ "A firedrake node in a 'DG' space got duplicated" return FiredrakeConnection(discr, fspace, mm2fd_node_mapping, group_nr=group_nr)
def test_from_fd_idempotency(ctx_factory, fdrake_mesh, fspace_degree, fspace_type, only_convert_bdy): """ Make sure fd->mm->fd and (fd->)->mm->fd->mm are identity """ # Make a function space and a function with unique values at each node if fspace_type == "scalar": fdrake_fspace = FunctionSpace(fdrake_mesh, "DG", fspace_degree) # Just use the node nr fdrake_unique = Function(fdrake_fspace) fdrake_unique.dat.data[:] = np.arange(fdrake_unique.dat.data.shape[0]) elif fspace_type == "vector": fdrake_fspace = VectorFunctionSpace(fdrake_mesh, "DG", fspace_degree) # use the coordinates xx = SpatialCoordinate(fdrake_fspace.mesh()) fdrake_unique = Function(fdrake_fspace).interpolate(xx) elif fspace_type == "tensor": fdrake_fspace = TensorFunctionSpace(fdrake_mesh, "DG", fspace_degree) # use the coordinates, duplicated into the right tensor shape xx = SpatialCoordinate(fdrake_fspace.mesh()) dim = fdrake_fspace.mesh().geometric_dimension() unique_expr = as_tensor([xx for _ in range(dim)]) fdrake_unique = Function(fdrake_fspace).interpolate(unique_expr) # Make connection cl_ctx = ctx_factory() queue = cl.CommandQueue(cl_ctx) actx = PyOpenCLArrayContext(queue) # If only converting boundary, first go ahead and do one round of # fd->mm->fd. This will zero out any degrees of freedom absent in # the meshmode mesh (because they are not associated to cells # with >= 1 node on the boundary) # # Otherwise, just continue as normal if only_convert_bdy: fdrake_connection = \ build_connection_from_firedrake(actx, fdrake_fspace, restrict_to_boundary="on_boundary") temp = fdrake_connection.from_firedrake(fdrake_unique, actx=actx) fdrake_unique = fdrake_connection.from_meshmode(temp) else: fdrake_connection = build_connection_from_firedrake( actx, fdrake_fspace) # Test for idempotency fd->mm->fd mm_field = fdrake_connection.from_firedrake(fdrake_unique, actx=actx) fdrake_unique_copy = Function(fdrake_fspace) fdrake_connection.from_meshmode(mm_field, out=fdrake_unique_copy) np.testing.assert_allclose(fdrake_unique_copy.dat.data, fdrake_unique.dat.data, atol=CLOSE_ATOL) # Test for idempotency (fd->)mm->fd->mm mm_field_copy = fdrake_connection.from_firedrake(fdrake_unique_copy, actx=actx) if fspace_type == "scalar": np.testing.assert_allclose(actx.to_numpy(mm_field_copy[0]), actx.to_numpy(mm_field[0]), atol=CLOSE_ATOL) else: for dof_arr_cp, dof_arr in zip(mm_field_copy.flatten(), mm_field.flatten()): np.testing.assert_allclose(actx.to_numpy(dof_arr_cp[0]), actx.to_numpy(dof_arr[0]), atol=CLOSE_ATOL)
def test_from_boundary_consistency(ctx_factory, fdrake_mesh, fspace_degree): """ Make basic checks that FiredrakeConnection restricted to cells near the boundary is not doing something obviously wrong, i.e. that the firedrake boundary tags partition the converted meshmode mesh, that the firedrake boundary tags correspond to the same physical regions in the converted meshmode mesh as in the original firedrake mesh, and that each boundary tag is associated to the same number of facets in the converted meshmode mesh as in the original firedrake mesh. """ fdrake_fspace = FunctionSpace(fdrake_mesh, "DG", fspace_degree) cl_ctx = ctx_factory() queue = cl.CommandQueue(cl_ctx) actx = PyOpenCLArrayContext(queue) frombdy_conn = \ build_connection_from_firedrake(actx, fdrake_fspace, restrict_to_boundary="on_boundary") # Ensure the meshmode mesh has one group and make sure both # meshes agree on some basic properties discr = frombdy_conn.discr assert len(discr.mesh.groups) == 1 fdrake_mesh_fspace = fdrake_mesh.coordinates.function_space() fdrake_mesh_order = fdrake_mesh_fspace.finat_element.degree assert discr.mesh.groups[0].dim == fdrake_mesh.topological_dimension() assert discr.mesh.groups[0].order == fdrake_mesh_order # Get the unit vertex indices (in each cell) fdrake_mesh = fdrake_fspace.mesh() cfspace = fdrake_mesh.coordinates.function_space() entity_dofs = cfspace.finat_element.entity_dofs()[0] fdrake_unit_vert_indices = [] for _, local_node_nrs in sorted(entity_dofs.items()): assert len(local_node_nrs) == 1 fdrake_unit_vert_indices.append(local_node_nrs[0]) fdrake_unit_vert_indices = np.array(fdrake_unit_vert_indices) # only look at cells "near" bdy (with >= 1 vertex on) from meshmode.interop.firedrake.connection import _get_cells_to_use cells_near_bdy = _get_cells_to_use(fdrake_mesh, "on_boundary") # get the firedrake vertices of cells near the boundary, # in no particular order fdrake_vert_indices = \ cfspace.cell_node_list[cells_near_bdy, fdrake_unit_vert_indices[:, np.newaxis]] fdrake_vert_indices = np.unique(fdrake_vert_indices) fdrake_verts = fdrake_mesh.coordinates.dat.data[fdrake_vert_indices, ...] if fdrake_mesh.geometric_dimension() == 1: fdrake_verts = fdrake_verts[:, np.newaxis] # Get meshmode vertices (shaped like (dim, nverts)) meshmode_verts = discr.mesh.vertices # Ensure that the vertices of firedrake elements on # the boundary are identical to the resultant meshes' vertices up to # reordering # Nb: I got help on this from stack overflow: # https://stackoverflow.com/questions/38277143/sort-2d-numpy-array-lexicographically # noqa: E501 lex_sorted_mm_verts = meshmode_verts[:, np.lexsort(meshmode_verts)] lex_sorted_fdrake_verts = fdrake_verts[np.lexsort(fdrake_verts.T)] np.testing.assert_allclose(lex_sorted_mm_verts, lex_sorted_fdrake_verts.T, atol=CLOSE_ATOL) # Ensure the discretization and the firedrake function space reference element # agree on some basic properties finat_elt = fdrake_fspace.finat_element assert len(discr.groups) == 1 assert discr.groups[0].order == finat_elt.degree assert discr.groups[0].nunit_dofs == finat_elt.space_dimension()
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, coords_to_adjust=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.coords_to_adjust = coords_to_adjust self.method = method mesh = v_CG1.function_space().mesh() VDG0 = FunctionSpace(mesh, "DG", 0) VCG1 = FunctionSpace(mesh, "CG", 1) if VDG0.extruded: cell = mesh._base_mesh.ufl_cell().cellname() DG1_hori_elt = FiniteElement("DG", cell, 1, variant="equispaced") DG1_vert_elt = FiniteElement("DG", interval, 1, variant="equispaced") DG1_element = TensorProductElement(DG1_hori_elt, DG1_vert_elt) else: cell = mesh.ufl_cell().cellname() DG1_element = FiniteElement("DG", cell, 1, variant="equispaced") VDG1 = FunctionSpace(mesh, DG1_element) self.num_ext = Function(VDG0) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != VCG1: raise NotImplementedError( "This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != VDG1: raise NotImplementedError( "This boundary recovery method requires v_out to be in DG1." ) # check whether mesh is valid if mesh.topological_dimension() == 2: # if mesh is extruded then we're fine, but if not needs to be quads if not VDG0.extruded and mesh.ufl_cell().cellname( ) != 'quadrilateral': raise NotImplementedError( 'For 2D meshes this recovery method requires that elements are quadrilaterals' ) elif mesh.topological_dimension() == 3: # assume that 3D mesh is extruded if mesh._base_mesh.ufl_cell().cellname() != 'quadrilateral': raise NotImplementedError( 'For 3D extruded meshes this recovery method requires a base mesh with quadrilateral elements' ) elif mesh.topological_dimension() != 1: raise NotImplementedError( 'This boundary recovery is implemented only on certain classes of mesh.' ) if coords_to_adjust is None: raise ValueError( 'Need coords_to_adjust field for dynamics boundary methods' ) elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not VDG0.extruded: raise NotImplementedError( 'The physics boundary method only works on extruded meshes' ) # base spaces cell = mesh._base_mesh.ufl_cell().cellname() w_hori = FiniteElement("DG", cell, 0, variant="equispaced") w_vert = FiniteElement("CG", interval, 1, variant="equispaced") # build element theta_element = TensorProductElement(w_hori, w_vert) # spaces Vtheta = FunctionSpace(mesh, theta_element) Vtheta_broken = FunctionSpace(mesh, BrokenElement(theta_element)) if v_CG1.function_space() != Vtheta: raise ValueError( "This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace." ) if v_DG1.function_space() != Vtheta_broken: raise ValueError( "This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace." ) else: raise ValueError( "Boundary method should be a Boundary Method Enum object.") VuDG1 = VectorFunctionSpace(VDG0.mesh(), DG1_element) x = SpatialCoordinate(VDG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes self.act_coords = Function(VuDG1).project(x) # actual coordinates self.eff_coords = Function(VuDG1).project( x) # effective coordinates self.output = Function(VDG1) shapes = { "nDOFs": self.v_DG1.function_space().finat_element.space_dimension(), "dim": np.prod(VuDG1.shape, dtype=int) } num_ext_domain = ("{{[i]: 0 <= i < {nDOFs}}}").format(**shapes) num_ext_instructions = (""" <float64> SUM_EXT = 0 for i SUM_EXT = SUM_EXT + EXT_V1[i] end NUM_EXT[0] = SUM_EXT """) coords_domain = ("{{[i, j, k, ii, jj, kk, ll, mm, iii, kkk]: " "0 <= i < {nDOFs} and " "0 <= j < {nDOFs} and 0 <= k < {dim} and " "0 <= ii < {nDOFs} and 0 <= jj < {nDOFs} and " "0 <= kk < {dim} and 0 <= ll < {dim} and " "0 <= mm < {dim} and 0 <= iii < {nDOFs} and " "0 <= kkk < {dim}}}").format(**shapes) coords_insts = ( """ <float64> sum_V1_ext = 0 <int> index = 100 <float64> dist = 0.0 <float64> max_dist = 0.0 <float64> min_dist = 0.0 """ # only do adjustment in cells with at least one DOF to adjust """ if NUM_EXT[0] > 0 """ # find the maximum distance between DOFs in this cell, to serve as starting point for finding min distances """ for i for j dist = 0.0 for k dist = dist + pow(ACT_COORDS[i,k] - ACT_COORDS[j,k], 2.0) end dist = pow(dist, 0.5) {{id=sqrt_max_dist, dep=*}} max_dist = fmax(dist, max_dist) {{id=max_dist, dep=sqrt_max_dist}} end end """ # loop through cells and find which ones to adjust """ for ii if EXT_V1[ii] > 0.5 """ # find closest interior node """ min_dist = max_dist index = 100 for jj if EXT_V1[jj] < 0.5 dist = 0.0 for kk dist = dist + pow(ACT_COORDS[ii,kk] - ACT_COORDS[jj,kk], 2) end dist = pow(dist, 0.5) if dist <= min_dist index = jj end min_dist = fmin(min_dist, dist) for ll EFF_COORDS[ii,ll] = 0.5 * (ACT_COORDS[ii,ll] + ACT_COORDS[index,ll]) end end end else """ # for DOFs that aren't exterior, use the original coordinates """ for mm EFF_COORDS[ii, mm] = ACT_COORDS[ii, mm] end end end else """ # for interior elements, just use the original coordinates """ for iii for kkk EFF_COORDS[iii, kkk] = ACT_COORDS[iii, kkk] end end end """).format(**shapes) _num_ext_kernel = (num_ext_domain, num_ext_instructions) _eff_coords_kernel = (coords_domain, coords_insts) self.gaussian_elimination_kernel = kernels.GaussianElimination( VDG1) # find number of external DOFs per cell par_loop(_num_ext_kernel, dx, { "NUM_EXT": (self.num_ext, WRITE), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) # find effective coordinates logger.warning( 'Finding effective coordinates for boundary recovery. This could give unexpected results for deformed meshes over very steep topography.' ) par_loop(_eff_coords_kernel, dx, { "EFF_COORDS": (self.eff_coords, WRITE), "ACT_COORDS": (self.act_coords, READ), "NUM_EXT": (self.num_ext, READ), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) elif self.method == Boundary_Method.physics: self.bottom_kernel = kernels.PhysicsRecoveryBottom() self.top_kernel = kernels.PhysicsRecoveryTop()
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, coords_to_adjust=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.coords_to_adjust = coords_to_adjust self.method = method mesh = v_CG1.function_space().mesh() VDG0 = FunctionSpace(mesh, "DG", 0) VCG1 = FunctionSpace(mesh, "CG", 1) VDG1 = FunctionSpace(mesh, "DG", 1) self.num_ext = Function(VDG0) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != VCG1: raise NotImplementedError( "This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != VDG1: raise NotImplementedError( "This boundary recovery method requires v_out to be in DG1." ) # check whether mesh is valid if mesh.topological_dimension() == 2: # if mesh is extruded then we're fine, but if not needs to be quads if not VDG0.extruded and mesh.ufl_cell().cellname( ) != 'quadrilateral': raise NotImplementedError( 'For 2D meshes this recovery method requires that elements are quadrilaterals' ) elif mesh.topological_dimension() == 3: # assume that 3D mesh is extruded if mesh._base_mesh.ufl_cell().cellname() != 'quadrilateral': raise NotImplementedError( 'For 3D extruded meshes this recovery method requires a base mesh with quadrilateral elements' ) elif mesh.topological_dimension() != 1: raise NotImplementedError( 'This boundary recovery is implemented only on certain classes of mesh.' ) if coords_to_adjust is None: raise ValueError( 'Need coords_to_adjust field for dynamics boundary methods' ) elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not VDG0.extruded: raise NotImplementedError( 'The physics boundary method only works on extruded meshes' ) # base spaces cell = mesh._base_mesh.ufl_cell().cellname() w_hori = FiniteElement("DG", cell, 0) w_vert = FiniteElement("CG", interval, 1) # build element theta_element = TensorProductElement(w_hori, w_vert) # spaces Vtheta = FunctionSpace(mesh, theta_element) Vtheta_broken = FunctionSpace(mesh, BrokenElement(theta_element)) if v_CG1.function_space() != Vtheta: raise ValueError( "This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace." ) if v_DG1.function_space() != Vtheta_broken: raise ValueError( "This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace." ) else: raise ValueError( "Boundary method should be a Boundary Method Enum object.") VuDG1 = VectorFunctionSpace(VDG0.mesh(), "DG", 1) x = SpatialCoordinate(VDG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes VuDG1 = VectorFunctionSpace(mesh, "DG", 1) self.act_coords = Function(VuDG1).project(x) # actual coordinates self.eff_coords = Function(VuDG1).project( x) # effective coordinates shapes = { "nDOFs": self.v_DG1.function_space().finat_element.space_dimension(), "dim": np.prod(VuDG1.shape, dtype=int) } num_ext_domain = ("{{[i]: 0 <= i < {nDOFs}}}").format(**shapes) num_ext_instructions = (""" <float64> SUM_EXT = 0 for i SUM_EXT = SUM_EXT + EXT_V1[i] end NUM_EXT[0] = SUM_EXT """) coords_domain = ("{{[i, j, k, ii, jj, kk, ll, mm, iii, kkk]: " "0 <= i < {nDOFs} and " "0 <= j < {nDOFs} and 0 <= k < {dim} and " "0 <= ii < {nDOFs} and 0 <= jj < {nDOFs} and " "0 <= kk < {dim} and 0 <= ll < {dim} and " "0 <= mm < {dim} and 0 <= iii < {nDOFs} and " "0 <= kkk < {dim}}}").format(**shapes) coords_insts = ( """ <float64> sum_V1_ext = 0 <int> index = 100 <float64> dist = 0.0 <float64> max_dist = 0.0 <float64> min_dist = 0.0 """ # only do adjustment in cells with at least one DOF to adjust """ if NUM_EXT[0] > 0 """ # find the maximum distance between DOFs in this cell, to serve as starting point for finding min distances """ for i for j dist = 0.0 for k dist = dist + pow(ACT_COORDS[i,k] - ACT_COORDS[j,k], 2.0) end dist = pow(dist, 0.5) {{id=sqrt_max_dist, dep=*}} max_dist = fmax(dist, max_dist) {{id=max_dist, dep=sqrt_max_dist}} end end """ # loop through cells and find which ones to adjust """ for ii if EXT_V1[ii] > 0.5 """ # find closest interior node """ min_dist = max_dist index = 100 for jj if EXT_V1[jj] < 0.5 dist = 0.0 for kk dist = dist + pow(ACT_COORDS[ii,kk] - ACT_COORDS[jj,kk], 2) end dist = pow(dist, 0.5) if dist <= min_dist index = jj end min_dist = fmin(min_dist, dist) for ll EFF_COORDS[ii,ll] = 0.5 * (ACT_COORDS[ii,ll] + ACT_COORDS[index,ll]) end end end else """ # for DOFs that aren't exterior, use the original coordinates """ for mm EFF_COORDS[ii, mm] = ACT_COORDS[ii, mm] end end end else """ # for interior elements, just use the original coordinates """ for iii for kkk EFF_COORDS[iii, kkk] = ACT_COORDS[iii, kkk] end end end """).format(**shapes) elimin_domain = ( "{{[i, ii_loop, jj_loop, kk, ll_loop, mm, iii_loop, kkk_loop, iiii, iiiii]: " "0 <= i < {nDOFs} and 0 <= ii_loop < {nDOFs} and " "ii_loop + 1 <= jj_loop < {nDOFs} and ii_loop <= kk < {nDOFs} and " "ii_loop + 1 <= ll_loop < {nDOFs} and ii_loop <= mm < {nDOFs} + 1 and " "0 <= iii_loop < {nDOFs} and {nDOFs} - iii_loop <= kkk_loop < {nDOFs} + 1 and " "0 <= iiii < {nDOFs} and 0 <= iiiii < {nDOFs}}}").format( **shapes) elimin_insts = ( """ <int> ii = 0 <int> jj = 0 <int> ll = 0 <int> iii = 0 <int> jjj = 0 <int> i_max = 0 <float64> A_max = 0.0 <float64> temp_f = 0.0 <float64> temp_A = 0.0 <float64> c = 0.0 <float64> f[{nDOFs}] = 0.0 <float64> a[{nDOFs}] = 0.0 <float64> A[{nDOFs},{nDOFs}] = 0.0 """ # We are aiming to find the vector a that solves A*a = f, for matrix A and vector f. # This is done by performing row operations (swapping and scaling) to obtain A in upper diagonal form. # N.B. several for loops must be executed in numerical order (loopy does not necessarily do this). # For these loops we must manually iterate the index. """ if NUM_EXT[0] > 0.0 """ # only do Gaussian elimination for elements with effective coordinates """ for i """ # fill f with the original field values and A with the effective coordinate values """ f[i] = DG1_OLD[i] A[i,0] = 1.0 A[i,1] = EFF_COORDS[i,0] if {nDOFs} > 3 A[i,2] = EFF_COORDS[i,1] A[i,3] = EFF_COORDS[i,0]*EFF_COORDS[i,1] if {nDOFs} > 7 A[i,4] = EFF_COORDS[i,{dim}-1] A[i,5] = EFF_COORDS[i,0]*EFF_COORDS[i,{dim}-1] A[i,6] = EFF_COORDS[i,1]*EFF_COORDS[i,{dim}-1] A[i,7] = EFF_COORDS[i,0]*EFF_COORDS[i,1]*EFF_COORDS[i,{dim}-1] end end end """ # now loop through rows/columns of A """ for ii_loop A_max = fabs(A[ii,ii]) i_max = ii """ # loop to find the largest value in the ith column # set i_max as the index of the row with this largest value. """ jj = ii + 1 for jj_loop if fabs(A[jj,ii]) > A_max i_max = jj end A_max = fmax(A_max, fabs(A[jj,ii])) jj = jj + 1 end """ # if the max value in the ith column isn't in the ith row, we must swap the rows """ if i_max != ii """ # swap the elements of f """ temp_f = f[ii] {{id=set_temp_f, dep=*}} f[ii] = f[i_max] {{id=set_f_imax, dep=set_temp_f}} f[i_max] = temp_f {{id=set_f_ii, dep=set_f_imax}} """ # swap the elements of A # N.B. kk runs from ii to (nDOFs-1) as elements below diagonal should be 0 """ for kk temp_A = A[ii,kk] {{id=set_temp_A, dep=*}} A[ii, kk] = A[i_max, kk] {{id=set_A_ii, dep=set_temp_A}} A[i_max, kk] = temp_A {{id=set_A_imax, dep=set_A_ii}} end end """ # scale the rows below the ith row """ ll = ii + 1 for ll_loop if ll > ii """ # find scaling factor """ c = - A[ll,ii] / A[ii,ii] """ # N.B. mm runs from ii to (nDOFs-1) as elements below diagonal should be 0 """ for mm A[ll, mm] = A[ll, mm] + c * A[ii,mm] end f[ll] = f[ll] + c * f[ii] end ll = ll + 1 end ii = ii + 1 end """ # do back substitution of upper diagonal A to obtain a """ iii = 0 for iii_loop """ # jjj starts at the bottom row and works upwards """ jjj = {nDOFs} - iii - 1 {{id=assign_jjj, dep=*}} a[jjj] = f[jjj] {{id=set_a, dep=assign_jjj}} for kkk_loop a[jjj] = a[jjj] - A[jjj,kkk_loop] * a[kkk_loop] end a[jjj] = a[jjj] / A[jjj,jjj] iii = iii + 1 end """ # Having found a, this gives us the coefficients for the Taylor expansion with the actual coordinates. """ for iiii if {nDOFs} == 2 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] elif {nDOFs} == 4 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] + a[2]*ACT_COORDS[iiii,1] + a[3]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1] elif {nDOFs} == 8 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] + a[2]*ACT_COORDS[iiii,1] + a[3]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1] + a[4]*ACT_COORDS[iiii,{dim}-1] + a[5]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,{dim}-1] + a[6]*ACT_COORDS[iiii,1]*ACT_COORDS[iiii,{dim}-1] + a[7]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1]*ACT_COORDS[iiii,{dim}-1] end end """ # if element is not external, just use old field values. """ else for iiiii DG1[iiiii] = DG1_OLD[iiiii] end end """).format(**shapes) _num_ext_kernel = (num_ext_domain, num_ext_instructions) _eff_coords_kernel = (coords_domain, coords_insts) self._gaussian_elimination_kernel = (elimin_domain, elimin_insts) # find number of external DOFs per cell par_loop(_num_ext_kernel, dx, { "NUM_EXT": (self.num_ext, WRITE), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) # find effective coordinates logger.warning( 'Finding effective coordinates for boundary recovery. This could give unexpected results for deformed meshes over very steep topography.' ) par_loop(_eff_coords_kernel, dx, { "EFF_COORDS": (self.eff_coords, WRITE), "ACT_COORDS": (self.act_coords, READ), "NUM_EXT": (self.num_ext, READ), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) elif self.method == Boundary_Method.physics: top_bottom_domain = ("{[i]: 0 <= i < 1}") bottom_instructions = (""" DG1[0] = 2 * CG1[0] - CG1[1] DG1[1] = CG1[1] """) top_instructions = (""" DG1[0] = CG1[0] DG1[1] = -CG1[0] + 2 * CG1[1] """) self._bottom_kernel = (top_bottom_domain, bottom_instructions) self._top_kernel = (top_bottom_domain, top_instructions)
class ThetaLimiter(object): """ A vertex based limiter for fields in the DG1xCG2 space, i.e. temperature variables. This acts like the vertex-based limiter implemented in Firedrake, but in addition corrects the central nodes to prevent new maxima or minima forming. """ def __init__(self, space): """ Initialise limiter :param space: the space in which theta lies. It should be the DG1xCG2 space. """ self.Vt = FunctionSpace(space.mesh(), BrokenElement(space.ufl_element())) # check this is the right space, currently working for 2D and 3D extruded meshes # check that horizontal degree is 1 and vertical degree is 2 if self.Vt.ufl_element().degree()[0] != 1 or \ self.Vt.ufl_element().degree()[1] != 2: raise ValueError('This is not the right limiter for this space.') if not self.Vt.extruded: raise ValueError('This is not the right limiter for this space.') if self.Vt.mesh().topological_dimension() == 2: # check that continuity of the spaces is correct # this will fail if the space does not use broken elements if self.Vt.ufl_element()._element.sobolev_space()[0].name != 'L2' or \ self.Vt.ufl_element()._element.sobolev_space()[1].name != 'H1': raise ValueError( 'This is not the right limiter for this space.') elif self.Vt.mesh().topological_dimension() == 3: # check that continuity of the spaces is correct # this will fail if the space does not use broken elements if self.Vt.ufl_element()._element.sobolev_space()[0].name != 'L2' or \ self.Vt.ufl_element()._element.sobolev_space()[2].name != 'H1': raise ValueError( 'This is not the right limiter for this space.') else: raise ValueError('This is not the right limiter for this space.') self.DG1 = FunctionSpace(self.Vt.mesh(), 'DG', 1) # space with only vertex DOFs self.vertex_limiter = VertexBasedLimiter(self.DG1) self.theta_hat = Function( self.DG1) # theta function with only vertex DOFs self.theta_old = Function(self.Vt) self.w = Function(self.Vt) self.result = Function(self.Vt) shapes = { 'nDOFs': self.Vt.finat_element.space_dimension(), 'nDOFs_base': int(self.Vt.finat_element.space_dimension() / 3) } averager_domain = "{{[i]: 0 <= i < {nDOFs}}}".format(**shapes) theta_domain = "{{[i,j]: 0 <= i < {nDOFs_base} and 0 <= j < 2}}".format( **shapes) average_instructions = (""" for i vo[i] = vo[i] + v[i] / w[i] end """) weight_instructions = (""" for i w[i] = w[i] + 1.0 end """) copy_into_DG1_instrs = (""" for i for j theta_hat[i*2+j] = theta[i*3+j] end end """) copy_from_DG1_instrs = (""" <float64> max_value = 0.0 <float64> min_value = 0.0 for i for j theta[i*3+j] = theta_hat[i*2+j] end max_value = fmax(theta_hat[i*2], theta_hat[i*2+1]) min_value = fmin(theta_hat[i*2], theta_hat[i*2+1]) if theta_old[i*3+2] > max_value theta[i*3+2] = 0.5 * (theta_hat[i*2] + theta_hat[i*2+1]) elif theta_old[i*3+2] < min_value theta[i*3+2] = 0.5 * (theta_hat[i*2] + theta_hat[i*2+1]) else theta[i*3+2] = theta_old[i*3+2] end end """) self._average_kernel = (averager_domain, average_instructions) _weight_kernel = (averager_domain, weight_instructions) self._copy_into_DG1_kernel = (theta_domain, copy_into_DG1_instrs) self._copy_from_DG1_kernel = (theta_domain, copy_from_DG1_instrs) par_loop(_weight_kernel, dx, {"w": (self.w, INC)}, is_loopy_kernel=True) def copy_vertex_values(self, field): """ Copies the vertex values from temperature space to DG1 space which only has vertices. """ par_loop(self._copy_into_DG1_kernel, dx, { "theta": (field, READ), "theta_hat": (self.theta_hat, WRITE) }, is_loopy_kernel=True) def copy_vertex_values_back(self, field): """ Copies the vertex values back from the DG1 space to the original temperature space, and checks that the midpoint values are within the minimum and maximum at the adjacent vertices. If outside of the minimum and maximum, correct the values to be the average. """ par_loop(self._copy_from_DG1_kernel, dx, { "theta": (field, WRITE), "theta_hat": (self.theta_hat, READ), "theta_old": (self.theta_old, READ) }, is_loopy_kernel=True) def apply(self, field): """ The application of the limiter to the theta-space field. """ assert field.function_space() == self.Vt, \ 'Given field does not belong to this objects function space' self.theta_old.assign(field) self.copy_vertex_values(field) self.vertex_limiter.apply(self.theta_hat) self.copy_vertex_values_back(field)
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, eff_coords=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.eff_coords = eff_coords self.method = method mesh = v_CG1.function_space().mesh() DG0 = FunctionSpace(mesh, "DG", 0) CG1 = FunctionSpace(mesh, "CG", 1) if DG0.extruded: cell = mesh._base_mesh.ufl_cell().cellname() DG1_hori_elt = FiniteElement("DG", cell, 1, variant="equispaced") DG1_vert_elt = FiniteElement("DG", interval, 1, variant="equispaced") DG1_element = TensorProductElement(DG1_hori_elt, DG1_vert_elt) else: cell = mesh.ufl_cell().cellname() DG1_element = FiniteElement("DG", cell, 1, variant="equispaced") DG1 = FunctionSpace(mesh, DG1_element) self.num_ext = find_domain_boundaries(mesh) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != CG1: raise NotImplementedError( "This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != DG1: raise NotImplementedError( "This boundary recovery method requires v_out to be in DG1." ) if eff_coords is None: raise ValueError( 'Need eff_coords field for dynamics boundary methods') elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not DG0.extruded: raise NotImplementedError( 'The physics boundary method only works on extruded meshes' ) # base spaces cell = mesh._base_mesh.ufl_cell().cellname() w_hori = FiniteElement("DG", cell, 0, variant="equispaced") w_vert = FiniteElement("CG", interval, 1, variant="equispaced") # build element theta_element = TensorProductElement(w_hori, w_vert) # spaces Vtheta = FunctionSpace(mesh, theta_element) Vtheta_broken = FunctionSpace(mesh, BrokenElement(theta_element)) if v_CG1.function_space() != Vtheta: raise ValueError( "This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace." ) if v_DG1.function_space() != Vtheta_broken: raise ValueError( "This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace." ) else: raise ValueError( "Boundary method should be a Boundary Method Enum object.") vec_DG1 = VectorFunctionSpace(DG0.mesh(), DG1_element) x = SpatialCoordinate(DG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes self.act_coords = Function(vec_DG1).project( x) # actual coordinates self.eff_coords = eff_coords # effective coordinates self.output = Function(DG1) self.on_exterior = find_domain_boundaries(mesh) self.gaussian_elimination_kernel = kernels.GaussianElimination(DG1) elif self.method == Boundary_Method.physics: self.bottom_kernel = kernels.PhysicsRecoveryBottom() self.top_kernel = kernels.PhysicsRecoveryTop()
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, eff_coords=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.eff_coords = eff_coords self.method = method mesh = v_CG1.function_space().mesh() DG0 = FunctionSpace(mesh, "DG", 0) CG1 = FunctionSpace(mesh, "CG", 1) if DG0.extruded: cell = mesh._base_mesh.ufl_cell().cellname() DG1_hori_elt = FiniteElement("DG", cell, 1, variant="equispaced") DG1_vert_elt = FiniteElement("DG", interval, 1, variant="equispaced") DG1_element = TensorProductElement(DG1_hori_elt, DG1_vert_elt) else: cell = mesh.ufl_cell().cellname() DG1_element = FiniteElement("DG", cell, 1, variant="equispaced") DG1 = FunctionSpace(mesh, DG1_element) self.num_ext = find_domain_boundaries(mesh) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != CG1: raise NotImplementedError("This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != DG1: raise NotImplementedError("This boundary recovery method requires v_out to be in DG1.") if eff_coords is None: raise ValueError('Need eff_coords field for dynamics boundary methods') elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not DG0.extruded: raise NotImplementedError('The physics boundary method only works on extruded meshes') # check that function spaces are valid sub_elements = v_CG1.function_space().ufl_element().sub_elements() if (sub_elements[0].family() not in ['Discontinuous Lagrange', 'DQ'] or sub_elements[1].family() != 'Lagrange' or v_CG1.function_space().ufl_element().degree() != (0, 1)): raise ValueError("This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace.") brok_elt = v_DG1.function_space().ufl_element() if (brok_elt.degree() != (0, 1) or (type(brok_elt) is not BrokenElement and (brok_elt.sub_elements[0].family() not in ['Discontinuous Lagrange', 'DQ'] or brok_elt.sub_elements[1].family() != 'Discontinuous Lagrange'))): raise ValueError("This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace.") else: raise ValueError("Boundary method should be a Boundary Method Enum object.") vec_DG1 = VectorFunctionSpace(DG0.mesh(), DG1_element) x = SpatialCoordinate(DG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes self.act_coords = Function(vec_DG1).project(x) # actual coordinates self.eff_coords = eff_coords # effective coordinates self.output = Function(DG1) self.on_exterior = find_domain_boundaries(mesh) self.gaussian_elimination_kernel = kernels.GaussianElimination(DG1) elif self.method == Boundary_Method.physics: self.bottom_kernel = kernels.PhysicsRecoveryBottom() self.top_kernel = kernels.PhysicsRecoveryTop()