Skip to content

Commit 5e7f654

Browse files
committed
implement hierachical matrix solver
1 parent b44f4a8 commit 5e7f654

File tree

5 files changed

+356
-7
lines changed

5 files changed

+356
-7
lines changed

pytential/linalg/cluster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class ClusterTree:
129129
def nclusters(self):
130130
return self.leaf_cluster_box_ids.size
131131

132-
def levels(self) -> Iterator[ClusterLevel]:
132+
def levels(self, root: bool = False) -> Iterator[ClusterLevel]:
133133
"""
134134
:returns: an iterator over all the :class:`ClusterLevel` levels.
135135
"""
@@ -142,7 +142,7 @@ def levels(self) -> Iterator[ClusterLevel]:
142142
parent_map=make_cluster_parent_map(parent_ids),
143143
)
144144

145-
for _ in range(self.nlevels - 1, 0, -1):
145+
for _ in range(self.nlevels - 1, -int(root), -1):
146146
yield clevel
147147

148148
box_ids = np.unique(self.tree_cluster_parent_ids[clevel.box_ids])

pytential/linalg/hmatrix.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
from typing import Any, Dict, Iterable, Optional, Union
25+
26+
import numpy as np
27+
28+
from arraycontext import PyOpenCLArrayContext, ArrayOrContainerT
29+
from meshmode.dof_array import DOFArray
30+
31+
from pytential import GeometryCollection, sym
32+
from pytential.linalg.cluster import ClusterTree, cluster
33+
34+
__doc__ = """
35+
Hierarical Matrix Construction
36+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37+
"""
38+
39+
40+
# {{{ ProxyHierarchicalMatrix
41+
42+
@dataclass(frozen=True)
43+
class ProxyHierarchicalMatrix:
44+
"""
45+
.. attribute:: ctree
46+
47+
A :class:`~pytential.linalg.cluster.ClusterTree`.
48+
49+
.. attribute:: skeletons
50+
51+
An :class:`~numpy.ndarray` containing skeletonization information
52+
for each level of the hierarchy. For additional details, see
53+
:class:`~pytential.linalg.skeletonization.SkeletonizationResult`.
54+
55+
This class implements the :class:`scipy.sparse.linalg.LinearOperator`
56+
interface. In particular, the following attributes and methods:
57+
58+
.. attribute:: shape
59+
60+
A :class:`tuple` that gives the matrix size ``(m, n)``.
61+
62+
.. attribute:: dtype
63+
64+
The data type of the matrix entries.
65+
66+
.. automethod:: matvec
67+
.. automethod:: __matmul__
68+
"""
69+
70+
ctree: ClusterTree
71+
skeletons: np.ndarray
72+
73+
@property
74+
def shape(self):
75+
return self.skeletons[0].tgt_src_index.shape
76+
77+
@property
78+
def dtype(self):
79+
# FIXME: assert that everyone has this dtype?
80+
return self.skeletons[0].R[0].dtype
81+
82+
@property
83+
def nlevels(self):
84+
return self.skeletons.size
85+
86+
def matvec(self, x: ArrayOrContainerT) -> ArrayOrContainerT:
87+
"""Implements a matrix-vector multiplication :math:`H x`."""
88+
from arraycontext import get_container_context_recursively_opt
89+
actx = get_container_context_recursively_opt(x)
90+
if actx is None:
91+
raise ValueError("input array is frozen")
92+
93+
return apply_skeleton_matvec(actx, self, x)
94+
95+
def __matmul__(self, x: ArrayOrContainerT) -> ArrayOrContainerT:
96+
"""Same as :meth:`matvec`."""
97+
return self.matvec(x)
98+
99+
def rmatvec(self, x):
100+
raise NotImplementedError
101+
102+
def matmat(self, mat):
103+
raise NotImplementedError
104+
105+
def rmatmat(self, mat):
106+
raise NotImplementedError
107+
108+
109+
def apply_skeleton_matvec(
110+
actx: PyOpenCLArrayContext,
111+
hmat: ProxyHierarchicalMatrix,
112+
x: ArrayOrContainerT,
113+
) -> ArrayOrContainerT:
114+
from arraycontext import flatten
115+
x = actx.to_numpy(flatten(x, actx, leaf_class=DOFArray))
116+
117+
from pytential.linalg.utils import split_array
118+
y = split_array(x, hmat.skeletons[0].tgt_src_index.sources)
119+
120+
assert x.dtype == hmat.dtype
121+
assert x.shape == (hmat.shape[1],)
122+
123+
d_dot_y = np.empty(hmat.nlevels, dtype=object)
124+
r_dot_y = np.empty(hmat.nlevels, dtype=object)
125+
126+
# recurse down
127+
for k, clevel in enumerate(hmat.ctree.levels(root=True)):
128+
skeleton = hmat.skeletons[k]
129+
assert skeleton.tgt_src_index.shape[1] == sum(xi.size for xi in y)
130+
131+
d_dot_y_k = np.empty(skeleton.nclusters, dtype=object)
132+
r_dot_y_k = np.empty(skeleton.nclusters, dtype=object)
133+
134+
for i in range(skeleton.nclusters):
135+
r_dot_y_k[i] = skeleton.R[i] @ y[i]
136+
d_dot_y_k[i] = skeleton.D[i] @ y[i]
137+
138+
r_dot_y[k] = r_dot_y_k
139+
d_dot_y[k] = d_dot_y_k
140+
y = cluster(r_dot_y_k, clevel)
141+
142+
# recurse up
143+
for k, skeleton in reversed(list(enumerate(hmat.skeletons))):
144+
r_dot_y_k = r_dot_y[k]
145+
d_dot_y_k = d_dot_y[k]
146+
147+
result = np.empty(skeleton.nclusters, dtype=object)
148+
for i in range(skeleton.nclusters):
149+
result[i] = skeleton.L[i] @ r_dot_y_k[i] + d_dot_y_k[i]
150+
151+
from arraycontext import unflatten
152+
return unflatten(
153+
x,
154+
actx.from_numpy(np.concatenate(result)),
155+
actx)
156+
157+
# }}}
158+
159+
160+
# {{{ build_hmatrix_matvec_by_proxy
161+
162+
def build_hmatrix_matvec_by_proxy(
163+
actx: PyOpenCLArrayContext,
164+
places: GeometryCollection,
165+
exprs: Union[sym.Expression, Iterable[sym.Expression]],
166+
input_exprs: Union[sym.Expression, Iterable[sym.Expression]], *,
167+
domains: Optional[Iterable[sym.DOFDescriptorLike]] = None,
168+
context: Optional[Dict[str, Any]] = None,
169+
id_eps: float = 1.0e-8,
170+
171+
# NOTE: these are dev variables and can disappear at any time!
172+
# TODO: plugin in error model to get an estimate for:
173+
# * how many points we want per cluster?
174+
# * how many proxy points we want?
175+
# * how far away should the proxy points be?
176+
# based on id_eps. How many of these should be user tunable?
177+
_tree_kind: Optional[str] = "adaptive-level-restricted",
178+
_max_particles_in_box: Optional[int] = None,
179+
180+
_id_rank: Optional[int] = None,
181+
182+
_approx_nproxy: Optional[int] = None,
183+
_proxy_radius_factor: Optional[float] = None,
184+
_proxy_cls: Optional[type] = None,
185+
):
186+
from pytential.linalg.cluster import partition_by_nodes
187+
cluster_index, ctree = partition_by_nodes(
188+
actx, places,
189+
tree_kind=_tree_kind,
190+
max_particles_in_box=_max_particles_in_box)
191+
192+
from pytential.linalg.utils import TargetAndSourceClusterList
193+
tgt_src_index = TargetAndSourceClusterList(
194+
targets=cluster_index, sources=cluster_index)
195+
196+
from pytential.linalg.skeletonization import rec_skeletonize_by_proxy
197+
skeletons = rec_skeletonize_by_proxy(
198+
actx, places, ctree, tgt_src_index, exprs, input_exprs,
199+
domains=domains,
200+
context=context,
201+
id_eps=id_eps,
202+
id_rank=_id_rank,
203+
approx_nproxy=_approx_nproxy,
204+
proxy_radius_factor=_proxy_radius_factor,
205+
max_particles_in_box=_max_particles_in_box,
206+
_proxy_cls=_proxy_cls,
207+
)
208+
209+
return ProxyHierarchicalMatrix(ctree=ctree, skeletons=skeletons)
210+
211+
# }}}

