From 19803f9d13727c0469f0698714e7c2c3093f537c Mon Sep 17 00:00:00 2001 From: "[6~" <inform@tiker.net> Date: Fri, 8 May 2020 01:53:46 -0500 Subject: [PATCH] Add executing (but not yet working) cartoon DG solver --- examples/simple-dg.py | 509 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 examples/simple-dg.py diff --git a/examples/simple-dg.py b/examples/simple-dg.py new file mode 100644 index 00000000..dc780f26 --- /dev/null +++ b/examples/simple-dg.py @@ -0,0 +1,509 @@ +from __future__ import division, print_function + +__copyright__ = "Copyright (C) 2020 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 pyopencl as cl +import pyopencl.array as cla # noqa +import pyopencl.clmath as clmath +from pytools import memoize_method, memoize_in +from pytools.obj_array import ( + join_fields, make_obj_array, + with_object_array_or_scalar, + is_obj_array) +import loopy as lp +from meshmode.mesh import BTAG_ALL # noqa + + +# Features lost vs. https://github.com/inducer/grudge: +# - dimension independence / differential geometry +# - overintegration +# - operator fusion +# - distributed-memory + + +def with_queue(queue, ary): + return with_object_array_or_scalar( + lambda x: x.with_queue(queue), ary) + + +def without_queue(ary): + return with_queue(None, ary) + + +# {{{ discretization + +def parametrization_derivative(queue, discr): + result = np.zeros((discr.ambient_dim, discr.dim), dtype=object) + for iambient in range(discr.ambient_dim): + for idim in range(discr.dim): + result[iambient, idim] = discr.num_reference_derivative( + queue, (idim,), discr.nodes()[iambient]) + + return result + + +class DGDiscretization: + def __init__(self, cl_ctx, mesh, order): + self.order = order + + from meshmode.discretization import Discretization + from meshmode.discretization.poly_element import \ + PolynomialWarpAndBlendGroupFactory + self.group_factory = PolynomialWarpAndBlendGroupFactory(order=order) + self.volume_discr = Discretization(cl_ctx, mesh, self.group_factory) + + assert self.volume_discr.dim == 2 + + @property + def cl_context(self): + return self.volume_discr.cl_context + + @property + def dim(self): + return self.volume_discr.dim + + # {{{ discretizations/connections + + @memoize_method + def boundary_connection(self, boundary_tag): + from meshmode.discretization.connection import make_face_restriction + return make_face_restriction( + self.volume_discr, + self.group_factory, + boundary_tag=boundary_tag) + + @memoize_method + def interior_faces_connection(self): + from meshmode.discretization.connection import ( + make_face_restriction, FACE_RESTR_INTERIOR) + return make_face_restriction( + self.volume_discr, + self.group_factory, + FACE_RESTR_INTERIOR, + per_face_groups=False) + + @memoize_method + def opposite_face_connection(self): + from meshmode.discretization.connection import \ + make_opposite_face_connection + + return make_opposite_face_connection(self.interior_faces_connection()) + + @memoize_method + def all_faces_connection(self): + from meshmode.discretization.connection import ( + make_face_restriction, FACE_RESTR_ALL) + return make_face_restriction( + self.volume_discr, + self.group_factory, + FACE_RESTR_ALL, + per_face_groups=False) + + @memoize_method + def get_to_all_face_embedding(self, where): + from meshmode.discretization.connection import \ + make_face_to_all_faces_embedding + + faces_conn = self.get_connection("vol", where) + return make_face_to_all_faces_embedding( + faces_conn, self.get_discr("all_faces")) + + def get_connection(self, src, tgt): + src_tgt = (src, tgt) + + if src_tgt == ("vol", "int_faces"): + return self.interior_faces_connection() + elif src_tgt == ("vol", "all_faces"): + return self.all_faces_connection() + elif src_tgt == ("int_faces", "all_faces"): + return self.get_to_all_face_embedding(src) + else: + raise ValueError(f"locations '{src}'->'{tgt}' not understood") + + def interp(self, src, tgt, vec): + if is_obj_array(vec): + return with_object_array_or_scalar( + lambda el: self.interp(src, tgt, el), vec) + + return self.get_connection(src, tgt)(vec.queue, vec) + + def get_discr(self, where): + if where == "vol": + return self.volume_discr + elif where == "all_faces": + return self.all_faces_connection().to_discr + elif where == "int_faces": + return self.interior_faces_connection().to_discr + else: + raise ValueError(f"location '{where}' not understood") + + # }}} + + @memoize_method + def parametrization_derivative(self): + with cl.CommandQueue(self.cl_context) as queue: + return without_queue( + parametrization_derivative(queue, self.volume_discr)) + + @memoize_method + def vol_jacobian(self): + with cl.CommandQueue(self.cl_context) as queue: + [a, b], [c, d] = with_queue(queue, self.parametrization_derivative()) + return (a*d-b*c).with_queue(None) + + @memoize_method + def inverse_parametrization_derivative(self): + with cl.CommandQueue(self.cl_context) as queue: + [a, b], [c, d] = with_queue(queue, self.parametrization_derivative()) + + result = np.zeros((2, 2), dtype=object) + det = a*d-b*c + result[0, 0] = d/det + result[0, 1] = -b/det + result[1, 0] = -c/det + result[1, 1] = a/det + + return without_queue(result) + + def zeros(self, queue): + return self.volume_discr.zeros(queue) + + def grad(self, vec): + ipder = self.inverse_parametrization_derivative() + + queue = vec.queue + dref = [ + self.volume_discr.num_reference_derivative( + queue, (idim,), vec).with_queue(queue) + for idim in range(self.volume_discr.dim)] + + return make_obj_array([ + sum(dref_i*ipder_i for dref_i, ipder_i in zip(dref, ipder[iambient])) + for iambient in range(self.volume_discr.ambient_dim)]) + + def div(self, vecs): + return sum( + self.grad(vec_i)[i] for i, vec_i in enumerate(vecs)) + + @memoize_method + def normal(self, where): + bdry_discr = self.get_discr(where) + + with cl.CommandQueue(self.cl_context) as queue: + ((a,), (b,)) = with_queue( + queue, parametrization_derivative(queue, bdry_discr)) + + return without_queue(join_fields(b, -a)) + + @memoize_method + def face_jacobian(self, where): + bdry_discr = self.get_discr(where) + + with cl.CommandQueue(self.cl_context) as queue: + ((a,), (b,)) = with_queue(queue, + parametrization_derivative(queue, bdry_discr)) + + return ((a**2 + b**2)**0.5).with_queue(None) + + @memoize_method + def get_inverse_mass_matrix(self, grp, dtype): + import modepy as mp + matrix = mp.inverse_mass_matrix( + grp.basis(), + grp.unit_nodes) + + with cl.CommandQueue(self.cl_context) as queue: + return (cla.to_device(queue, matrix) + .with_queue(None)) + + def inverse_mass(self, vec): + if is_obj_array(vec): + return with_object_array_or_scalar( + lambda el: self.face_mass(el), vec) + + @memoize_in(self, "elwise_linear_knl") + def knl(): + knl = lp.make_kernel( + """{[k,i,j]: + 0<=k<nelements and + 0<=i<ndiscr_nodes_out and + 0<=j<ndiscr_nodes_in}""", + "result[k,i] = sum(j, mat[i, j] * vec[k, j])", + default_offset=lp.auto, name="diff") + + knl = lp.split_iname(knl, "i", 16, inner_tag="l.0") + return lp.tag_inames(knl, dict(k="g.0")) + + discr = self.volume_discr + + result = discr.empty(queue=self.queue, dtype=vec.dtype) + + for grp in discr.groups: + matrix = self.get_inverse_mass_matrix(grp, vec.dtype) + + knl()(self.queue, mat=matrix, result=grp.view(result), + vec=grp.view(vec)) + + return result/self.vol_jacobian() + + @memoize_method + def get_local_face_mass_matrix(self, afgrp, volgrp, dtype): + nfaces = volgrp.mesh_el_group.nfaces + assert afgrp.nelements == nfaces * volgrp.nelements + + matrix = np.empty( + (volgrp.nunit_nodes, + nfaces, + afgrp.nunit_nodes), + dtype=dtype) + + from modepy.tools import UNIT_VERTICES + import modepy as mp + for iface, fvi in enumerate( + volgrp.mesh_el_group.face_vertex_indices()): + face_vertices = UNIT_VERTICES[volgrp.dim][np.array(fvi)].T + matrix[:, iface, :] = mp.nodal_face_mass_matrix( + volgrp.basis(), volgrp.unit_nodes, afgrp.unit_nodes, + volgrp.order, + face_vertices) + + with cl.CommandQueue(self.cl_context) as queue: + return (cla.to_device(queue, matrix) + .with_queue(None)) + + def face_mass(self, vec): + if is_obj_array(vec): + return with_object_array_or_scalar( + lambda el: self.face_mass(el), vec) + + @memoize_in(self, "face_mass_knl") + def knl(): + knl = lp.make_kernel( + """{[k,i,f,j]: + 0<=k<nelements and + 0<=f<nfaces and + 0<=i<nvol_nodes and + 0<=j<nface_nodes}""", + "result[k,i] = sum(f, sum(j, mat[i, f, j] * vec[f, k, j]))", + default_offset=lp.auto, name="face_mass") + + knl = lp.split_iname(knl, "i", 16, inner_tag="l.0") + return lp.tag_inames(knl, dict(k="g.0")) + + all_faces_conn = self.get_connection("vol", "all_faces") + all_faces_discr = all_faces_conn.to_discr + vol_discr = all_faces_conn.from_discr + + result = vol_discr.empty(queue=vec.queue, dtype=vec.dtype) + + assert len(all_faces_discr.groups) == len(vol_discr.groups) + + for afgrp, volgrp in zip(all_faces_discr.groups, vol_discr.groups): + nfaces = volgrp.mesh_el_group.nfaces + + matrix = self.get_local_face_mass_matrix(afgrp, volgrp, vec.dtype) + + input_view = afgrp.view(vec).reshape( + nfaces, volgrp.nelements, afgrp.nunit_nodes) + knl()(vec.queue, mat=matrix, result=volgrp.view(result), + vec=input_view) + + return result*self.face_jacobian("all_faces") + +# }}} + + +# {{{ trace pair + +class TracePair: + def __init__(self, where, interior, exterior): + self.where = where + self.interior = interior + self.exterior = exterior + + def __getitem__(self, index): + return TracePair( + self.where, + self.exterior[index], + self.interior[index]) + + def __len__(self): + assert len(self.exterior) == len(self.interior) + return len(self.exterior) + + @property + def int(self): + return self.interior + + @property + def ext(self): + return self.exterior + + @property + def avg(self): + return 0.5*(self.int + self.ext) + + +def interior_trace_pair(discr, vec): + i = discr.interp("vol", "int_faces", vec) + e = with_object_array_or_scalar( + lambda el: discr.opposite_face_connection()(el.queue, el), + i) + return TracePair("int_faces", i, e) + +# }}} + + +# {{{ wave equation bits + +def wave_flux(discr, c, w_tpair): + u = w_tpair[0] + v = w_tpair[1:] + + normal = with_queue(u.int.queue, discr.normal(w_tpair.where)) + + flux_weak = join_fields( + np.dot(v.avg, normal), + normal[0] * u.avg, + normal[1] * u.avg) + + # upwind + v_jump = np.dot(normal, v.int-v.ext) + flux_weak -= join_fields( + 0.5*(u.int-u.ext), + 0.5*normal[0]*v_jump, + 0.5*normal[1]*v_jump, + ) + + flux_strong = join_fields( + np.dot(v.int, normal), + u.int * normal[0], + u.int * normal[1], + ) - flux_weak + + return discr.interp(w_tpair.where, "all_faces", c*flux_strong) + + +def wave_operator(discr, c, w): + u = w[0] + v = w[1:] + + # dir_u = discr.interp("vol", BTAG_ALL, u) + # dir_v = discr.interp("vol", BTAG_ALL, v) + # dir_bc = join_fields(-dir_u, dir_v) + + result = ( + - join_fields( + -c*discr.div(v), + -c*discr.grad(u) + ) + + # noqa: W504 + discr.inverse_mass( + discr.face_mass( + wave_flux(discr, c=c, w_tpair=interior_trace_pair(discr, w)) + #+ flux(sym.bv_tpair(self.dirichlet_tag, w, dir_bc)) + )) + ) + + return result + +# }}} + + +def rk4_step(y, t, h, f): + k1 = f(t, y) + k2 = f(t+h/2, y + h/2*k1) + k3 = f(t+h/2, y + h/2*k2) + k4 = f(t+h, y + h*k3) + return y + h/6*(k1 + 2*k2 + 2*k3 + k4) + + +def main(): + cl_ctx = cl.create_some_context() + queue = cl.CommandQueue(cl_ctx) + + from meshmode.mesh.generation import generate_regular_rect_mesh + mesh = generate_regular_rect_mesh( + a=(-0.5, -0.5), + b=(0.5, 0.5), + n=(16, 16)) + + dt = 0.04 + + print("%d elements" % mesh.nelements) + + discr = DGDiscretization(cl_ctx, mesh, order=4) + + fields = join_fields(discr.zeros(queue), + [discr.zeros(queue) for i in range(discr.dim)]) + + from meshmode.discretization.visualization import make_visualizer + vis = make_visualizer(queue, discr.volume_discr, discr.order) + + source_center = np.array([0.0, 0.05]) + source_width = 0.05 + source_omega = 3 + + def rhs(t, w): + queue = w[0].queue + + nodes = discr.volume_discr.nodes().with_queue(queue) + center_dist = join_fields([ + nodes[0] - source_center[0], + nodes[1] - source_center[1], + ]) + + source_f = ( + np.sin(source_omega*t) + * clmath.exp( + -np.dot(center_dist, center_dist) + / source_width**2)) + result = wave_operator(discr, c=1, w=w) + if t < 0.3: + result[0] = result[0] + source_f + + return result + + t = 0 + t_final = 5 + istep = 0 + while t < t_final: + fields = rk4_step(fields, t, dt, rhs) + + if istep % 2 == 0: + vis.write_vtk_file("fld-wave-min-%04d.vtu" % istep, + [ + ("u", fields[0]), + ("v", fields[1:]), + ]) + + t += dt + istep += 1 + + +if __name__ == "__main__": + main() + +# vim: foldmethod=marker -- GitLab