Skip to content

Commit

Permalink
add recursive clustering and skeletonization
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfikl committed Jul 14, 2022
1 parent a54676b commit 78699ec
Show file tree
Hide file tree
Showing 13 changed files with 619 additions and 175 deletions.
4 changes: 4 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"DOFDescriptorLike": "pytential.symbolic.dof_desc.DOFDescriptorLike",
}

nitpick_ignore_regex = [
["py:class", r"_ProxyNeighborEvaluationResult"],
]

intersphinx_mapping = {
"https://docs.python.org/3/": None,
"https://numpy.org/doc/stable/": None,
Expand Down
22 changes: 18 additions & 4 deletions doc/linalg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,38 @@ scheme is used:
component of the Stokeslet.
* ``cluster`` refers to a piece of a ``block`` as used by the recursive
proxy-based skeletonization of the direct solver algorithms. Clusters
are represented by a :class:`~pytential.linalg.TargetAndSourceClusterList`.
are represented by a :class:`~pytential.linalg.utils.TargetAndSourceClusterList`.

.. _direct_solver:

Hierarchical Direct Solver
--------------------------

.. note::

High-level API for direct solvers is in progress.

Low-level Functionality
-----------------------

.. warning::

All the classes and routines in this module are experimental and the
API can change at any point.

.. automodule:: pytential.linalg.skeletonization
.. automodule:: pytential.linalg.cluster
.. automodule:: pytential.linalg.proxy
.. automodule:: pytential.linalg.utils

Internal Functionality
----------------------
Internal Functionality and Utilities
------------------------------------

.. warning::

All the classes and routines in this module are experimental and the
API can change at any point.

.. automodule:: pytential.linalg.utils
.. automodule:: pytential.linalg.direct_solver_symbolic

.. vim: sw=4:tw=75
16 changes: 0 additions & 16 deletions pytential/linalg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,9 @@
make_index_list, make_index_cluster_cartesian_product,
interp_decomp,
)
from pytential.linalg.proxy import (
ProxyClusterGeometryData, ProxyPointTarget, ProxyPointSource,
ProxyGeneratorBase, ProxyGenerator, QBXProxyGenerator,
partition_by_nodes, gather_cluster_neighbor_points,
)
from pytential.linalg.skeletonization import (
SkeletonizationWrangler, make_skeletonization_wrangler,
SkeletonizationResult, skeletonize_by_proxy,
)

__all__ = (
"IndexList", "TargetAndSourceClusterList",
"make_index_list", "make_index_cluster_cartesian_product",
"interp_decomp",

"ProxyClusterGeometryData", "ProxyPointTarget", "ProxyPointSource",
"ProxyGeneratorBase", "ProxyGenerator", "QBXProxyGenerator",
"partition_by_nodes", "gather_cluster_neighbor_points",

"SkeletonizationWrangler", "make_skeletonization_wrangler",
"SkeletonizationResult", "skeletonize_by_proxy",
)
277 changes: 277 additions & 0 deletions pytential/linalg/cluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
__copyright__ = "Copyright (C) 2022 Alexandru Fikl"

