|
| 1 | +__copyright__ = "Copyright (C) 2022 Alexandru Fikl" |
| 2 | + |
| 3 | +__license__ = """ |
| 4 | +Permission is hereby granted, free of charge, to any person obtaining a copy |
| 5 | +of this software and associated documentation files (the "Software"), to deal |
| 6 | +in the Software without restriction, including without limitation the rights |
| 7 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 8 | +copies of the Software, and to permit persons to whom the Software is |
| 9 | +furnished to do so, subject to the following conditions: |
| 10 | +
|
| 11 | +The above copyright notice and this permission notice shall be included in |
| 12 | +all copies or substantial portions of the Software. |
| 13 | +
|
| 14 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 15 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 16 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 17 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 18 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 19 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 20 | +THE SOFTWARE. |
| 21 | +""" |
| 22 | + |
| 23 | +from dataclasses import dataclass |
| 24 | + |
| 25 | +import numpy as np |
| 26 | + |
| 27 | +from meshmode.array_context import PyOpenCLArrayContext |
| 28 | +from pytools.convergence import EOCRecorder |
| 29 | + |
| 30 | +from pytential import GeometryCollection, sym |
| 31 | + |
| 32 | +import logging |
| 33 | + |
| 34 | +logging.basicConfig(level=logging.INFO) |
| 35 | +logger = logging.getLogger(__name__) |
| 36 | + |
| 37 | + |
| 38 | +@dataclass(frozen=True) |
| 39 | +class Timings: |
| 40 | + build: float |
| 41 | + matvec: float |
| 42 | + |
| 43 | + |
| 44 | +def run_hmatrix_matvec( |
| 45 | + actx: PyOpenCLArrayContext, |
| 46 | + places: GeometryCollection, *, |
| 47 | + dofdesc: sym.DOFDescriptor) -> None: |
| 48 | + from sumpy.kernel import LaplaceKernel |
| 49 | + kernel = LaplaceKernel(places.ambient_dim) |
| 50 | + sym_u = sym.var("u") |
| 51 | + sym_op = -0.5 * sym_u + sym.D(kernel, sym_u, qbx_forced_limit="avg") |
| 52 | + |
| 53 | + density_discr = places.get_discretization(dofdesc.geometry, dofdesc.discr_stage) |
| 54 | + u = actx.thaw(density_discr.nodes()[0]) |
| 55 | + |
| 56 | + def build_hmat(): |
| 57 | + from pytential.linalg.hmatrix import build_hmatrix_by_proxy |
| 58 | + return build_hmatrix_by_proxy( |
| 59 | + actx, places, sym_op, sym_u, |
| 60 | + domains=[dofdesc], |
| 61 | + context={}, |
| 62 | + auto_where=dofdesc, |
| 63 | + id_eps=1.0e-10, |
| 64 | + _tree_kind="adaptive-level-restricted", |
| 65 | + _approx_nproxy=64, |
| 66 | + _proxy_radius_factor=1.15).get_forward() |
| 67 | + |
| 68 | + # warmup |
| 69 | + from pytools import ProcessTimer |
| 70 | + with ProcessTimer() as pt: |
| 71 | + hmat = build_hmat() |
| 72 | + actx.queue.finish() |
| 73 | + |
| 74 | + logger.info("build(warmup): %s", pt) |
| 75 | + |
| 76 | + # build |
| 77 | + with ProcessTimer() as pt: |
| 78 | + hmat = build_hmat() |
| 79 | + actx.queue.finish() |
| 80 | + |
| 81 | + t_build = pt.wall_elapsed |
| 82 | + logger.info("build: %s", pt) |
| 83 | + |
| 84 | + # matvec |
| 85 | + with ProcessTimer() as pt: |
| 86 | + du = hmat @ u |
| 87 | + assert du is not None |
| 88 | + actx.queue.finish() |
| 89 | + |
| 90 | + t_matvec = pt.wall_elapsed |
| 91 | + logger.info("matvec: %s", pt) |
| 92 | + |
| 93 | + return Timings(t_build, t_matvec) |
| 94 | + |
| 95 | + |
| 96 | +def run_scaling_study( |
| 97 | + ambient_dim: int, *, |
| 98 | + target_order: int = 4, |
| 99 | + source_ovsmp: int = 4, |
| 100 | + qbx_order: int = 4, |
| 101 | + ) -> None: |
| 102 | + dd = sym.DOFDescriptor(f"d{ambient_dim}", discr_stage=sym.QBX_SOURCE_STAGE2) |
| 103 | + |
| 104 | + import pyopencl as cl |
| 105 | + ctx = cl.create_some_context() |
| 106 | + queue = cl.CommandQueue(ctx) |
| 107 | + actx = PyOpenCLArrayContext(queue, force_device_scalars=True) |
| 108 | + |
| 109 | + eoc_build = EOCRecorder() |
| 110 | + eoc_matvec = EOCRecorder() |
| 111 | + |
| 112 | + import meshmode.mesh.generation as mgen |
| 113 | + import meshmode.discretization.poly_element as mpoly |
| 114 | + resolutions = [64, 128, 256, 512, 1024, 1536, 2048, 2560, 3072] |
| 115 | + |
| 116 | + for n in resolutions: |
| 117 | + mesh = mgen.make_curve_mesh( |
| 118 | + mgen.NArmedStarfish(5, 0.25), |
| 119 | + np.linspace(0, 1, n), |
| 120 | + order=target_order) |
| 121 | + |
| 122 | + from meshmode.discretization import Discretization |
| 123 | + pre_density_discr = Discretization(actx, mesh, |
| 124 | + mpoly.InterpolatoryQuadratureGroupFactory(target_order)) |
| 125 | + |
| 126 | + from pytential.qbx import QBXLayerPotentialSource |
| 127 | + qbx = QBXLayerPotentialSource( |
| 128 | + pre_density_discr, |
| 129 | + fine_order=source_ovsmp * target_order, |
| 130 | + qbx_order=qbx_order, |
| 131 | + fmm_order=False, fmm_backend=None, |
| 132 | + ) |
| 133 | + places = GeometryCollection(qbx, auto_where=dd.geometry) |
| 134 | + density_discr = places.get_discretization(dd.geometry, dd.discr_stage) |
| 135 | + |
| 136 | + logger.info("ndofs: %d", density_discr.ndofs) |
| 137 | + logger.info("nelements: %d", density_discr.mesh.nelements) |
| 138 | + |
| 139 | + timings = run_hmatrix_matvec(actx, places, dofdesc=dd) |
| 140 | + eoc_build.add_data_point(density_discr.ndofs, timings.build) |
| 141 | + eoc_matvec.add_data_point(density_discr.ndofs, timings.matvec) |
| 142 | + |
| 143 | + for name, eoc in [("build", eoc_build), ("matvec", eoc_matvec)]: |
| 144 | + logger.info("%s\n%s", |
| 145 | + name, eoc.pretty_print( |
| 146 | + abscissa_label="dofs", |
| 147 | + error_label=f"{name} (s)", |
| 148 | + abscissa_format="%d", |
| 149 | + error_format="%.3fs", |
| 150 | + eoc_format="%.2f", |
| 151 | + ) |
| 152 | + ) |
| 153 | + visualize_eoc(f"scaling-study-hmatrix-{name}", eoc, 1) |
| 154 | + |
| 155 | + |
| 156 | +def visualize_eoc( |
| 157 | + filename: str, eoc: EOCRecorder, order: int, |
| 158 | + overwrite: bool = False) -> None: |
| 159 | + try: |
| 160 | + import matplotlib.pyplot as plt |
| 161 | + except ImportError: |
| 162 | + logger.info("matplotlib not available for plotting") |
| 163 | + return |
| 164 | + |
| 165 | + fig = plt.figure(figsize=(10, 10), dpi=300) |
| 166 | + ax = fig.gca() |
| 167 | + |
| 168 | + h, error = np.array(eoc.history).T # type: ignore[no-untyped-call] |
| 169 | + ax.loglog(h, error, "o-") |
| 170 | + |
| 171 | + max_h = np.max(h) |
| 172 | + min_e = np.min(error) |
| 173 | + max_e = np.max(error) |
| 174 | + min_h = np.exp(np.log(max_h) + np.log(min_e / max_e) / order) |
| 175 | + |
| 176 | + ax.loglog( |
| 177 | + [max_h, min_h], [max_e, min_e], "k-", label=rf"$\mathcal{{O}}(h^{order})$" |
| 178 | + ) |
| 179 | + |
| 180 | + # }}} |
| 181 | + |
| 182 | + ax.grid(True, which="major", linestyle="-", alpha=0.75) |
| 183 | + ax.grid(True, which="minor", linestyle="--", alpha=0.5) |
| 184 | + |
| 185 | + ax.set_xlabel("$N$") |
| 186 | + ax.set_ylabel("$T~(s)$") |
| 187 | + |
| 188 | + import pathlib |
| 189 | + filename = pathlib.Path(filename) |
| 190 | + if not overwrite and filename.exists(): |
| 191 | + raise FileExistsError(f"output file '{filename}' already exists") |
| 192 | + |
| 193 | + fig.savefig(filename) |
| 194 | + plt.close(fig) |
| 195 | + |
| 196 | + |
| 197 | +if __name__ == "__main__": |
| 198 | + run_scaling_study(ambient_dim=2) |
0 commit comments