def test_mass_operator_inverse(actx_factory, name): actx = actx_factory() # {{{ cases import mesh_data if name == "2-1-ellipse": # curve builder = mesh_data.EllipseMeshBuilder(radius=3.1, aspect_ratio=2.0) elif name == "spheroid": # surface builder = mesh_data.SpheroidMeshBuilder() elif name.startswith("warped_rect"): builder = mesh_data.WarpedRectMeshBuilder(dim=int(name[-1])) else: raise ValueError("unknown geometry name: %s" % name) # }}} # {{{ inv(m) @ m == id from pytools.convergence import EOCRecorder eoc = EOCRecorder() for resolution in builder.resolutions: mesh = builder.get_mesh(resolution, builder.mesh_order) dcoll = DiscretizationCollection(actx, mesh, order=builder.order) volume_discr = dcoll.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 def f(x): return actx.np.cos(4.0 * x[0]) dd = dof_desc.DD_VOLUME x_volm = thaw(volume_discr.nodes(), actx) f_volm = f(x_volm) f_inv = op.inverse_mass(dcoll, op.mass(dcoll, dd, f_volm)) inv_error = actx.to_numpy( op.norm(dcoll, f_volm - f_inv, 2) / op.norm(dcoll, f_volm, 2)) # }}} # compute max element size from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(dcoll) 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
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 actx.np.sin(10 * x) def u_analytic(x, t=0): return f(-v.dot(x) / norm_v + t * norm_v) from grudge.models.advection import (StrongAdvectionOperator, WeakAdvectionOperator) from meshmode.mesh import BTAG_ALL dcoll = DiscretizationCollection(actx, mesh, order=order) op_class = { "strong": StrongAdvectionOperator, "weak": WeakAdvectionOperator }[op_type] adv_operator = op_class(dcoll, v, inflow_u=lambda t: u_analytic( thaw(dcoll.nodes(dd=BTAG_ALL), actx), t=t), flux_type=flux_type) nodes = thaw(dcoll.nodes(), actx) u = u_analytic(nodes, t=0) def rhs(t, u): return adv_operator.operator(t, u) if dim == 3: final_time = 0.1 else: final_time = 0.2 from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(dcoll, dim=dcoll.ambient_dim) 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(dcoll) 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 = op.norm(dcoll, last_u - u_analytic(nodes, t=last_t), 2) logger.info("h_max %.5e error %.5e", h_max, error_l2) eoc_rec.add_data_point(h_max, actx.to_numpy(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
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) # }}} # {{{ convergence def f(x): return flat_obj_array( actx.np.sin(3 * x[1]) + actx.np.cos(3 * x[0]) + 1.0, actx.np.sin(2 * x[0]) + actx.np.cos(x[1]), 3.0 * actx.np.cos(x[0] / 2) + actx.np.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 qtag = dof_desc.DISCR_TAG_QUAD dcoll = DiscretizationCollection(actx, mesh, order=builder.order, discr_tag_to_group_factory={ qtag: QuadratureSimplexGroupFactory( 2 * builder.order) }) volume = dcoll.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(qtag) df = dof_desc.as_dofdesc(FACE_RESTR_ALL) ambient_dim = dcoll.ambient_dim # variables f_num = f(thaw(dcoll.nodes(dd=dd), actx)) f_quad_num = f(thaw(dcoll.nodes(dd=dq), actx)) from grudge.geometry import normal, summed_curvature kappa = summed_curvature(actx, dcoll, dd=dq) normal = normal(actx, dcoll, dd=dq) face_normal = thaw(dcoll.normal(df), actx) face_f = op.project(dcoll, dd, df, f_num) # operators stiff = op.mass( dcoll, sum( op.local_d_dx(dcoll, i, f_num_i) for i, f_num_i in enumerate(f_num))) stiff_t = sum( op.weak_local_d_dx(dcoll, i, f_num_i) for i, f_num_i in enumerate(f_num)) kterm = op.mass(dcoll, dq, kappa * f_quad_num.dot(normal)) flux = op.face_mass(dcoll, face_f.dot(face_normal)) # sum everything up op_global = op.nodal_sum(dcoll, dd, stiff - (stiff_t + kterm)) op_local = op.elementwise_sum(dcoll, dd, stiff - (stiff_t + kterm + flux)) err_global = abs(op_global) err_local = op.norm(dcoll, op_local, np.inf) logger.info("errors: global %.5e local %.5e", err_global, err_local) # compute max element size from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(dcoll) eoc_global.add_data_point(h_max, actx.to_numpy(err_global)) eoc_local.add_data_point(h_max, err_local) if visualize: from grudge.shortcuts import make_visualizer vis = make_visualizer(dcoll) 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
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) dcoll = DiscretizationCollection(actx, mesh, order=builder.order) volume_discr = dcoll.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 ones_volm = volume_discr.zeros(actx) + 1 approx_surface_area = actx.to_numpy(op.integral(dcoll, dd, ones_volm)) logger.info("surface: got {:.5e} / expected {:.5e}".format( approx_surface_area, surface_area)) area_error = abs(approx_surface_area - surface_area) / abs(surface_area) # }}} # compute max element size from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(dcoll) eoc.add_data_point(h_max, area_error) # }}} logger.info("surface area error\n%s", str(eoc)) assert eoc.max_error() < 3e-13 or eoc.order_estimate() > builder.order
def test_grad_operator(actx_factory, dim, order, sym_test_func_factory): """Test the gradient operator for sanity. Check whether we get the right answers for gradients of analytic functions with some simple input fields and states: - constant - multilinear funcs - quadratic funcs - trig funcs - :class:`~mirgecom.fluid.ConservedVars` composed of funcs from above """ actx = actx_factory() sym_test_func = sym_test_func_factory(dim) tol = 1e-10 if dim < 3 else 1e-9 from pytools.convergence import EOCRecorder eoc = EOCRecorder() for nfac in [1, 2, 4]: npts_axis = (nfac * 4, ) * dim box_ll = (0, ) * dim box_ur = (1, ) * dim mesh = _get_box_mesh(dim, a=box_ll, b=box_ur, n=npts_axis) logger.info(f"Number of {dim}d elements: {mesh.nelements}") discr = EagerDGDiscretization(actx, mesh, order=order) # compute max element size from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(discr) def sym_eval(expr, x_vec): # FIXME: When pymbolic supports array containers mapper = sym.EvaluationMapper({"x": x_vec}) from arraycontext import rec_map_array_container result = rec_map_array_container(mapper, expr) # If expressions don't depend on coords (e.g., order 0), evaluated result # will be scalar-valued, so promote to DOFArray(s) before returning return result * (0 * x_vec[0] + 1) test_func = partial(sym_eval, sym_test_func) grad_test_func = partial(sym_eval, sym_grad(dim, sym_test_func)) nodes = thaw(actx, discr.nodes()) int_flux = partial(central_flux_interior, actx, discr) bnd_flux = partial(central_flux_boundary, actx, discr, test_func) test_data = test_func(nodes) exact_grad = grad_test_func(nodes) from mirgecom.simutil import componentwise_norms from arraycontext import flatten err_scale = max( flatten(componentwise_norms(discr, exact_grad, np.inf), actx)) if err_scale <= 1e-16: err_scale = 1 print(f"{test_data=}") print(f"{exact_grad=}") test_data_int_tpair = interior_trace_pair(discr, test_data) boundaries = [BTAG_ALL] test_data_flux_bnd = _elbnd_flux(discr, int_flux, bnd_flux, test_data_int_tpair, boundaries) from mirgecom.operators import grad_operator from grudge.dof_desc import as_dofdesc dd_vol = as_dofdesc("vol") dd_faces = as_dofdesc("all_faces") test_grad = grad_operator(discr, dd_vol, dd_faces, test_data, test_data_flux_bnd) print(f"{test_grad=}") grad_err = \ max(flatten(componentwise_norms(discr, test_grad - exact_grad, np.inf), actx)) / err_scale eoc.add_data_point(actx.to_numpy(h_max), actx.to_numpy(grad_err)) assert (eoc.order_estimate() >= order - 0.5 or eoc.max_error() < tol)
def test_poiseuille_fluxes(actx_factory, order, kappa): """Test the viscous fluxes using a Poiseuille input state.""" actx = actx_factory() dim = 2 from pytools.convergence import EOCRecorder e_eoc_rec = EOCRecorder() p_eoc_rec = EOCRecorder() base_pressure = 100000.0 pressure_ratio = 1.001 mu = 42 # arbitrary left_boundary_location = 0 right_boundary_location = 0.1 ybottom = 0. ytop = .02 nspecies = 0 spec_diffusivity = 0 * np.ones(nspecies) transport_model = SimpleTransport(viscosity=mu, thermal_conductivity=kappa, species_diffusivity=spec_diffusivity) xlen = right_boundary_location - left_boundary_location p_low = base_pressure p_hi = pressure_ratio * base_pressure dpdx = (p_low - p_hi) / xlen rho = 1.0 eos = IdealSingleGas() gas_model = GasModel(eos=eos, transport=transport_model) from mirgecom.initializers import PlanarPoiseuille initializer = PlanarPoiseuille(density=rho, mu=mu) def _elbnd_flux(discr, compute_interior_flux, compute_boundary_flux, int_tpair, boundaries): return (compute_interior_flux(int_tpair) + sum(compute_boundary_flux(btag) for btag in boundaries)) from mirgecom.flux import gradient_flux_central def cv_flux_interior(int_tpair): normal = thaw(actx, discr.normal(int_tpair.dd)) flux_weak = gradient_flux_central(int_tpair, normal) dd_all_faces = int_tpair.dd.with_dtag("all_faces") return discr.project(int_tpair.dd, dd_all_faces, flux_weak) def cv_flux_boundary(btag): boundary_discr = discr.discr_from_dd(btag) bnd_nodes = thaw(actx, boundary_discr.nodes()) cv_bnd = initializer(x_vec=bnd_nodes, eos=eos) bnd_nhat = thaw(actx, discr.normal(btag)) from grudge.trace_pair import TracePair bnd_tpair = TracePair(btag, interior=cv_bnd, exterior=cv_bnd) flux_weak = gradient_flux_central(bnd_tpair, bnd_nhat) dd_all_faces = bnd_tpair.dd.with_dtag("all_faces") return discr.project(bnd_tpair.dd, dd_all_faces, flux_weak) for nfac in [1, 2, 4]: npts_axis = nfac * (11, 21) box_ll = (left_boundary_location, ybottom) box_ur = (right_boundary_location, ytop) mesh = _get_box_mesh(2, a=box_ll, b=box_ur, n=npts_axis) logger.info(f"Number of {dim}d elements: {mesh.nelements}") discr = EagerDGDiscretization(actx, mesh, order=order) nodes = thaw(actx, discr.nodes()) def inf_norm(x): return actx.to_numpy(discr.norm(x, np.inf)) # compute max element size from grudge.dt_utils import h_max_from_volume h_max = h_max_from_volume(discr) # form exact cv cv = initializer(x_vec=nodes, eos=eos) cv_int_tpair = interior_trace_pair(discr, cv) boundaries = [BTAG_ALL] cv_flux_bnd = _elbnd_flux(discr, cv_flux_interior, cv_flux_boundary, cv_int_tpair, boundaries) from mirgecom.operators import grad_operator from grudge.dof_desc import as_dofdesc dd_vol = as_dofdesc("vol") dd_faces = as_dofdesc("all_faces") grad_cv = grad_operator(discr, dd_vol, dd_faces, cv, cv_flux_bnd) xp_grad_cv = initializer.exact_grad(x_vec=nodes, eos=eos, cv_exact=cv) xp_grad_v = 1 / cv.mass * xp_grad_cv.momentum xp_tau = mu * (xp_grad_v + xp_grad_v.transpose()) # sanity check the gradient: relerr_scale_e = 1.0 / inf_norm(xp_grad_cv.energy) relerr_scale_p = 1.0 / inf_norm(xp_grad_cv.momentum) graderr_e = inf_norm(grad_cv.energy - xp_grad_cv.energy) graderr_p = inf_norm(grad_cv.momentum - xp_grad_cv.momentum) graderr_e *= relerr_scale_e graderr_p *= relerr_scale_p assert graderr_e < 5e-7 assert graderr_p < 5e-11 zeros = discr.zeros(actx) ones = zeros + 1 pressure = eos.pressure(cv) # grad of p should be dp/dx xp_grad_p = make_obj_array([dpdx * ones, zeros]) grad_p = op.local_grad(discr, pressure) dpscal = 1.0 / np.abs(dpdx) temperature = eos.temperature(cv) tscal = rho * eos.gas_const() * dpscal xp_grad_t = xp_grad_p / (cv.mass * eos.gas_const()) grad_t = op.local_grad(discr, temperature) # sanity check assert inf_norm(grad_p - xp_grad_p) * dpscal < 5e-9 assert inf_norm(grad_t - xp_grad_t) * tscal < 5e-9 fluid_state = make_fluid_state(cv, gas_model) # verify heat flux from mirgecom.viscous import conductive_heat_flux heat_flux = conductive_heat_flux(fluid_state, grad_t) xp_heat_flux = -kappa * xp_grad_t assert inf_norm(heat_flux - xp_heat_flux) < 2e-8 xp_e_flux = np.dot(xp_tau, cv.velocity) - xp_heat_flux xp_mom_flux = xp_tau from mirgecom.viscous import viscous_flux vflux = viscous_flux(fluid_state, grad_cv, grad_t) efluxerr = (inf_norm(vflux.energy - xp_e_flux) / inf_norm(xp_e_flux)) momfluxerr = (inf_norm(vflux.momentum - xp_mom_flux) / inf_norm(xp_mom_flux)) assert inf_norm(vflux.mass) == 0 e_eoc_rec.add_data_point(actx.to_numpy(h_max), efluxerr) p_eoc_rec.add_data_point(actx.to_numpy(h_max), momfluxerr) assert (e_eoc_rec.order_estimate() >= order - 0.5 or e_eoc_rec.max_error() < 3e-9) assert (p_eoc_rec.order_estimate() >= order - 0.5 or p_eoc_rec.max_error() < 2e-12)