__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.
"""

from dataclasses import dataclass, replace
from functools import singledispatch
from typing import Optional

import numpy as np

from pytools import T, memoize_method

from arraycontext import PyOpenCLArrayContext
from boxtree.tree import Tree
from pytential import sym, GeometryCollection
from pytential.linalg.utils import IndexList, TargetAndSourceClusterList

__doc__ = """
Clustering
~~~~~~~~~~
.. autoclass:: ClusterTreeLevel
.. autofunction:: cluster
.. autofunction:: partition_by_nodes
"""

# FIXME: this is just an arbitrary value
_DEFAULT_MAX_PARTICLES_IN_BOX = 32


# {{{ cluster tree

@dataclass(frozen=True)
class ClusterTreeLevel:
"""
.. attribute:: level
Current level that is represented.
.. attribute:: nlevels
Total number of levels in the tree.
.. attribute:: nclusters
Number of clusters on the current level (same as number of boxes
in :attr:`partition_box_ids`).
.. attribute:: partition_box_ids
Box IDs on the current level.
.. attribute:: partition_parent_ids
Parent box IDs for :attr:`partition_box_ids`.
.. attribute:: partition_parent_map
An object :class:`~numpy.ndarray`, where each entry maps a parent
to its children indices in :attr:`partition_box_ids`. This can be used to
:func:`cluster` all indices in ``partition_parent_map[i]`` into their
parent.
.. automethod:: parent
"""

# level info
level: int
partition_box_ids: np.ndarray

# tree info
nlevels: int
box_parent_ids: np.ndarray

# NOTE: only here to allow easier debugging + testing
_tree: Optional[Tree]

@property
def nclusters(self):
return self.partition_box_ids.size

@property
def partition_parent_ids(self):
return self.box_parent_ids[self.partition_box_ids]

@property
@memoize_method
def partition_parent_map(self):
# NOTE: np.unique returns a sorted array
unique_parent_ids = np.unique(self.partition_parent_ids)
# find the index of each parent id
unique_parent_index = np.searchsorted(
unique_parent_ids, self.partition_parent_ids
)

unique_parent_map = np.empty(unique_parent_ids.size, dtype=object)
for i in range(unique_parent_ids.size):
unique_parent_map[i], = np.nonzero(unique_parent_index == i)

return unique_parent_map

def parent(self) -> "ClusterTreeLevel":
"""
:returns: a new :class:`ClusterTreeLevel` that represents the parent of
the current one, with appropriately updated :attr:`partition_box_ids`,
etc.
"""

if self.nclusters == 1:
assert self.level == 0
return self

return replace(self,
level=self.level - 1,
partition_box_ids=np.unique(self.partition_parent_ids))


@singledispatch
def cluster(obj: T, ctree: ClusterTreeLevel) -> T:
"""Merge together elements of *obj* into their parent object, as described
by *ctree*.
"""
raise NotImplementedError(type(obj).__name__)


@cluster.register(IndexList)
def _cluster_index_list(obj: IndexList, ctree: ClusterTreeLevel) -> IndexList:
assert obj.nclusters == ctree.nclusters

if ctree.nclusters == 1:
return obj

sizes = [
sum(obj.cluster_size(j) for j in ppm)
for ppm in ctree.partition_parent_map
]
return replace(obj, starts=np.cumsum([0] + sizes))


@cluster.register(TargetAndSourceClusterList)
def _cluster_target_and_source_cluster_list(
obj: TargetAndSourceClusterList, ctree: ClusterTreeLevel,
) -> TargetAndSourceClusterList:
assert obj.nclusters == ctree.nclusters

if ctree.nclusters == 1:
return obj

return replace(obj,
targets=cluster(obj.targets, ctree),
sources=cluster(obj.sources, ctree))

# }}}


# {{{ cluster generation

def _build_binary_ish_tree_from_indices(starts: np.ndarray) -> ClusterTreeLevel:
partition_box_ids = np.arange(starts.size - 1)

box_ids = partition_box_ids

box_parent_ids = []
offset = box_ids.size
while box_ids.size > 1:
# NOTE: this is probably not the most efficient way to do it, but this
# code is mostly meant for debugging using a simple tree
clusters = np.array_split(box_ids, box_ids.size // 2)
parent_ids = offset + np.arange(len(clusters))
box_parent_ids.append(np.repeat(parent_ids, [len(c) for c in clusters]))

box_ids = parent_ids
offset += box_ids.size

# NOTE: make the root point to itself
box_parent_ids.append(np.array([offset - 1]))
nlevels = len(box_parent_ids)

return ClusterTreeLevel(
level=nlevels - 1,
partition_box_ids=partition_box_ids,
nlevels=nlevels,
box_parent_ids=np.concatenate(box_parent_ids),
_tree=None)


def partition_by_nodes(
actx: PyOpenCLArrayContext, places: GeometryCollection, *,
dofdesc: Optional[sym.DOFDescriptorLike] = None,
tree_kind: Optional[str] = "adaptive-level-restricted",
max_particles_in_box: Optional[int] = None) -> IndexList:
"""Generate equally sized ranges of nodes. The partition is created at the
lowest level of granularity, i.e. nodes. This results in balanced ranges
of points, but will split elements across different ranges.
:arg dofdesc: a :class:`~pytential.symbolic.dof_desc.DOFDescriptor` for
the geometry in *places* which should be partitioned.
:arg tree_kind: if not *None*, it is passed to :class:`boxtree.TreeBuilder`.
:arg max_particles_in_box: value used to control the number of points
in each partition (and thus the number of partitions). See the documentation
in :class:`boxtree.TreeBuilder`.
"""
if dofdesc is None:
dofdesc = places.auto_source
dofdesc = sym.as_dofdesc(dofdesc)

if max_particles_in_box is None:
max_particles_in_box = _DEFAULT_MAX_PARTICLES_IN_BOX

lpot_source = places.get_geometry(dofdesc.geometry)
discr = places.get_discretization(dofdesc.geometry, dofdesc.discr_stage)

if tree_kind is not None:
from pytential.qbx.utils import tree_code_container
tcc = tree_code_container(lpot_source._setup_actx)

from arraycontext import flatten
from meshmode.dof_array import DOFArray
tree, _ = tcc.build_tree()(actx.queue,
particles=flatten(
actx.thaw(discr.nodes()), actx, leaf_class=DOFArray
),
max_particles_in_box=max_particles_in_box,
kind=tree_kind)

from boxtree import box_flags_enum
tree = tree.get(actx.queue)
leaf_boxes, = (tree.box_flags & box_flags_enum.HAS_CHILDREN == 0).nonzero()

indices = np.empty(len(leaf_boxes), dtype=object)
starts = None

for i, ibox in enumerate(leaf_boxes):
box_start = tree.box_source_starts[ibox]
box_end = box_start + tree.box_source_counts_cumul[ibox]
indices[i] = tree.user_source_ids[box_start:box_end]

ctree = ClusterTreeLevel(
level=tree.nlevels - 1,
nlevels=tree.nlevels,
box_parent_ids=tree.box_parent_ids,
partition_box_ids=leaf_boxes,
_tree=tree)
else:
if discr.ambient_dim != 2 and discr.dim == 1:
raise ValueError("only curves are supported for 'tree_kind=None'")

nclusters = max(discr.ndofs // max_particles_in_box, 2)
indices = np.arange(0, discr.ndofs, dtype=np.int64)
starts = np.linspace(0, discr.ndofs, nclusters + 1, dtype=np.int64)
assert starts[-1] == discr.ndofs

ctree = _build_binary_ish_tree_from_indices(starts)

from pytential.linalg import make_index_list
return make_index_list(indices, starts=starts), ctree

# }}}
Loading

0 comments on commit 78699ec

Please sign in to comment.