pytential/linalg/skeletonization.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ def evaluate_self(
305305
cls = self.neighbor_cluster_builder
306306
return self._evaluate_expr(
307307
actx, places, cls, tgt_src_index, self.exprs[ibrow],
308-
idomain=ibcol, _weighted=self.weighted_sources)
308+
idomain=ibcol, _weighted=self.weighted_sources,
309+
)
309310

310311
# {{{ nearfield
311312

@@ -604,7 +605,8 @@ def _skeletonize_block_by_proxy_with_mats(
604605
wrangler: SkeletonizationWrangler,
605606
tgt_src_index: "TargetAndSourceClusterList", *,
606607
id_eps: Optional[float] = None, id_rank: Optional[int] = None,
607-
max_particles_in_box: Optional[int] = None
608+
max_particles_in_box: Optional[int] = None,
609+
evaluate_diagonal: bool = False,
608610
) -> "SkeletonizationResult":
609611
nclusters = tgt_src_index.nclusters
610612
if nclusters == 1:
@@ -772,6 +774,9 @@ def __post_init__(self):
772774
if self.R.shape != shape:
773775
raise ValueError(f"'R' has shape {self.R.shape}, expected {shape}")
774776

777+
if self.D.shape != shape:
778+
raise ValueError(f"'D' has shape {self.L.shape}, expected {shape}")
779+
775780
@property
776781
def nclusters(self):
777782
return self.tgt_src_index.nclusters
@@ -792,7 +797,9 @@ def skeletonize_by_proxy(
792797

793798
id_eps: Optional[float] = None,
794799
id_rank: Optional[int] = None,
795-
max_particles_in_box: Optional[int] = None) -> np.ndarray:
800+
max_particles_in_box: Optional[int] = None,
801+
802+
_evaluate_diagonal: bool = False) -> np.ndarray:
796803
r"""Evaluate and skeletonize a symbolic expression using proxy-based methods.
797804
798805
:arg tgt_src_index: a :class:`~pytential.linalg.utils.TargetAndSourceClusterList`
@@ -832,7 +839,8 @@ def skeletonize_by_proxy(
832839
skels[ibrow, ibcol] = _skeletonize_block_by_proxy_with_mats(
833840
actx, ibrow, ibcol, proxy.places, proxy, wrangler, tgt_src_index,
834841
id_eps=id_eps, id_rank=id_rank,
835-
max_particles_in_box=max_particles_in_box)
842+
max_particles_in_box=max_particles_in_box,
843+
evaluate_diagonal=_evaluate_diagonal)
836844

837845
return skels
838846

@@ -954,7 +962,8 @@ def rec_skeletonize_by_proxy(
954962
skeleton = _skeletonize_block_by_proxy_with_mats(
955963
actx, ibrow, ibcol, proxy.places, proxy, wrangler, tgt_src_index,
956964
id_eps=id_eps, id_rank=id_rank,
957-
max_particles_in_box=max_particles_in_box)
965+
max_particles_in_box=max_particles_in_box,
966+
)
958967

959968
skeleton = _fix_skeleton_diagonal(
960969
skeleton, skel_per_level[i - 1], parent_level)

pytential/linalg/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,15 @@ def make_flat_cluster_diag(
334334

335335
return cluster_mat
336336

337+
338+
def split_array(x: np.ndarray, index: IndexList) -> np.ndarray:
339+
assert x.size == index.nindices
340+
341+
from pytools.obj_array import make_obj_array
342+
return make_obj_array([
343+
index.cluster_take(x, i) for i in range(index.nclusters)
344+
])
345+
337346
# }}}
338347

339348

0 commit comments

Comments
 (0)