From ff5c3ead23a36e22dccc9a92077b7d224a353c7e Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner <inform@tiker.net> Date: Mon, 24 May 2021 16:27:35 -0500 Subject: [PATCH] Re-add old symbolic tests, drop now-redundant symbolic tests from test_grudge This ensures that the symbolic stuff remains tested until we remove it. --- test/test_grudge.py | 134 ---- test/test_grudge_sym_old.py | 1242 +++++++++++++++++++++++++++++++++++ 2 files changed, 1242 insertions(+), 134 deletions(-) create mode 100644 test/test_grudge_sym_old.py diff --git a/test/test_grudge.py b/test/test_grudge.py index 56d76624..e95e633f 100644 --- a/test/test_grudge.py +++ b/test/test_grudge.py @@ -1118,140 +1118,6 @@ def test_empty_boundary(actx_factory): assert len(component) == len(dcoll.discr_from_dd(BTAG_NONE).groups) -# {{{ Deprecated symbolic tests - -def test_op_collector_order_determinism(): - from grudge import sym - - class TestOperator(sym.Operator): - - def __init__(self): - sym.Operator.__init__(self, dof_desc.DD_VOLUME, dof_desc.DD_VOLUME) - - mapper_method = "map_test_operator" - - from grudge.symbolic.mappers import BoundOperatorCollector - - class TestBoundOperatorCollector(BoundOperatorCollector): - - def map_test_operator(self, expr): - return self.map_operator(expr) - - v0 = sym.var("v0") - ob0 = sym.OperatorBinding(TestOperator(), v0) - - v1 = sym.var("v1") - ob1 = sym.OperatorBinding(TestOperator(), v1) - - # The output order isn't significant, but it should always be the same. - assert list(TestBoundOperatorCollector(TestOperator)(ob0 + ob1)) == [ob0, ob1] - - -def test_map_if(actx_factory): - """Test :meth:`grudge.symbolic.execution.ExecutionMapper.map_if` handling - of scalar conditions. - """ - from grudge import sym, bind - - actx = actx_factory() - - dim = 2 - mesh = mgen.generate_regular_rect_mesh( - a=(-0.5,)*dim, b=(0.5,)*dim, - nelements_per_axis=(8,)*dim, order=4) - dcoll = DiscretizationCollection(actx, mesh, order=4) - - sym_if = sym.If(sym.Comparison(2.0, "<", 1.0e-14), 1.0, 2.0) - bind(dcoll, sym_if)(actx) - - -def test_operator_compiler_overwrite(actx_factory): - """Tests that the same expression in ``eval_code`` and ``discr_code`` - does not confuse the OperatorCompiler in grudge/symbolic/compiler.py. - """ - from grudge import sym, bind - - actx = actx_factory() - - ambient_dim = 2 - target_order = 4 - - from meshmode.mesh.generation import generate_regular_rect_mesh - mesh = generate_regular_rect_mesh( - a=(-0.5,)*ambient_dim, b=(0.5,)*ambient_dim, - nelements_per_axis=(8,)*ambient_dim, order=1) - dcoll = DiscretizationCollection(actx, mesh, order=target_order) - - # {{{ test - - sym_u = sym.nodes(ambient_dim) - sym_div_u = sum(d(u) for d, u in zip(sym.nabla(ambient_dim), sym_u)) - - div_u = bind(dcoll, sym_div_u)(actx) - error = bind(dcoll, sym.norm(2, sym.var("x")))(actx, x=div_u - dcoll.dim) - logger.info("error: %.5e", error) - - # }}} - - -@pytest.mark.parametrize("ambient_dim", [ - 2, - # FIXME, cf. https://github.com/inducer/grudge/pull/78/ - pytest.param(3, marks=pytest.mark.xfail) - ]) -def test_incorrect_assignment_aggregation(actx_factory, ambient_dim): - """Tests that the greedy assignemnt aggregation code works on a non-trivial - expression (on which it didn't work at the time of writing). - """ - from grudge import sym, bind - - actx = actx_factory() - - target_order = 4 - - from meshmode.mesh.generation import generate_regular_rect_mesh - mesh = generate_regular_rect_mesh( - a=(-0.5,)*ambient_dim, b=(0.5,)*ambient_dim, - nelements_per_axis=(8,)*ambient_dim, order=1) - dcoll = DiscretizationCollection(actx, mesh, order=target_order) - - # {{{ test with a relative norm - - from grudge.dof_desc import DD_VOLUME - dd = DD_VOLUME - sym_x = sym.make_sym_array("y", ambient_dim, dd=dd) - sym_y = sym.make_sym_array("y", ambient_dim, dd=dd) - - sym_norm_y = sym.norm(2, sym_y, dd=dd) - sym_norm_d = sym.norm(2, sym_x - sym_y, dd=dd) - sym_op = sym_norm_d / sym_norm_y - logger.info("%s", sym.pretty(sym_op)) - - # FIXME: this shouldn't raise a RuntimeError - with pytest.raises(RuntimeError): - bind(dcoll, sym_op)(actx, x=1.0, y=dcoll.discr_from_dd(dd).nodes()) - - # }}} - - # {{{ test with repeated mass inverses - - sym_minv_y = sym.cse(sym.InverseMassOperator()(sym_y), "minv_y") - - sym_u = make_obj_array([0.5 * sym.Ones(dd), 0.0, 0.0])[:ambient_dim] - sym_div_u = sum(d(u) for d, u in zip(sym.nabla(ambient_dim), sym_u)) - - sym_op = sym.MassOperator(dd)(sym_u) \ - + sym.MassOperator(dd)(sym_minv_y * sym_div_u) - logger.info("%s", sym.pretty(sym_op)) - - # FIXME: this shouldn't raise a RuntimeError either - bind(dcoll, sym_op)(actx, y=dcoll.discr_from_dd(dd).nodes()) - - # }}} - -# }}} - - # You can test individual routines by typing # $ python test_grudge.py 'test_routine()' diff --git a/test/test_grudge_sym_old.py b/test/test_grudge_sym_old.py new file mode 100644 index 00000000..f246a895 --- /dev/null +++ b/test/test_grudge_sym_old.py @@ -0,0 +1,1242 @@ +__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. +""" + +import numpy as np +import numpy.linalg as la + +from meshmode import _acf # noqa: F401 +from meshmode.dof_array import flatten, thaw +import meshmode.mesh.generation as mgen + +from pytools.obj_array import flat_obj_array, make_obj_array + +from grudge import sym, bind, DiscretizationCollection + +import grudge.dof_desc as dof_desc + +import pytest +from meshmode.array_context import ( # noqa + pytest_generate_tests_for_pyopencl_array_context + as pytest_generate_tests) + +import logging + +logger = logging.getLogger(__name__) + + +# {{{ inverse metric + +@pytest.mark.parametrize("dim", [2, 3]) +def test_inverse_metric(actx_factory, dim): + actx = actx_factory() + + mesh = mgen.generate_regular_rect_mesh(a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(6,)*dim, order=4) + + def m(x): + result = np.empty_like(x) + result[0] = ( + 1.5*x[0] + np.cos(x[0]) + + 0.1*np.sin(10*x[1])) + result[1] = ( + 0.05*np.cos(10*x[0]) + + 1.3*x[1] + np.sin(x[1])) + if len(x) == 3: + result[2] = x[2] + return result + + from meshmode.mesh.processing import map_mesh + mesh = map_mesh(mesh, m) + + discr = DiscretizationCollection(actx, mesh, order=4) + + sym_op = ( + sym.forward_metric_derivative_mat(mesh.dim) + .dot( + sym.inverse_metric_derivative_mat(mesh.dim) + ) + .reshape(-1)) + + op = bind(discr, sym_op) + mat = op(actx).reshape(mesh.dim, mesh.dim) + + for i in range(mesh.dim): + for j in range(mesh.dim): + tgt = 1 if i == j else 0 + + err = actx.np.linalg.norm(mat[i, j] - tgt, ord=np.inf) + logger.info("error[%d, %d]: %.5e", i, j, err) + assert err < 1.0e-12, (i, j, err) + +# }}} + + +# {{{ mass operator trig integration + +@pytest.mark.parametrize("ambient_dim", [1, 2, 3]) +@pytest.mark.parametrize("discr_tag", [dof_desc.DISCR_TAG_BASE, + dof_desc.DISCR_TAG_QUAD]) +def test_mass_mat_trig(actx_factory, ambient_dim, discr_tag): + """Check the integral of some trig functions on an interval using the mass + matrix. + """ + actx = actx_factory() + + nel_1d = 16 + order = 4 + + a = -4.0 * np.pi + b = +9.0 * np.pi + true_integral = 13*np.pi/2 * (b - a)**(ambient_dim - 1) + + from meshmode.discretization.poly_element import QuadratureSimplexGroupFactory + dd_quad = dof_desc.DOFDesc(dof_desc.DTAG_VOLUME_ALL, discr_tag) + if discr_tag is dof_desc.DISCR_TAG_BASE: + discr_tag_to_group_factory = {} + else: + discr_tag_to_group_factory = { + discr_tag: QuadratureSimplexGroupFactory(order=2*order) + } + + mesh = mgen.generate_regular_rect_mesh( + a=(a,)*ambient_dim, b=(b,)*ambient_dim, + nelements_per_axis=(nel_1d,)*ambient_dim, order=1) + discr = DiscretizationCollection( + actx, mesh, order=order, + discr_tag_to_group_factory=discr_tag_to_group_factory + ) + + def _get_variables_on(dd): + sym_f = sym.var("f", dd=dd) + sym_x = sym.nodes(ambient_dim, dd=dd) + sym_ones = sym.Ones(dd) + + return sym_f, sym_x, sym_ones + + sym_f, sym_x, sym_ones = _get_variables_on(dof_desc.DD_VOLUME) + f_volm = actx.to_numpy(flatten(bind(discr, sym.cos(sym_x[0])**2)(actx))) + ones_volm = actx.to_numpy(flatten(bind(discr, sym_ones)(actx))) + + sym_f, sym_x, sym_ones = _get_variables_on(dd_quad) + f_quad = bind(discr, sym.cos(sym_x[0])**2)(actx) + ones_quad = bind(discr, sym_ones)(actx) + + mass_op = bind(discr, sym.MassOperator(dd_quad, dof_desc.DD_VOLUME)(sym_f)) + + num_integral_1 = np.dot(ones_volm, actx.to_numpy(flatten(mass_op(f=f_quad)))) + err_1 = abs(num_integral_1 - true_integral) + assert err_1 < 1e-9, err_1 + + num_integral_2 = np.dot(f_volm, actx.to_numpy(flatten(mass_op(f=ones_quad)))) + err_2 = abs(num_integral_2 - true_integral) + assert err_2 < 1.0e-9, err_2 + + if discr_tag is dof_desc.DISCR_TAG_BASE: + # NOTE: `integral` always makes a square mass matrix and + # `QuadratureSimplexGroupFactory` does not have a `mass_matrix` method. + num_integral_3 = bind(discr, + sym.integral(sym_f, dd=dd_quad))(f=f_quad) + err_3 = abs(num_integral_3 - true_integral) + assert err_3 < 5.0e-10, err_3 + +# }}} + + +# {{{ mass operator surface area + +def _ellipse_surface_area(radius, aspect_ratio): + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.ellipe.html + eccentricity = 1.0 - (1/aspect_ratio)**2 + + if abs(aspect_ratio - 2.0) < 1.0e-14: + # NOTE: hardcoded value so we don't need scipy for the test + ellip_e = 1.2110560275684594 + else: + from scipy.special import ellipe # pylint: disable=no-name-in-module + ellip_e = ellipe(eccentricity) + + return 4.0 * radius * ellip_e + + +def _spheroid_surface_area(radius, aspect_ratio): + # https://en.wikipedia.org/wiki/Ellipsoid#Surface_area + a = 1.0 + c = aspect_ratio + + if a < c: + e = np.sqrt(1.0 - (a/c)**2) + return 2.0 * np.pi * radius**2 * (1.0 + (c/a) / e * np.arcsin(e)) + else: + e = np.sqrt(1.0 - (c/a)**2) + return 2.0 * np.pi * radius**2 * (1 + (c/a)**2 / e * np.arctanh(e)) + + +@pytest.mark.parametrize("name", [ + "2-1-ellipse", "spheroid", "box2d", "box3d" + ]) +def test_mass_surface_area(actx_factory, name): + actx = actx_factory() + + # {{{ cases + + if name == "2-1-ellipse": + from mesh_data import EllipseMeshBuilder + builder = EllipseMeshBuilder(radius=3.1, aspect_ratio=2.0) + surface_area = _ellipse_surface_area(builder.radius, builder.aspect_ratio) + elif name == "spheroid": + from mesh_data import SpheroidMeshBuilder + builder = SpheroidMeshBuilder() + surface_area = _spheroid_surface_area(builder.radius, builder.aspect_ratio) + elif name == "box2d": + from mesh_data import BoxMeshBuilder + builder = BoxMeshBuilder(ambient_dim=2) + surface_area = 1.0 + elif name == "box3d": + from mesh_data import BoxMeshBuilder + builder = BoxMeshBuilder(ambient_dim=3) + surface_area = 1.0 + else: + raise ValueError("unknown geometry name: %s" % name) + + # }}} + + # {{{ convergence + + from pytools.convergence import EOCRecorder + eoc = EOCRecorder() + + for resolution in builder.resolutions: + mesh = builder.get_mesh(resolution, builder.mesh_order) + discr = DiscretizationCollection(actx, mesh, order=builder.order) + volume_discr = discr.discr_from_dd(dof_desc.DD_VOLUME) + + logger.info("ndofs: %d", volume_discr.ndofs) + logger.info("nelements: %d", volume_discr.mesh.nelements) + + # {{{ compute surface area + + dd = dof_desc.DD_VOLUME + sym_op = sym.NodalSum(dd)(sym.MassOperator(dd, dd)(sym.Ones(dd))) + approx_surface_area = bind(discr, sym_op)(actx) + + logger.info("surface: got {:.5e} / expected {:.5e}".format( + approx_surface_area, surface_area)) + area_error = abs(approx_surface_area - surface_area) / abs(surface_area) + + # }}} + + h_max = bind(discr, sym.h_max_from_volume( + discr.ambient_dim, dim=discr.dim, dd=dd))(actx) + eoc.add_data_point(h_max, area_error + 1.0e-16) + + # }}} + + logger.info("surface area error\n%s", str(eoc)) + + assert eoc.max_error() < 1.0e-14 \ + or eoc.order_estimate() > builder.order + +# }}} + + +# {{{ surface mass inverse + +@pytest.mark.parametrize("name", ["2-1-ellipse", "spheroid"]) +def test_surface_mass_operator_inverse(actx_factory, name): + actx = actx_factory() + + # {{{ cases + + if name == "2-1-ellipse": + from mesh_data import EllipseMeshBuilder + builder = EllipseMeshBuilder(radius=3.1, aspect_ratio=2.0) + elif name == "spheroid": + from mesh_data import SpheroidMeshBuilder + builder = SpheroidMeshBuilder() + else: + raise ValueError("unknown geometry name: %s" % name) + + # }}} + + # {{{ convergence + + from pytools.convergence import EOCRecorder + eoc = EOCRecorder() + + for resolution in builder.resolutions: + mesh = builder.get_mesh(resolution, builder.mesh_order) + discr = DiscretizationCollection(actx, mesh, order=builder.order) + volume_discr = discr.discr_from_dd(dof_desc.DD_VOLUME) + + logger.info("ndofs: %d", volume_discr.ndofs) + logger.info("nelements: %d", volume_discr.mesh.nelements) + + # {{{ compute inverse mass + + dd = dof_desc.DD_VOLUME + sym_f = sym.cos(4.0 * sym.nodes(mesh.ambient_dim, dd)[0]) + sym_op = sym.InverseMassOperator(dd, dd)( + sym.MassOperator(dd, dd)(sym.var("f"))) + + f = bind(discr, sym_f)(actx) + f_inv = bind(discr, sym_op)(actx, f=f) + + inv_error = bind(discr, + sym.norm(2, sym.var("x") - sym.var("y")) + / sym.norm(2, sym.var("y")))(actx, x=f_inv, y=f) + + # }}} + + h_max = bind(discr, sym.h_max_from_volume( + discr.ambient_dim, dim=discr.dim, dd=dd))(actx) + eoc.add_data_point(h_max, inv_error) + + # }}} + + logger.info("inverse mass error\n%s", str(eoc)) + + # NOTE: both cases give 1.0e-16-ish at the moment, but just to be on the + # safe side, choose a slightly larger tolerance + assert eoc.max_error() < 1.0e-14 + +# }}} + + +# {{{ surface face normal orthogonality + +@pytest.mark.parametrize("mesh_name", ["2-1-ellipse", "spheroid"]) +def test_face_normal_surface(actx_factory, mesh_name): + """Check that face normals are orthogonal to the surface normal""" + actx = actx_factory() + + # {{{ geometry + + if mesh_name == "2-1-ellipse": + from mesh_data import EllipseMeshBuilder + builder = EllipseMeshBuilder(radius=3.1, aspect_ratio=2.0) + elif mesh_name == "spheroid": + from mesh_data import SpheroidMeshBuilder + builder = SpheroidMeshBuilder() + else: + raise ValueError("unknown mesh name: %s" % mesh_name) + + mesh = builder.get_mesh(builder.resolutions[0], builder.mesh_order) + discr = DiscretizationCollection(actx, mesh, order=builder.order) + + volume_discr = discr.discr_from_dd(dof_desc.DD_VOLUME) + logger.info("ndofs: %d", volume_discr.ndofs) + logger.info("nelements: %d", volume_discr.mesh.nelements) + + # }}} + + # {{{ symbolic + from meshmode.discretization.connection import FACE_RESTR_INTERIOR + + dv = dof_desc.DD_VOLUME + df = dof_desc.as_dofdesc(FACE_RESTR_INTERIOR) + + ambient_dim = mesh.ambient_dim + dim = mesh.dim + + sym_surf_normal = sym.project(dv, df)( + sym.surface_normal(ambient_dim, dim=dim, dd=dv).as_vector() + ) + sym_surf_normal = sym_surf_normal / sym.sqrt(sum(sym_surf_normal**2)) + + sym_face_normal_i = sym.normal(df, ambient_dim, dim=dim - 1) + sym_face_normal_e = sym.OppositeInteriorFaceSwap(df)(sym_face_normal_i) + + if mesh.ambient_dim == 3: + # NOTE: there's only one face tangent in 3d + sym_face_tangent = ( + sym.pseudoscalar(ambient_dim, dim - 1, dd=df) + / sym.area_element(ambient_dim, dim - 1, dd=df)).as_vector() + + # }}} + + # {{{ checks + + def _eval_error(x): + return bind(discr, sym.norm(np.inf, sym.var("x", dd=df), dd=df))(actx, x=x) + + rtol = 1.0e-14 + + surf_normal = bind(discr, sym_surf_normal)(actx) + + face_normal_i = bind(discr, sym_face_normal_i)(actx) + face_normal_e = bind(discr, sym_face_normal_e)(actx) + + # check interpolated surface normal is orthogonal to face normal + error = _eval_error(surf_normal.dot(face_normal_i)) + logger.info("error[n_dot_i]: %.5e", error) + assert error < rtol + + # check angle between two neighboring elements + error = _eval_error(face_normal_i.dot(face_normal_e) + 1.0) + logger.info("error[i_dot_e]: %.5e", error) + assert error > rtol + + # check orthogonality with face tangent + if ambient_dim == 3: + face_tangent = bind(discr, sym_face_tangent)(actx) + + error = _eval_error(face_tangent.dot(face_normal_i)) + logger.info("error[t_dot_i]: %.5e", error) + assert error < 5 * rtol + + # }}} + +# }}} + + +# {{{ diff operator + +@pytest.mark.parametrize("dim", [1, 2, 3]) +def test_tri_diff_mat(actx_factory, dim, order=4): + """Check differentiation matrix along the coordinate axes on a disk + + Uses sines as the function to differentiate. + """ + + actx = actx_factory() + + from pytools.convergence import EOCRecorder + axis_eoc_recs = [EOCRecorder() for axis in range(dim)] + + for n in [4, 8, 16]: + mesh = mgen.generate_regular_rect_mesh(a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(n,)*dim, order=4) + + discr = DiscretizationCollection(actx, mesh, order=4) + nabla = sym.nabla(dim) + + for axis in range(dim): + x = sym.nodes(dim) + + f = bind(discr, sym.sin(3*x[axis]))(actx) + df = bind(discr, 3*sym.cos(3*x[axis]))(actx) + + sym_op = nabla[axis](sym.var("f")) + bound_op = bind(discr, sym_op) + df_num = bound_op(f=f) + + linf_error = actx.np.linalg.norm(df_num - df, ord=np.inf) + axis_eoc_recs[axis].add_data_point(1/n, linf_error) + + for axis, eoc_rec in enumerate(axis_eoc_recs): + logger.info("axis %d\n%s", axis, eoc_rec) + assert eoc_rec.order_estimate() > order - 0.25 + +# }}} + + +# {{{ divergence theorem + +def test_2d_gauss_theorem(actx_factory): + """Verify Gauss's theorem explicitly on a mesh""" + + pytest.importorskip("meshpy") + + from meshpy.geometry import make_circle, GeometryBuilder + from meshpy.triangle import MeshInfo, build + + geob = GeometryBuilder() + geob.add_geometry(*make_circle(1)) + mesh_info = MeshInfo() + geob.set(mesh_info) + + mesh_info = build(mesh_info) + + from meshmode.mesh.io import from_meshpy + from meshmode.mesh import BTAG_ALL + + mesh = from_meshpy(mesh_info, order=1) + + actx = actx_factory() + + discr = DiscretizationCollection(actx, mesh, order=2) + + def f(x): + return flat_obj_array( + sym.sin(3*x[0])+sym.cos(3*x[1]), + sym.sin(2*x[0])+sym.cos(x[1])) + + gauss_err = bind(discr, + sym.integral(( + sym.nabla(2) * f(sym.nodes(2)) + ).sum()) + - # noqa: W504 + sym.integral( + sym.project("vol", BTAG_ALL)(f(sym.nodes(2))) + .dot(sym.normal(BTAG_ALL, 2)), + dd=BTAG_ALL) + )(actx) + + assert abs(gauss_err) < 1e-13 + + +@pytest.mark.parametrize("mesh_name", ["2-1-ellipse", "spheroid"]) +def test_surface_divergence_theorem(actx_factory, mesh_name, visualize=False): + r"""Check the surface divergence theorem. + + .. math:: + + \int_Sigma \phi \nabla_i f_i = + \int_\Sigma \nabla_i \phi f_i + + \int_\Sigma \kappa \phi f_i n_i + + \int_{\partial \Sigma} \phi f_i m_i + + where :math:`n_i` is the surface normal and :class:`m_i` is the + face normal (which should be orthogonal to both the surface normal + and the face tangent). + """ + actx = actx_factory() + + # {{{ cases + + if mesh_name == "2-1-ellipse": + from mesh_data import EllipseMeshBuilder + builder = EllipseMeshBuilder(radius=3.1, aspect_ratio=2.0) + elif mesh_name == "spheroid": + from mesh_data import SpheroidMeshBuilder + builder = SpheroidMeshBuilder() + elif mesh_name == "circle": + from mesh_data import EllipseMeshBuilder + builder = EllipseMeshBuilder(radius=1.0, aspect_ratio=1.0) + elif mesh_name == "starfish": + from mesh_data import StarfishMeshBuilder + builder = StarfishMeshBuilder() + elif mesh_name == "sphere": + from mesh_data import SphereMeshBuilder + builder = SphereMeshBuilder(radius=1.0, mesh_order=16) + else: + raise ValueError("unknown mesh name: %s" % mesh_name) + + # }}} + + # {{{ convergene + + def f(x): + return flat_obj_array( + sym.sin(3*x[1]) + sym.cos(3*x[0]) + 1.0, + sym.sin(2*x[0]) + sym.cos(x[1]), + 3.0 * sym.cos(x[0] / 2) + sym.cos(x[1]), + )[:ambient_dim] + + from pytools.convergence import EOCRecorder + eoc_global = EOCRecorder() + eoc_local = EOCRecorder() + + theta = np.pi / 3.33 + ambient_dim = builder.ambient_dim + if ambient_dim == 2: + mesh_rotation = np.array([ + [np.cos(theta), -np.sin(theta)], + [np.sin(theta), np.cos(theta)], + ]) + else: + mesh_rotation = np.array([ + [1.0, 0.0, 0.0], + [0.0, np.cos(theta), -np.sin(theta)], + [0.0, np.sin(theta), np.cos(theta)], + ]) + + mesh_offset = np.array([0.33, -0.21, 0.0])[:ambient_dim] + + for i, resolution in enumerate(builder.resolutions): + from meshmode.mesh.processing import affine_map + from meshmode.discretization.connection import FACE_RESTR_ALL + + mesh = builder.get_mesh(resolution, builder.mesh_order) + mesh = affine_map(mesh, A=mesh_rotation, b=mesh_offset) + + from meshmode.discretization.poly_element import \ + QuadratureSimplexGroupFactory + discr = DiscretizationCollection( + actx, mesh, order=builder.order, + discr_tag_to_group_factory={ + "product": QuadratureSimplexGroupFactory(2 * builder.order) + } + ) + + volume = discr.discr_from_dd(dof_desc.DD_VOLUME) + logger.info("ndofs: %d", volume.ndofs) + logger.info("nelements: %d", volume.mesh.nelements) + + dd = dof_desc.DD_VOLUME + dq = dd.with_discr_tag("product") + df = dof_desc.as_dofdesc(FACE_RESTR_ALL) + ambient_dim = discr.ambient_dim + dim = discr.dim + + # variables + sym_f = f(sym.nodes(ambient_dim, dd=dd)) + sym_f_quad = f(sym.nodes(ambient_dim, dd=dq)) + sym_kappa = sym.summed_curvature(ambient_dim, dim=dim, dd=dq) + sym_normal = sym.surface_normal(ambient_dim, dim=dim, dd=dq).as_vector() + + sym_face_normal = sym.normal(df, ambient_dim, dim=dim - 1) + sym_face_f = sym.project(dd, df)(sym_f) + + # operators + sym_stiff = sum( + sym.StiffnessOperator(d)(f) for d, f in enumerate(sym_f) + ) + sym_stiff_t = sum( + sym.StiffnessTOperator(d)(f) for d, f in enumerate(sym_f) + ) + sym_k = sym.MassOperator(dq, dd)(sym_kappa * sym_f_quad.dot(sym_normal)) + sym_flux = sym.FaceMassOperator()(sym_face_f.dot(sym_face_normal)) + + # sum everything up + sym_op_global = sym.NodalSum(dd)( + sym_stiff - (sym_stiff_t + sym_k)) + sym_op_local = sym.ElementwiseSumOperator(dd)( + sym_stiff - (sym_stiff_t + sym_k + sym_flux)) + + # evaluate + op_global = bind(discr, sym_op_global)(actx) + op_local = bind(discr, sym_op_local)(actx) + + err_global = abs(op_global) + err_local = bind(discr, sym.norm(np.inf, sym.var("x")))(actx, x=op_local) + logger.info("errors: global %.5e local %.5e", err_global, err_local) + + # compute max element size + h_max = bind(discr, sym.h_max_from_volume( + discr.ambient_dim, dim=discr.dim, dd=dd))(actx) + eoc_global.add_data_point(h_max, err_global) + eoc_local.add_data_point(h_max, err_local) + + if visualize: + from grudge.shortcuts import make_visualizer + vis = make_visualizer(discr, vis_order=builder.order) + + filename = f"surface_divergence_theorem_{mesh_name}_{i:04d}.vtu" + vis.write_vtk_file(filename, [ + ("r", actx.np.log10(op_local)) + ], overwrite=True) + + # }}} + + order = min(builder.order, builder.mesh_order) - 0.5 + logger.info("\n%s", str(eoc_global)) + logger.info("\n%s", str(eoc_local)) + + assert eoc_global.max_error() < 1.0e-12 \ + or eoc_global.order_estimate() > order - 0.5 + + assert eoc_local.max_error() < 1.0e-12 \ + or eoc_local.order_estimate() > order - 0.5 + +# }}} + + +# {{{ models: advection + +@pytest.mark.parametrize(("mesh_name", "mesh_pars"), [ + ("segment", [8, 16, 32]), + ("disk", [0.1, 0.05]), + ("rect2", [4, 8]), + ("rect3", [4, 6]), + ("warped2", [4, 8]), + ]) +@pytest.mark.parametrize("op_type", ["strong", "weak"]) +@pytest.mark.parametrize("flux_type", ["central"]) +@pytest.mark.parametrize("order", [3, 4, 5]) +# test: 'test_convergence_advec(cl._csc, "disk", [0.1, 0.05], "strong", "upwind", 3)' +def test_convergence_advec(actx_factory, mesh_name, mesh_pars, op_type, flux_type, + order, visualize=False): + """Test whether 2D advection actually converges""" + + actx = actx_factory() + + from pytools.convergence import EOCRecorder + eoc_rec = EOCRecorder() + + for mesh_par in mesh_pars: + if mesh_name == "segment": + mesh = mgen.generate_box_mesh( + [np.linspace(-1.0, 1.0, mesh_par)], + order=order) + + dim = 1 + dt_factor = 1.0 + elif mesh_name == "disk": + pytest.importorskip("meshpy") + + from meshpy.geometry import make_circle, GeometryBuilder + from meshpy.triangle import MeshInfo, build + + geob = GeometryBuilder() + geob.add_geometry(*make_circle(1)) + mesh_info = MeshInfo() + geob.set(mesh_info) + + mesh_info = build(mesh_info, max_volume=mesh_par) + + from meshmode.mesh.io import from_meshpy + mesh = from_meshpy(mesh_info, order=1) + dim = 2 + dt_factor = 4 + elif mesh_name.startswith("rect"): + dim = int(mesh_name[-1:]) + mesh = mgen.generate_regular_rect_mesh(a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(mesh_par,)*dim, order=4) + + if dim == 2: + dt_factor = 4 + elif dim == 3: + dt_factor = 2 + else: + raise ValueError("dt_factor not known for %dd" % dim) + elif mesh_name.startswith("warped"): + dim = int(mesh_name[-1:]) + mesh = mgen.generate_warped_rect_mesh(dim, order=order, + nelements_side=mesh_par) + + if dim == 2: + dt_factor = 4 + elif dim == 3: + dt_factor = 2 + else: + raise ValueError("dt_factor not known for %dd" % dim) + else: + raise ValueError("invalid mesh name: " + mesh_name) + + v = np.array([0.27, 0.31, 0.1])[:dim] + norm_v = la.norm(v) + + def f(x): + return sym.sin(10*x) + + def u_analytic(x): + return f( + -v.dot(x)/norm_v + + sym.var("t", dof_desc.DD_SCALAR)*norm_v) + + from grudge.models.advection import ( + StrongAdvectionOperator, WeakAdvectionOperator) + from meshmode.mesh import BTAG_ALL + + discr = DiscretizationCollection(actx, mesh, order=order) + op_class = { + "strong": StrongAdvectionOperator, + "weak": WeakAdvectionOperator, + }[op_type] + op = op_class(v, + inflow_u=u_analytic(sym.nodes(dim, BTAG_ALL)), + flux_type=flux_type) + + bound_op = bind(discr, op.sym_operator()) + + u = bind(discr, u_analytic(sym.nodes(dim)))(actx, t=0) + + def rhs(t, u): + return bound_op(t=t, u=u) + + if dim == 3: + final_time = 0.1 + else: + final_time = 0.2 + + h_max = bind(discr, sym.h_max_from_volume(discr.ambient_dim))(actx) + dt = dt_factor * h_max/order**2 + nsteps = (final_time // dt) + 1 + dt = final_time/nsteps + 1e-15 + + from grudge.shortcuts import set_up_rk4 + dt_stepper = set_up_rk4("u", dt, u, rhs) + + last_u = None + + from grudge.shortcuts import make_visualizer + vis = make_visualizer(discr, vis_order=order) + + step = 0 + + for event in dt_stepper.run(t_end=final_time): + if isinstance(event, dt_stepper.StateComputed): + step += 1 + logger.debug("[%04d] t = %.5f", step, event.t) + + last_t = event.t + last_u = event.state_component + + if visualize: + vis.write_vtk_file("fld-%s-%04d.vtu" % (mesh_par, step), + [("u", event.state_component)]) + + error_l2 = bind(discr, + sym.norm(2, sym.var("u")-u_analytic(sym.nodes(dim))))( + t=last_t, u=last_u) + logger.info("h_max %.5e error %.5e", h_max, error_l2) + eoc_rec.add_data_point(h_max, error_l2) + + logger.info("\n%s", eoc_rec.pretty_print( + abscissa_label="h", + error_label="L2 Error")) + + if mesh_name.startswith("warped"): + # NOTE: curvilinear meshes are hard + assert eoc_rec.order_estimate() > order - 0.5 + else: + assert eoc_rec.order_estimate() > order + +# }}} + + +# {{{ models: maxwell + +@pytest.mark.parametrize("order", [3, 4, 5]) +def test_convergence_maxwell(actx_factory, order): + """Test whether 3D Maxwell's actually converges""" + + actx = actx_factory() + + from pytools.convergence import EOCRecorder + eoc_rec = EOCRecorder() + + dims = 3 + ns = [4, 6, 8] + for n in ns: + mesh = mgen.generate_regular_rect_mesh( + a=(0.0,)*dims, + b=(1.0,)*dims, + nelements_per_axis=(n,)*dims) + + discr = DiscretizationCollection(actx, mesh, order=order) + + epsilon = 1 + mu = 1 + + from grudge.models.em import get_rectangular_cavity_mode + sym_mode = get_rectangular_cavity_mode(1, (1, 2, 2)) + + analytic_sol = bind(discr, sym_mode) + fields = analytic_sol(actx, t=0, epsilon=epsilon, mu=mu) + + from grudge.models.em import MaxwellOperator + op = MaxwellOperator(epsilon, mu, flux_type=0.5, dimensions=dims) + op.check_bc_coverage(mesh) + bound_op = bind(discr, op.sym_operator()) + + def rhs(t, w): + return bound_op(t=t, w=w) + + dt = 0.002 + final_t = dt * 5 + nsteps = int(final_t/dt) + + from grudge.shortcuts import set_up_rk4 + dt_stepper = set_up_rk4("w", dt, fields, rhs) + + logger.info("dt %.5e nsteps %5d", dt, nsteps) + + norm = bind(discr, sym.norm(2, sym.var("u"))) + + step = 0 + for event in dt_stepper.run(t_end=final_t): + if isinstance(event, dt_stepper.StateComputed): + assert event.component_id == "w" + esc = event.state_component + + step += 1 + logger.debug("[%04d] t = %.5e", step, event.t) + + sol = analytic_sol(actx, mu=mu, epsilon=epsilon, t=step * dt) + vals = [norm(u=(esc[i] - sol[i])) / norm(u=sol[i]) for i in range(5)] # noqa E501 + total_error = sum(vals) + eoc_rec.add_data_point(1.0/n, total_error) + + logger.info("\n%s", eoc_rec.pretty_print( + abscissa_label="h", + error_label="L2 Error")) + + assert eoc_rec.order_estimate() > order + +# }}} + + +# {{{ models: variable coefficient advection oversampling + +@pytest.mark.parametrize("order", [2, 3, 4]) +def test_improvement_quadrature(actx_factory, order): + """Test whether quadrature improves things and converges""" + from grudge.models.advection import VariableCoefficientAdvectionOperator + from pytools.convergence import EOCRecorder + from meshmode.discretization.poly_element import QuadratureSimplexGroupFactory + + actx = actx_factory() + + dims = 2 + sym_nds = sym.nodes(dims) + advec_v = flat_obj_array(-1*sym_nds[1], sym_nds[0]) + + flux = "upwind" + op = VariableCoefficientAdvectionOperator(advec_v, 0, flux_type=flux) + + def gaussian_mode(): + source_width = 0.1 + sym_x = sym.nodes(2) + return sym.exp(-np.dot(sym_x, sym_x) / source_width**2) + + def conv_test(descr, use_quad): + logger.info("-" * 75) + logger.info(descr) + logger.info("-" * 75) + eoc_rec = EOCRecorder() + + ns = [20, 25] + for n in ns: + mesh = mgen.generate_regular_rect_mesh( + a=(-0.5,)*dims, + b=(0.5,)*dims, + nelements_per_axis=(n,)*dims, + order=order) + + if use_quad: + discr_tag_to_group_factory = { + "product": QuadratureSimplexGroupFactory(order=4*order) + } + else: + discr_tag_to_group_factory = {"product": None} + + discr = DiscretizationCollection( + actx, mesh, order=order, + discr_tag_to_group_factory=discr_tag_to_group_factory + ) + + bound_op = bind(discr, op.sym_operator()) + fields = bind(discr, gaussian_mode())(actx, t=0) + norm = bind(discr, sym.norm(2, sym.var("u"))) + + esc = bound_op(u=fields) + total_error = norm(u=esc) + eoc_rec.add_data_point(1.0/n, total_error) + + logger.info("\n%s", eoc_rec.pretty_print( + abscissa_label="h", + error_label="L2 Error")) + + return eoc_rec.order_estimate(), np.array([x[1] for x in eoc_rec.history]) + + eoc, errs = conv_test("no quadrature", False) + q_eoc, q_errs = conv_test("with quadrature", True) + + assert q_eoc > eoc + assert (q_errs < errs).all() + assert q_eoc > order - 0.1 + +# }}} + + +# {{{ operator collector determinism + +def test_op_collector_order_determinism(): + class TestOperator(sym.Operator): + + def __init__(self): + sym.Operator.__init__(self, dof_desc.DD_VOLUME, dof_desc.DD_VOLUME) + + mapper_method = "map_test_operator" + + from grudge.symbolic.mappers import BoundOperatorCollector + + class TestBoundOperatorCollector(BoundOperatorCollector): + + def map_test_operator(self, expr): + return self.map_operator(expr) + + v0 = sym.var("v0") + ob0 = sym.OperatorBinding(TestOperator(), v0) + + v1 = sym.var("v1") + ob1 = sym.OperatorBinding(TestOperator(), v1) + + # The output order isn't significant, but it should always be the same. + assert list(TestBoundOperatorCollector(TestOperator)(ob0 + ob1)) == [ob0, ob1] + +# }}} + + +# {{{ bessel + +def test_bessel(actx_factory): + actx = actx_factory() + + dims = 2 + + mesh = mgen.generate_regular_rect_mesh( + a=(0.1,)*dims, + b=(1.0,)*dims, + nelements_per_axis=(8,)*dims) + + discr = DiscretizationCollection(actx, mesh, order=3) + + nodes = sym.nodes(dims) + r = sym.cse(sym.sqrt(nodes[0]**2 + nodes[1]**2)) + + # https://dlmf.nist.gov/10.6.1 + n = 3 + bessel_zero = ( + sym.bessel_j(n+1, r) + + sym.bessel_j(n-1, r) + - 2*n/r * sym.bessel_j(n, r)) + + z = bind(discr, sym.norm(2, bessel_zero))(actx) + + assert z < 1e-15 + +# }}} + + +# {{{ function symbol + +def test_external_call(actx_factory): + actx = actx_factory() + + def double(queue, x): + return 2 * x + + dims = 2 + + mesh = mgen.generate_regular_rect_mesh( + a=(0,) * dims, b=(1,) * dims, nelements_per_axis=(4,) * dims) + discr = DiscretizationCollection(actx, mesh, order=1) + + ones = sym.Ones(dof_desc.DD_VOLUME) + op = ( + ones * 3 + + sym.FunctionSymbol("double")(ones)) + + from grudge.function_registry import ( + base_function_registry, register_external_function) + + freg = register_external_function( + base_function_registry, + "double", + implementation=double, + dd=dof_desc.DD_VOLUME) + + bound_op = bind(discr, op, function_registry=freg) + + result = bound_op(actx, double=double) + assert actx.to_numpy(flatten(result) == 5).all() + + +@pytest.mark.parametrize("array_type", ["scalar", "vector"]) +def test_function_symbol_array(actx_factory, array_type): + """Test if `FunctionSymbol` distributed properly over object arrays.""" + + actx = actx_factory() + + dim = 2 + mesh = mgen.generate_regular_rect_mesh( + a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(8,)*dim, order=4) + discr = DiscretizationCollection(actx, mesh, order=4) + volume_discr = discr.discr_from_dd(dof_desc.DD_VOLUME) + + if array_type == "scalar": + sym_x = sym.var("x") + x = thaw(actx, actx.np.cos(volume_discr.nodes()[0])) + elif array_type == "vector": + sym_x = sym.make_sym_array("x", dim) + x = thaw(actx, volume_discr.nodes()) + else: + raise ValueError("unknown array type") + + norm = bind(discr, sym.norm(2, sym_x))(x=x) + assert isinstance(norm, float) + +# }}} + + +@pytest.mark.parametrize("p", [2, np.inf]) +def test_norm_obj_array(actx_factory, p): + """Test :func:`grudge.symbolic.operators.norm` for object arrays.""" + + actx = actx_factory() + + dim = 2 + mesh = mgen.generate_regular_rect_mesh( + a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(8,)*dim, order=1) + discr = DiscretizationCollection(actx, mesh, order=4) + + w = make_obj_array([1.0, 2.0, 3.0])[:dim] + + # {{ scalar + + sym_w = sym.var("w") + norm = bind(discr, sym.norm(p, sym_w))(actx, w=w[0]) + + norm_exact = w[0] + logger.info("norm: %.5e %.5e", norm, norm_exact) + assert abs(norm - norm_exact) < 1.0e-14 + + # }}} + + # {{{ vector + + sym_w = sym.make_sym_array("w", dim) + norm = bind(discr, sym.norm(p, sym_w))(actx, w=w) + + norm_exact = np.sqrt(np.sum(w**2)) if p == 2 else np.max(w) + logger.info("norm: %.5e %.5e", norm, norm_exact) + assert abs(norm - norm_exact) < 1.0e-14 + + # }}} + + +def test_map_if(actx_factory): + """Test :meth:`grudge.symbolic.execution.ExecutionMapper.map_if` handling + of scalar conditions. + """ + + actx = actx_factory() + + dim = 2 + mesh = mgen.generate_regular_rect_mesh( + a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(8,)*dim, order=4) + discr = DiscretizationCollection(actx, mesh, order=4) + + sym_if = sym.If(sym.Comparison(2.0, "<", 1.0e-14), 1.0, 2.0) + bind(discr, sym_if)(actx) + + +def test_empty_boundary(actx_factory): + # https://github.com/inducer/grudge/issues/54 + + from meshmode.mesh import BTAG_NONE + + actx = actx_factory() + + dim = 2 + mesh = mgen.generate_regular_rect_mesh( + a=(-0.5,)*dim, b=(0.5,)*dim, + nelements_per_axis=(8,)*dim, order=4) + discr = DiscretizationCollection(actx, mesh, order=4) + normal = bind(discr, + sym.normal(BTAG_NONE, dim, dim=dim - 1))(actx) + from meshmode.dof_array import DOFArray + for component in normal: + assert isinstance(component, DOFArray) + assert len(component) == len(discr.discr_from_dd(BTAG_NONE).groups) + + +def test_operator_compiler_overwrite(actx_factory): + """Tests that the same expression in ``eval_code`` and ``discr_code`` + does not confuse the OperatorCompiler in grudge/symbolic/compiler.py. + """ + + actx = actx_factory() + + ambient_dim = 2 + target_order = 4 + + from meshmode.mesh.generation import generate_regular_rect_mesh + mesh = generate_regular_rect_mesh( + a=(-0.5,)*ambient_dim, b=(0.5,)*ambient_dim, + n=(8,)*ambient_dim, order=1) + discr = DiscretizationCollection(actx, mesh, order=target_order) + + # {{{ test + + sym_u = sym.nodes(ambient_dim) + sym_div_u = sum(d(u) for d, u in zip(sym.nabla(ambient_dim), sym_u)) + + div_u = bind(discr, sym_div_u)(actx) + error = bind(discr, sym.norm(2, sym.var("x")))(actx, x=div_u - discr.dim) + logger.info("error: %.5e", error) + + # }}} + + +@pytest.mark.parametrize("ambient_dim", [ + 2, + # FIXME, cf. https://github.com/inducer/grudge/pull/78/ + pytest.param(3, marks=pytest.mark.xfail) + ]) +def test_incorrect_assignment_aggregation(actx_factory, ambient_dim): + """Tests that the greedy assignemnt aggregation code works on a non-trivial + expression (on which it didn't work at the time of writing). + """ + + actx = actx_factory() + + target_order = 4 + + from meshmode.mesh.generation import generate_regular_rect_mesh + mesh = generate_regular_rect_mesh( + a=(-0.5,)*ambient_dim, b=(0.5,)*ambient_dim, + n=(8,)*ambient_dim, order=1) + discr = DiscretizationCollection(actx, mesh, order=target_order) + + # {{{ test with a relative norm + + from grudge.dof_desc import DD_VOLUME + dd = DD_VOLUME + sym_x = sym.make_sym_array("y", ambient_dim, dd=dd) + sym_y = sym.make_sym_array("y", ambient_dim, dd=dd) + + sym_norm_y = sym.norm(2, sym_y, dd=dd) + sym_norm_d = sym.norm(2, sym_x - sym_y, dd=dd) + sym_op = sym_norm_d / sym_norm_y + logger.info("%s", sym.pretty(sym_op)) + + # FIXME: this shouldn't raise a RuntimeError + with pytest.raises(RuntimeError): + bind(discr, sym_op)(actx, x=1.0, y=discr.discr_from_dd(dd).nodes()) + + # }}} + + # {{{ test with repeated mass inverses + + sym_minv_y = sym.cse(sym.InverseMassOperator()(sym_y), "minv_y") + + sym_u = make_obj_array([0.5 * sym.Ones(dd), 0.0, 0.0])[:ambient_dim] + sym_div_u = sum(d(u) for d, u in zip(sym.nabla(ambient_dim), sym_u)) + + sym_op = sym.MassOperator(dd)(sym_u) \ + + sym.MassOperator(dd)(sym_minv_y * sym_div_u) + logger.info("%s", sym.pretty(sym_op)) + + # FIXME: this shouldn't raise a RuntimeError either + bind(discr, sym_op)(actx, y=discr.discr_from_dd(dd).nodes()) + + # }}} + + +# You can test individual routines by typing +# $ python test_grudge.py 'test_routine()' + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + pytest.main([__file__]) + +# vim: fdm=marker -- GitLab