diff --git a/grudge/dt_finding.py b/grudge/dt_finding.py deleted file mode 100644 index fd5a3d8e2ddcc2d13170182656eb59d40a23b798..0000000000000000000000000000000000000000 --- a/grudge/dt_finding.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Helpers for estimating a stable time step.""" - -__copyright__ = "Copyright (C) 2015 Andreas Kloeckner" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -from pytools import memoize_on_first_arg -from meshmode.discretization.poly_element import PolynomialWarpAndBlendElementGroup -import numpy.linalg as la - - -class WarpAndBlendTimestepInfo: - @staticmethod - def dt_non_geometric_factor(discr, grp): - if grp.dim == 1: - if grp.order == 0: - return 1 - else: - unodes = grp.unit_nodes - return la.norm(unodes[0] - unodes[1]) * 0.85 - else: - unodes = grp.unit_nodes - vertex_indices = grp.vertex_indices() - return 2 / 3 * \ - min(min(min( - la.norm(unodes[face_node_index]-unodes[vertex_index]) - for vertex_index in vertex_indices - if vertex_index != face_node_index) - for face_node_index in face_indices) - for face_indices in self.face_indices()) - - @staticmethod - def dt_geometric_factor(discr, grp): - if grp.dim == 1: - return abs(el.map.jacobian()) - - elif grp.dim == 2: - area = abs(2 * el.map.jacobian()) - semiperimeter = sum(la.norm(vertices[vi1] - vertices[vi2]) - for vi1, vi2 in [(0, 1), (1, 2), (2, 0)])/2 - return area / semiperimeter - - elif grp.dim == 3: - result = abs(el.map.jacobian())/max(abs(fj) for fj in el.face_jacobians) - if grp.order in [1, 2]: - from warnings import warn - warn("cowardly halving timestep for order 1 and 2 tets " - "to avoid CFL issues") - result /= 2 - - return result - - else: - raise NotImplementedError("cannot estimate timestep for " - "%d-dimensional elements" % grp.dim) - - -_GROUP_TYPE_TO_INFO = { - PolynomialWarpAndBlendElementGroup: WarpAndBlendTimestepInfo - } - - -@memoize_on_first_arg -def dt_non_geometric_factor(discr): - return min( - _GROUP_TYPE_TO_INFO[type(grp)].dt_non_geometric_factor(discr, grp) - for grp in discr.groups) - - -@memoize_on_first_arg -def dt_geometric_factor(discr): - return min( - _GROUP_TYPE_TO_INFO[type(grp)].dt_geometric_factor(discr, grp) - for grp in discr.groups) diff --git a/grudge/dt_utils.py b/grudge/dt_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..94c1f81e2d33062f85b32769653da85ef4a21195 --- /dev/null +++ b/grudge/dt_utils.py @@ -0,0 +1,170 @@ +"""Helper functions for estimating stable time steps for RKDG methods. + +.. autofunction:: dt_non_geometric_factor +""" + +__copyright__ = """ +Copyright (C) 2021 University of Illinois Board of Trustees +""" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +import numpy as np + +from arraycontext import rec_map_array_container + +from functools import reduce + +from grudge.dof_desc import DD_VOLUME +from grudge.geometry import forward_metric_derivative_mat +from grudge.discretization import DiscretizationCollection + +from pytools import memoize_on_first_arg + + +@memoize_on_first_arg +def dt_non_geometric_factor(dcoll: DiscretizationCollection, dd=None) -> float: + r"""Computes the non-geometric scale factor: + + .. math:: + + \frac{2}{3}\operatorname{min}_i\left( \Delta r_i \right), + + where :math:`\Delta r_i` denotes the distance between two distinct + nodes on the reference element. + + :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one. + Defaults to the base volume discretization if not provided. + :returns: a :class:`float` denoting the minimum node distance on the + reference element. + """ + if dd is None: + dd = DD_VOLUME + + discr = dcoll.discr_from_dd(dd) + min_delta_rs = [] + for mgrp in discr.mesh.groups: + nodes = np.asarray(list(zip(*mgrp.unit_nodes))) + nnodes = mgrp.nunit_nodes + + # NOTE: order 0 elements have 1 node located at the centroid of + # the reference element and is equidistant from each vertex + if mgrp.order == 0: + assert nnodes == 1 + min_delta_rs.append( + 2/3 * np.linalg.norm(nodes[0] - mgrp.vertex_unit_coordinates()[0]) + ) + else: + min_delta_rs.append( + 2/3 * min( + np.linalg.norm(nodes[i] - nodes[j]) + for i in range(nnodes) for j in range(nnodes) if i != j + ) + ) + + # Return minimum over all element groups in the discretization + return min(min_delta_rs) + + +def symmetric_eigenvalues(actx, amat): + """*amat* must be complex-valued, or ``actx.np.sqrt`` must automatically + up-cast to complex data. + """ + + # https://gist.github.com/inducer/75ede170638c389c387e72e0ef1f0ef4 + sqrt = actx.np.sqrt + + if amat.shape == (1, 1): + (a,), = amat + return a + elif amat.shape == (2, 2): + (a, b), (_b, c) = amat + x0 = sqrt(a**2 - 2*a*c + 4*b**2 + c**2)/2 + x1 = a/2 + c/2 + + return [-x0 + x1, + x0 + x1] + elif amat.shape == (3, 3): + # This is likely awful numerically, but *shrug*, we're only using + # it for time step estimation. + (a, b, c), (_b, d, e), (_c, _e, f) = amat + x0 = a*d + x1 = f*x0 + x2 = b*c*e + x3 = e**2 + x4 = a*x3 + x5 = b**2 + x6 = f*x5 + x7 = c**2 + x8 = d*x7 + x9 = -a - d - f + x10 = x9**3 + x11 = a*f + x12 = d*f + x13 = (-9*a - 9*d - 9*f)*(x0 + x11 + x12 - x3 - x5 - x7) + x14 = -3*x0 - 3*x11 - 3*x12 + 3*x3 + 3*x5 + 3*x7 + x9**2 + x15_0 = (-4*x14**3 + (-27*x1 + 2*x10 - x13 - 54*x2 + 27*x4 + 27*x6 + + 27*x8)**2) + x15_1 = sqrt(x15_0) + x15_2 =(-27*x1/2 + x10 - x13/2 - 27*x2 + 27*x4/2 + 27*x6/2 + 27*x8/2 + + x15_1/2) + x15 = x15_2**(1/3) + x16 = x15/3 + x17 = x14/(3*x15) + x18 = a/3 + d/3 + f/3 + x19 = 3**(1/2)*1j/2 + x20 = x19 - 1/2 + x21 = -x19 - 1/2 + + return [-x16 - x17 + x18, + -x16*x20 - x17/x20 + x18, + -x16*x21 - x17/x21 + x18] + else: + raise NotImplementedError( + "Unsupported shape ({amat.shape}) for analytically computing eigenvalues" + ) + + +@memoize_on_first_arg +def dt_geometric_factor(dcoll: DiscretizationCollection, dd=None) -> float: + """ + :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one. + Defaults to the base volume discretization if not provided. + :returns: a :class:`float` denoting the geometric scaling factor. + """ + if dd is None: + dd = DD_VOLUME + + actx = dcoll._setup_actx + fmd = forward_metric_derivative_mat(actx, dcoll, dd=dd) + ata = fmd @ fmd.T + + complex_dtype = dcoll.discr_from_dd(dd).complex_dtype + + ata_complex = rec_map_array_container( + lambda ary: ary.astype(complex_dtype), ata + ) + + sing_values = [actx.np.sqrt(abs(v)) + for v in symmetric_eigenvalues(actx, ata_complex)] + + return reduce(actx.np.minimum, sing_values)