def mv_normal(dd, ambient_dim, dim=None): """Exterior unit normal as a :class:`~pymbolic.geometric_algebra.MultiVector`.""" dd = as_dofdesc(dd) if not dd.is_trace(): raise ValueError("may only request normals on boundaries") if dim is None: dim = ambient_dim - 1 # NOTE: Don't be tempted to add a sign here. As it is, it produces # exterior normals for positively oriented curves. pder = pseudoscalar(ambient_dim, dim, dd=dd) \ / area_element(ambient_dim, dim, dd=dd) # Dorst Section 3.7.2 mv = pder << pder.I.inv() if dim == 0: # NOTE: when the mesh is 0D, we do not have a clear notion of # `exterior normal`, so we just take the tangent of the 1D element, # project it to the element faces and make it signed tangent = parametrization_derivative(ambient_dim, dim=1, dd=DD_VOLUME) tangent = tangent / sqrt(tangent.norm_squared()) from grudge.symbolic.operators import project project = project(DD_VOLUME, dd) mv = MultiVector( np.array( [mv.as_scalar() * project(t) for t in tangent.as_vector()])) return cse(mv, "normal", cse_scope.DISCRETIZATION)
def map_nabla(self, expr): from pytools.obj_array import make_obj_array return MultiVector( make_obj_array([ prim.NablaComponent(axis, expr.nabla_id) for axis in range(self.ambient_dim) ]))
def parametrization_derivative(actx: ArrayContext, dcoll: DiscretizationCollection, dd) -> MultiVector: r"""Computes the product of forward metric derivatives spanning the tangent space with topological dimension *dim*. :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one. Defaults to the base volume discretization. :returns: a :class:`pymbolic.geometric_algebra.MultiVector` containing the product of metric derivatives. """ if dd is None: dd = DD_VOLUME dim = dcoll.discr_from_dd(dd).dim if dim == 0: from pymbolic.geometric_algebra import get_euclidean_space return MultiVector(_signed_face_ones(actx, dcoll, dd), space=get_euclidean_space(dcoll.ambient_dim)) from pytools import product return product( forward_metric_derivative_mv(actx, dcoll, rst_axis, dd) for rst_axis in range(dim))
def dnabla(self, ambient_dim): from pymbolic.geometric_algebra import MultiVector from pytools.obj_array import make_obj_array return MultiVector( make_obj_array([ NablaComponent(axis, self.my_id) for axis in range(ambient_dim) ]))
def face_normal(face: Face, normalize=True) -> np.ndarray: """ .. versionadded :: 2021.2.1 """ volume_vertices = unit_vertices_for_shape(face.volume_shape) face_vertices = volume_vertices[:, face.volume_vertex_indices] if face.dim == 0: # FIXME Grrrr. Hardcoded special case. Got a better idea? (fv,), = face_vertices return np.array([np.sign(fv)]) # Compute the outer product of the vectors spanning the surface, obtaining # the surface pseudoscalar. from pymbolic.geometric_algebra import MultiVector from operator import xor as outerprod from functools import reduce surface_ps = reduce(outerprod, [ MultiVector(face_vertices[:, i+1] - face_vertices[:, 0]) for i in range(face.dim)]) if normalize: surface_ps = surface_ps / np.sqrt(surface_ps.norm_squared()) # Compute the normal as the dual of the surface pseudoscalar. return surface_ps.dual().as_vector()
def outprod_with_unit(i, at): unit_vec = np.zeros(dim) unit_vec[i] = 1 vecs = par_vecs[:] vecs[at] = MultiVector(unit_vec) return outerprod(vecs)
def det(v): nnodes = v[0].shape[0] det_v = np.empty(nnodes) for i in range(nnodes): outer_product = reduce(xor, [MultiVector(x[i, :].T) for x in v]) det_v[i] = abs((outer_product.I | outer_product).as_scalar()) return det_v
def mv_normal( actx: ArrayContext, dcoll: DiscretizationCollection, dd, ) -> MultiVector: """Exterior unit normal as a :class:`~pymbolic.geometric_algebra.MultiVector`. This supports both volume discretizations (where ambient == topological dimension) and surface discretizations (where ambient == topological dimension + 1). In the latter case, extra processing ensures that the returned normal is in the local tangent space of the element at the point where the normal is being evaluated. :arg dd: a :class:`~grudge.dof_desc.DOFDesc` as the surface discretization. :returns: a :class:`~pymbolic.geometric_algebra.MultiVector` containing the unit normals. """ import grudge.dof_desc as dof_desc dd = dof_desc.as_dofdesc(dd) dim = dcoll.discr_from_dd(dd).dim ambient_dim = dcoll.ambient_dim if dim == ambient_dim: raise ValueError( "may only request normals on domains whose topological " f"dimension ({dim}) differs from " f"their ambient dimension ({ambient_dim})") if dim == ambient_dim - 1: return rel_mv_normal(actx, dcoll, dd=dd) # NOTE: In the case of (d - 2)-dimensional curves, we don't really have # enough information on the face to decide what an "exterior face normal" # is (e.g the "normal" to a 1D curve in 3D space is actually a # "normal plane") # # The trick done here is that we take the surface normal, move it to the # face and then take a cross product with the face tangent to get the # correct exterior face normal vector. assert dim == ambient_dim - 2 from grudge.op import project import grudge.dof_desc as dof_desc volm_normal = MultiVector( project( dcoll, dof_desc.DD_VOLUME, dd, rel_mv_normal(actx, dcoll, dd=dof_desc.DD_VOLUME).as_vector(dtype=object))) pder = pseudoscalar(actx, dcoll, dd=dd) mv = -(volm_normal ^ pder) << volm_normal.I.inv() return mv / actx.np.sqrt(mv.norm_squared())
def parametrization_derivative(ambient_dim, dim=None, dd=None): if dim is None: dim = ambient_dim if dim == 0: return MultiVector(np.array([_SignedFaceOnes(dd)])) from pytools import product return product( forward_metric_derivative_mv(ambient_dim, rst_axis, dd) for rst_axis in range(dim))
def dd_axis(axis, ambient_dim, operand): """Return the derivative along (XYZ) axis *axis* (in *ambient_dim*-dimensional space) of *operand*. """ from pytools.obj_array import is_obj_array, with_object_array_or_scalar if is_obj_array(operand): def dd_axis_comp(operand_i): return dd_axis(axis, ambient_dim, operand_i) return with_object_array_or_scalar(dd_axis_comp, operand) d = Derivative() unit_vector = np.zeros(ambient_dim) unit_vector[axis] = 1 unit_mvector = MultiVector(unit_vector) return d.resolve( (unit_mvector.scalar_product(d.dnabla(ambient_dim))) * d(operand))
def parametrization_derivative(ambient_dim, dim=None, dd=None): if dim is None: dim = ambient_dim if dim == 0: from pymbolic.geometric_algebra import get_euclidean_space return MultiVector(_SignedFaceOnes(dd), space=get_euclidean_space(ambient_dim)) from pytools import product return product( forward_metric_derivative_mv(ambient_dim, rst_axis, dd) for rst_axis in range(dim))
def find_volume_mesh_element_group_orientation(vertices, grp): """Return a positive floating point number for each positively oriented element, and a negative floating point number for each negatively oriented element. """ from meshmode.mesh import SimplexElementGroup if not isinstance(grp, SimplexElementGroup): raise NotImplementedError( "finding element orientations " "only supported on " "exclusively SimplexElementGroup-based meshes") # (ambient_dim, nelements, nvertices) my_vertices = vertices[:, grp.vertex_indices] # (ambient_dim, nelements, nspan_vectors) spanning_vectors = ( my_vertices[:, :, 1:] - my_vertices[:, :, 0][:, :, np.newaxis]) ambient_dim = spanning_vectors.shape[0] nspan_vectors = spanning_vectors.shape[-1] if ambient_dim != grp.dim: raise ValueError("can only find orientation of volume meshes") spanning_object_array = np.empty( (nspan_vectors, ambient_dim), dtype=np.object) for ispan in range(nspan_vectors): for idim in range(ambient_dim): spanning_object_array[ispan, idim] = \ spanning_vectors[idim, :, ispan] from pymbolic.geometric_algebra import MultiVector mvs = [MultiVector(vec) for vec in spanning_object_array] from operator import xor outer_prod = -reduce(xor, mvs) if grp.dim == 1: # FIXME: This is a little weird. outer_prod = -outer_prod return (outer_prod.I | outer_prod).as_scalar()
def forward_metric_derivative_mv(actx: ArrayContext, dcoll: DiscretizationCollection, rst_axis, dd=None) -> MultiVector: r"""Computes a :class:`pymbolic.geometric_algebra.MultiVector` containing the forward metric derivatives of each physical coordinate. :arg rst_axis: a :class:`tuple` of tuples indicating indices of coordinate axes of the reference element to the number of derivatives which will be taken. :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one. Defaults to the base volume discretization. :returns: a :class:`pymbolic.geometric_algebra.MultiVector` containing the forward metric derivatives in each physical coordinate. """ return MultiVector( forward_metric_derivative_vector(actx, dcoll, rst_axis, dd=dd))
def _normal(): dim = dcoll.discr_from_dd(dd).dim ambient_dim = dcoll.ambient_dim if dim == ambient_dim: raise ValueError( "may only request normals on domains whose topological " f"dimension ({dim}) differs from " f"their ambient dimension ({ambient_dim})") if dim == ambient_dim - 1: result = rel_mv_normal(actx, dcoll, dd=dd) else: # NOTE: In the case of (d - 2)-dimensional curves, we don't really have # enough information on the face to decide what an "exterior face normal" # is (e.g the "normal" to a 1D curve in 3D space is actually a # "normal plane") # # The trick done here is that we take the surface normal, move it to the # face and then take a cross product with the face tangent to get the # correct exterior face normal vector. assert dim == ambient_dim - 2 from grudge.op import project volm_normal = MultiVector( project( dcoll, dof_desc.DD_VOLUME, dd, rel_mv_normal( actx, dcoll, dd=dof_desc.DD_VOLUME).as_vector(dtype=object))) pder = pseudoscalar(actx, dcoll, dd=dd) mv = -(volm_normal ^ pder) << volm_normal.I.inv() result = mv / actx.np.sqrt(mv.norm_squared()) if _use_geoderiv_connection: result = dcoll._base_to_geoderiv_connection(dd)(result) return freeze(result, actx)
def map_multivector_variable(self, expr): from pymbolic.primitives import make_sym_vector return MultiVector( make_sym_vector(expr.name, self.ambient_dim, var_factory=type(expr)))
def test_geometric_algebra(dims): pytest.importorskip("numpy") import numpy as np from pymbolic.geometric_algebra import MultiVector as MV # noqa vec1 = MV(np.random.randn(dims)) vec2 = MV(np.random.randn(dims)) vec3 = MV(np.random.randn(dims)) vec4 = MV(np.random.randn(dims)) vec5 = MV(np.random.randn(dims)) # Fundamental identity assert ((vec1 ^ vec2) + (vec1 | vec2)).close_to(vec1*vec2) # Antisymmetry assert (vec1 ^ vec2 ^ vec3).close_to(- vec2 ^ vec1 ^ vec3) vecs = [vec1, vec2, vec3, vec4, vec5] if len(vecs) > dims: from operator import xor as outer assert reduce(outer, vecs).close_to(0) assert (vec1.inv()*vec1).close_to(1) assert (vec1*vec1.inv()).close_to(1) assert ((1/vec1)*vec1).close_to(1) assert (vec1/vec1).close_to(1) for a, b, c in [ (vec1, vec2, vec3), (vec1*vec2, vec3, vec4), (vec1, vec2*vec3, vec4), (vec1, vec2, vec3*vec4), (vec1, vec2, vec3*vec4*vec5), (vec1, vec2*vec1, vec3*vec4*vec5), ]: # Associativity assert ((a*b)*c).close_to(a*(b*c)) assert ((a ^ b) ^ c).close_to(a ^ (b ^ c)) # The inner product is not associative. # scalar product assert ((c*b).project(0)) .close_to(b.scalar_product(c)) assert ((c.rev()*b).project(0)) .close_to(b.rev().scalar_product(c)) assert ((b.rev()*b).project(0)) .close_to(b.norm_squared()) assert b.norm_squared() >= 0 assert c.norm_squared() >= 0 # Cauchy's inequality assert b.scalar_product(c) <= abs(b)*abs(c) + 1e-13 # contractions # (3.18) in [DFM] assert abs(b.scalar_product(a ^ c) - (b >> a).scalar_product(c)) < 1e-13 # duality, (3.20) in [DFM] assert ((a ^ b) << c) .close_to(a << (b << c)) # two definitions of the dual agree: (1.2.26) in [HS] # and (sec 3.5.3) in [DFW] assert (c << c.I.rev()).close_to(c | c.I.rev()) # inverse for div in list(b.gen_blades()) + [vec1, vec1.I]: assert (div.inv()*div).close_to(1) assert (div*div.inv()).close_to(1) assert ((1/div)*div).close_to(1) assert (div/div).close_to(1) assert ((c/div)*div).close_to(c) assert ((c*div)/div).close_to(c) # reverse properties (Sec 2.9.5 [DFM]) assert c.rev().rev() == c assert (b ^ c).rev() .close_to((c.rev() ^ b.rev())) # dual properties # (1.2.26) in [HS] assert c.dual() .close_to(c | c.I.rev()) assert c.dual() .close_to(c*c.I.rev()) # involution properties (Sec 2.9.5 DFW) assert c.invol().invol() == c assert (b ^ c).invol() .close_to((b.invol() ^ c.invol())) # commutator properties # Jacobi identity (1.1.56c) in [HS] or (8.2) in [DFW] assert (a.x(b.x(c)) + b.x(c.x(a)) + c.x(a.x(b))).close_to(0) # (1.57) in [HS] assert a.x(b*c) .close_to(a.x(b)*c + b*a.x(c))
def forward_metric_derivative_mv(ambient_dim, rst_axis, dd=None): return MultiVector( forward_metric_derivative_vector(ambient_dim, rst_axis, dd=dd))
def mv_nodes(ambient_dim, dd=None): return MultiVector(nodes(ambient_dim, dd))
def make_sym_mv(name, dim, var_factory=None): return MultiVector(make_sym_array(name, dim, var_factory))
def test_geometric_algebra(dims): pytest.importorskip("numpy") import numpy as np from pymbolic.geometric_algebra import MultiVector as MV # noqa vec1 = MV(np.random.randn(dims)) vec2 = MV(np.random.randn(dims)) vec3 = MV(np.random.randn(dims)) vec4 = MV(np.random.randn(dims)) vec5 = MV(np.random.randn(dims)) # Fundamental identity assert ((vec1 ^ vec2) + (vec1 | vec2)).close_to(vec1 * vec2) # Antisymmetry assert (vec1 ^ vec2 ^ vec3).close_to(-vec2 ^ vec1 ^ vec3) vecs = [vec1, vec2, vec3, vec4, vec5] if len(vecs) > dims: from operator import xor as outer assert reduce(outer, vecs).close_to(0) assert (vec1.inv() * vec1).close_to(1) assert (vec1 * vec1.inv()).close_to(1) assert ((1 / vec1) * vec1).close_to(1) assert (vec1 / vec1).close_to(1) for a, b, c in [ (vec1, vec2, vec3), (vec1 * vec2, vec3, vec4), (vec1, vec2 * vec3, vec4), (vec1, vec2, vec3 * vec4), (vec1, vec2, vec3 * vec4 * vec5), (vec1, vec2 * vec1, vec3 * vec4 * vec5), ]: # Associativity assert ((a * b) * c).close_to(a * (b * c)) assert ((a ^ b) ^ c).close_to(a ^ (b ^ c)) # The inner product is not associative. # scalar product assert ((c * b).project(0)).close_to(b.scalar_product(c)) assert ((c.rev() * b).project(0)).close_to(b.rev().scalar_product(c)) assert ((b.rev() * b).project(0)).close_to(b.norm_squared()) assert b.norm_squared() >= 0 assert c.norm_squared() >= 0 # Cauchy's inequality assert b.scalar_product(c) <= abs(b) * abs(c) + 1e-13 # contractions # (3.18) in [DFM] assert abs(b.scalar_product(a ^ c) - (b >> a).scalar_product(c)) < 1e-13 # duality, (3.20) in [DFM] assert ((a ^ b) << c).close_to(a << (b << c)) # two definitions of the dual agree: (1.2.26) in [HS] # and (sec 3.5.3) in [DFW] assert (c << c.I.rev()).close_to(c | c.I.rev()) # inverse for div in list(b.gen_blades()) + [vec1, vec1.I]: assert (div.inv() * div).close_to(1) assert (div * div.inv()).close_to(1) assert ((1 / div) * div).close_to(1) assert (div / div).close_to(1) assert ((c / div) * div).close_to(c) assert ((c * div) / div).close_to(c) # reverse properties (Sec 2.9.5 [DFM]) assert c.rev().rev() == c assert (b ^ c).rev().close_to((c.rev() ^ b.rev())) # dual properties # (1.2.26) in [HS] assert c.dual().close_to(c | c.I.rev()) assert c.dual().close_to(c * c.I.rev()) # involution properties (Sec 2.9.5 DFW) assert c.invol().invol() == c assert (b ^ c).invol().close_to((b.invol() ^ c.invol())) # commutator properties # Jacobi identity (1.1.56c) in [HS] or (8.2) in [DFW] assert (a.x(b.x(c)) + b.x(c.x(a)) + c.x(a.x(b))).close_to(0) # (1.57) in [HS] assert a.x(b * c).close_to(a.x(b) * c + b * a.x(c))
def make_common_subexpression(field, prefix=None, scope=None): """Wrap *field* in a :class:`CommonSubexpression` with *prefix*. If *field* is a :mod:`numpy` object array, each individual entry is instead wrapped. If *field* is a :class:`pymbolic.geometric_algebra.MultiVector`, each coefficient is individually wrapped. See :class:`CommonSubexpression` for the meaning of *prefix* and *scope*. """ if isinstance(field, CommonSubexpression) and (scope is None or scope == cse_scope.EVALUATION or field.scope == scope): # Don't re-wrap return field try: from pytools.obj_array import log_shape except ImportError: have_obj_array = False else: have_obj_array = True if have_obj_array: ls = log_shape(field) from pymbolic.geometric_algebra import MultiVector if isinstance(field, MultiVector): new_data = {} for bits, coeff in six.iteritems(field.data): if prefix is not None: blade_str = field.space.blade_bits_to_str(bits, "") component_prefix = prefix + "_" + blade_str else: component_prefix = None new_data[bits] = make_common_subexpression(coeff, component_prefix, scope) return MultiVector(new_data, field.space) elif have_obj_array and ls != (): from pytools import indices_in_shape result = numpy.zeros(ls, dtype=object) for i in indices_in_shape(ls): if prefix is not None: component_prefix = prefix + "_".join(str(i_i) for i_i in i) else: component_prefix = None if is_constant(field[i]): result[i] = field[i] else: result[i] = make_common_subexpression(field[i], component_prefix, scope) return result else: if is_constant(field): return field else: return CommonSubexpression(field, prefix, scope)