Skip to content

Commit 5d37d51

Browse files
committed
implement hierachical matrix solver
1 parent 3d8faf1 commit 5d37d51

File tree

13 files changed

+1720
-56
lines changed

13 files changed

+1720
-56
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ examples/*.pdf
2121

2222
*.vtu
2323
*.vts
24+
*.png
25+
*.gif
2426

2527
.cache
2628

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
}
3636

3737
nitpick_ignore_regex = [
38+
["py:class", r".*_ProxyNeighborEvaluationResult"],
3839
# Sphinx started complaining about these in 8.2.1(-ish)
3940
# -AK, 2025-02-24
4041
["py:class", r"TypeAliasForwardRef"],

examples/scaling-study-hmatrix.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
import logging
24+
from dataclasses import dataclass
25+
26+
import numpy as np
27+
28+
from meshmode.array_context import PyOpenCLArrayContext
29+
from pytools.convergence import EOCRecorder
30+
31+
from pytential import GeometryCollection, sym
32+
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)
108+
109+
eoc_build = EOCRecorder()
110+
eoc_matvec = EOCRecorder()
111+
112+
import meshmode.discretization.poly_element as mpoly
113+
import meshmode.mesh.generation as mgen
114+
115+
resolutions = [64, 128, 256, 512, 1024, 1536, 2048, 2560, 3072]
116+
117+
for n in resolutions:
118+
mesh = mgen.make_curve_mesh(
119+
mgen.NArmedStarfish(5, 0.25),
120+
np.linspace(0, 1, n),
121+
order=target_order)
122+
123+
from meshmode.discretization import Discretization
124+
pre_density_discr = Discretization(actx, mesh,
125+
mpoly.InterpolatoryQuadratureGroupFactory(target_order))
126+
127+
from pytential.qbx import QBXLayerPotentialSource
128+
qbx = QBXLayerPotentialSource(
129+
pre_density_discr,
130+
fine_order=source_ovsmp * target_order,
131+
qbx_order=qbx_order,
132+
fmm_order=False, fmm_backend=None,
133+
)
134+
places = GeometryCollection(qbx, auto_where=dd.geometry)
135+
density_discr = places.get_discretization(dd.geometry, dd.discr_stage)
136+
137+
logger.info("ndofs: %d", density_discr.ndofs)
138+
logger.info("nelements: %d", density_discr.mesh.nelements)
139+
140+
timings = run_hmatrix_matvec(actx, places, dofdesc=dd)
141+
eoc_build.add_data_point(density_discr.ndofs, timings.build)
142+
eoc_matvec.add_data_point(density_discr.ndofs, timings.matvec)
143+
144+
for name, eoc in [("build", eoc_build), ("matvec", eoc_matvec)]:
145+
logger.info("%s\n%s",
146+
name, eoc.pretty_print(
147+
abscissa_label="dofs",
148+
error_label=f"{name} (s)",
149+
abscissa_format="%d",
150+
error_format="%.3fs",
151+
eoc_format="%.2f",
152+
)
153+
)
154+
visualize_eoc(f"scaling-study-hmatrix-{name}", eoc, 1)
155+
156+
157+
def visualize_eoc(
158+
filename: str, eoc: EOCRecorder, order: int,
159+
overwrite: bool = False) -> None:
160+
try:
161+
import matplotlib.pyplot as plt
162+
except ImportError:
163+
logger.info("matplotlib not available for plotting")
164+
return
165+
166+
fig = plt.figure(figsize=(10, 10), dpi=300)
167+
ax = fig.gca()
168+
169+
h, error = np.array(eoc.history).T # type: ignore[no-untyped-call]
170+
ax.loglog(h, error, "o-")
171+
172+
max_h = np.max(h)
173+
min_e = np.min(error)
174+
max_e = np.max(error)
175+
min_h = np.exp(np.log(max_h) + np.log(min_e / max_e) / order)
176+
177+
ax.loglog(
178+
[max_h, min_h], [max_e, min_e], "k-", label=rf"$\mathcal{{O}}(h^{order})$"
179+
)
180+
181+
# }}}
182+
183+
ax.grid(True, which="major", linestyle="-", alpha=0.75)
184+
ax.grid(True, which="minor", linestyle="--", alpha=0.5)
185+
186+
ax.set_xlabel("$N$")
187+
ax.set_ylabel("$T~(s)$")
188+
189+
import pathlib
190+
filename = pathlib.Path(filename)
191+
if not overwrite and filename.exists():
192+
raise FileExistsError(f"output file '{filename}' already exists")
193+
194+
fig.savefig(filename)
195+
plt.close(fig)
196+
197+
198+
if __name__ == "__main__":
199+
run_scaling_study(ambient_dim=2)

pytential/linalg/cluster.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def cluster(obj: object, clevel: ClusterLevel) -> Any:
208208

209209

210210
@cluster.register(IndexList)
211-
def _cluster_index_list(obj: IndexList, clevel: ClusterLevel) -> IndexList:
211+
def cluster_index_list(obj: IndexList, clevel: ClusterLevel) -> IndexList:
212212
assert obj.nclusters == clevel.nclusters
213213

214214
if clevel.nclusters == 1:
@@ -224,7 +224,7 @@ def _cluster_index_list(obj: IndexList, clevel: ClusterLevel) -> IndexList:
224224

225225

226226
@cluster.register(TargetAndSourceClusterList)
227-
def _cluster_target_and_source_cluster_list(
227+
def cluster_target_and_source_cluster_list(
228228
obj: TargetAndSourceClusterList, clevel: ClusterLevel,
229229
) -> TargetAndSourceClusterList:
230230
assert obj.nclusters == clevel.nclusters
@@ -238,7 +238,7 @@ def _cluster_target_and_source_cluster_list(
238238

239239

240240
@cluster.register(np.ndarray)
241-
def _cluster_ndarray(obj: NDArray[Any], clevel: ClusterLevel) -> NDArray[Any]:
241+
def cluster_ndarray(obj: NDArray[Any], clevel: ClusterLevel) -> NDArray[Any]:
242242
assert obj.shape == (clevel.nclusters,)
243243
if clevel.nclusters == 1:
244244
return obj
@@ -420,26 +420,14 @@ def partition_by_nodes(
420420

421421
# {{{ visualize clusters
422422

423-
def visualize_clusters(actx: PyOpenCLArrayContext,
424-
generator: ProxyGenerator,
425-
srcindex: IndexList,
426-
tree: ClusterTree,
427-
filename: str | pathlib.Path, *,
428-
dofdesc: sym.DOFDescriptorLike = None,
429-
overwrite: bool = False) -> None:
430-
filename = pathlib.Path(filename)
431-
432-
places = generator.places
433-
if dofdesc is None:
434-
dofdesc = places.auto_source
435-
dofdesc = sym.as_dofdesc(dofdesc)
436-
437-
discr = places.get_discretization(dofdesc.geometry, dofdesc.discr_stage)
438-
assert isinstance(discr, Discretization)
439-
440-
if discr.ambient_dim != 2:
441-
raise NotImplementedError(f"Unsupported dimension: {discr.ambient_dim}")
442-
423+
def _visualize_clusters_2d(actx: PyOpenCLArrayContext,
424+
discr: Discretization,
425+
generator: ProxyGenerator,
426+
srcindex: IndexList,
427+
tree: ClusterTree,
428+
filename: pathlib.Path, *,
429+
dofdesc: sym.DOFDescriptor,
430+
overwrite: bool = False) -> None:
443431
import matplotlib.pyplot as pt
444432

445433
from arraycontext import flatten
@@ -448,7 +436,7 @@ def visualize_clusters(actx: PyOpenCLArrayContext,
448436

449437
x, y = actx.to_numpy(flatten(discr.nodes(), actx, leaf_class=DOFArray))
450438
for clevel in tree.levels:
451-
outfile = filename.with_stem(f"{filename.stem}-{clevel.level:03d}")
439+
outfile = filename.with_stem(f"{filename.stem}-lvl{clevel.level:03d}")
452440
if not overwrite and outfile.exists():
453441
raise FileExistsError(f"Output file '{outfile}' already exists")
454442

@@ -493,6 +481,102 @@ def visualize_clusters(actx: PyOpenCLArrayContext,
493481

494482
srcindex = cluster(srcindex, clevel)
495483

484+
485+
def _visualize_clusters_3d(actx: PyOpenCLArrayContext,
486+
discr: Discretization,
487+
generator: ProxyGenerator,
488+
srcindex: IndexList,
489+
tree: ClusterTree,
490+
filename: pathlib.Path, *,
491+
dofdesc: sym.DOFDescriptor,
492+
overwrite: bool = False) -> None:
493+
from arraycontext import unflatten
494+
from meshmode.discretization.visualization import make_visualizer
495+
496+
for clevel in tree.levels:
497+
outfile = filename.with_stem(f"{filename.stem}-lvl{clevel.level:03d}")
498+
outfile = outfile.with_suffix(".vtu")
499+
if not overwrite and outfile.exists():
500+
raise FileExistsError(f"Output file '{outfile}' already exists")
501+
502+
# construct proxy balls
503+
pxy = generator(actx, dofdesc, srcindex).to_numpy(actx)
504+
pxycenters = pxy.centers
505+
pxyradii = pxy.radii
506+
nclusters = srcindex.nclusters
507+
508+
# construct meshes for each proxy ball
509+
from meshmode.mesh.generation import generate_sphere
510+
from meshmode.mesh.processing import affine_map, merge_disjoint_meshes
511+
512+
ref_mesh = generate_sphere(1, 4, uniform_refinement_rounds=1)
513+
pxymeshes = [
514+
affine_map(ref_mesh, A=pxyradii[i], b=pxycenters[:, i].squeeze())
515+
for i in range(nclusters)
516+
]
517+
518+
# merge meshes into a single discretization
519+
from meshmode.discretization.poly_element import (
520+
InterpolatoryEdgeClusteredGroupFactory,
521+
)
522+
pxymesh = merge_disjoint_meshes([discr.mesh, *pxymeshes])
523+
pxydiscr = Discretization(actx, pxymesh,
524+
InterpolatoryEdgeClusteredGroupFactory(4))
525+
526+
# add a marker field for all clusters
527+
marker = np.full((pxydiscr.ndofs,), np.nan, dtype=np.float64)
528+
template_ary = actx.thaw(pxydiscr.nodes()[0])
529+
530+
for i in range(srcindex.nclusters):
531+
isrc = srcindex.cluster_indices(i)
532+
marker[isrc] = 10.0 * (i + 1.0)
533+
marker_dev = unflatten(template_ary, actx.from_numpy(marker), actx)
534+
535+
# add a marker field for all proxies
536+
pxymarker = np.full((pxydiscr.ndofs,), np.nan, dtype=np.float64)
537+
pxymarker[discr.ndofs:] = 1.0
538+
pxymarker_dev = unflatten(template_ary, actx.from_numpy(pxymarker), actx)
539+
540+
# write it all out
541+
vis = make_visualizer(actx, pxydiscr)
542+
vis.write_vtk_file(outfile, [
543+
("marker", marker_dev),
544+
("proxies", pxymarker_dev),
545+
], overwrite=overwrite)
546+
547+
srcindex = cluster(srcindex, clevel)
548+
549+
550+
def visualize_clusters(actx: PyOpenCLArrayContext,
551+
generator: ProxyGenerator,
552+
srcindex: IndexList,
553+
tree: ClusterTree,
554+
filename: str | pathlib.Path, *,
555+
dofdesc: sym.DOFDescriptorLike = None,
556+
overwrite: bool = False) -> None:
557+
filename = pathlib.Path(filename)
558+
559+
places = generator.places
560+
if dofdesc is None:
561+
dofdesc = places.auto_source
562+
dofdesc = sym.as_dofdesc(dofdesc)
563+
564+
discr = places.get_discretization(dofdesc.geometry, dofdesc.discr_stage)
565+
assert isinstance(discr, Discretization)
566+
567+
if discr.ambient_dim == 2:
568+
_visualize_clusters_2d(
569+
actx, discr, generator, srcindex, tree, filename,
570+
dofdesc=dofdesc,
571+
overwrite=overwrite)
572+
elif discr.ambient_dim == 3:
573+
_visualize_clusters_3d(
574+
actx, discr, generator, srcindex, tree, filename,
575+
dofdesc=dofdesc,
576+
overwrite=overwrite)
577+
else:
578+
raise NotImplementedError(f"Unsupported dimension: {discr.ambient_dim}")
579+
496580
# }}}
497581

498582

0 commit comments

Comments
 (0)