From 4db4a9d83be68c59483d0670145bc1c48c7494c2 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Wed, 17 Aug 2016 00:02:32 -0500 Subject: [PATCH 1/4] Remove boxtree.geo_lookup, implement leaves to balls lookup in terms of area query and put the code in boxtree.area_query. Closes #1. This change also removes mention of "geometric lookup" in favor of "balls to leaves lookup." --- boxtree/__init__.py | 2 +- boxtree/area_query.py | 166 +++++++++++++++++++++++++ boxtree/geo_lookup.py | 275 ------------------------------------------ doc/lookup.rst | 2 - test/test_tree.py | 6 +- 5 files changed, 170 insertions(+), 281 deletions(-) delete mode 100644 boxtree/geo_lookup.py diff --git a/boxtree/__init__.py b/boxtree/__init__.py index 1a398b9..1d88ebd 100644 --- a/boxtree/__init__.py +++ b/boxtree/__init__.py @@ -41,7 +41,7 @@ __doc__ = """ it is completely distinct in the software sense. * It can compute geometric lookup structures based on a :class:`boxtree.Tree`, - see :mod:`boxtree.geo_lookup`. + see :mod:`boxtree.area_query`. Tree modes ---------- diff --git a/boxtree/area_query.py b/boxtree/area_query.py index f055bfc..2b7c346 100644 --- a/boxtree/area_query.py +++ b/boxtree/area_query.py @@ -44,6 +44,14 @@ Area queries (Balls -> overlapping leaves) .. autoclass:: AreaQueryResult +Inverse of area query (Leaves -> overlapping balls) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: LeavesToBallsLookupBuilder + +.. autoclass:: LeavesToBallsLookup + + Peer Lists ^^^^^^^^^^ @@ -98,6 +106,28 @@ class AreaQueryResult(DeviceDataRecord): .. versionadded:: 2016.1 """ + +class LeavesToBallsLookup(DeviceDataRecord): + """ + .. attribute:: tree + + The :class:`boxtree.Tree` instance used to build this lookup. + + .. attribute:: balls_near_box_starts + + Indices into :attr:`balls_near_box_lists`. + ``balls_near_box_lists[balls_near_box_starts[ibox]: + balls_near_box_starts[ibox]+1]`` + results in a list of balls that overlap leaf box *ibox*. + + .. note:: Only leaf boxes have non-empty entries in this table. Nonetheless, + this list is indexed by the global box index. + + .. attribute:: balls_near_box_lists + + .. automethod:: get + """ + # }}} # {{{ kernel templates @@ -326,6 +356,42 @@ void generate(LIST_ARG_DECL USER_ARG_DECL box_id_t box_id) """ + +from pyopencl.elementwise import ElementwiseTemplate + + +STARTS_EXPANDER_TEMPLATE = ElementwiseTemplate( + arguments=r""" + idx_t *dst, + idx_t *starts, + idx_t starts_len + """, + operation=r"""//CL// + /* Find my index in starts, place the index in dst. */ + idx_t l_idx = 0, r_idx = starts_len - 1, my_idx; + + for (;;) + { + my_idx = (l_idx + r_idx) / 2; + + if (starts[my_idx] <= i && i < starts[my_idx + 1]) + { + dst[i] = my_idx; + break; + } + + if (starts[my_idx] > i) + { + r_idx = my_idx; + } + else + { + l_idx = my_idx; + } + } + """, + name="starts_expander") + # }}} @@ -472,8 +538,108 @@ class AreaQueryBuilder(object): leaves_near_ball_starts=result["leaves"].starts, leaves_near_ball_lists=result["leaves"].lists).with_queue(None), evt + # }}} +# {{{ area query transpose (leaves-to-balls) lookup build + +class LeavesToBallsLookupBuilder(object): + """Given a set of :math:`l^\infty` "balls", this class helps build a + look-up table from leaf boxes to balls that overlap with each leaf box. + + .. automethod:: __call__ + + """ + def __init__(self, context): + self.context = context + + from pyopencl.algorithm import KeyValueSorter + self.key_value_sorter = KeyValueSorter(context) + self.area_query_builder = AreaQueryBuilder(context) + + @memoize_method + def get_starts_expander_kernel(self, idx_dtype): + """ + Expands a "starts" array into a length starts[-1] array of increasing + indices: + + Eg: [0 2 5 6] => [0 0 1 1 1 2] + + """ + return STARTS_EXPANDER_TEMPLATE.build( + self.context, + type_aliases=(("idx_t", idx_dtype),)) + + def __call__(self, queue, tree, ball_centers, ball_radii, peer_lists=None, + wait_for=None): + """ + :arg queue: a :class:`pyopencl.CommandQueue` + :arg tree: a :class:`boxtree.Tree`. + :arg ball_centers: an object array of coordinate + :class:`pyopencl.array.Array` instances. + Their *dtype* must match *tree*'s + :attr:`boxtree.Tree.coord_dtype`. + :arg ball_radii: a + :class:`pyopencl.array.Array` + of positive numbers. + Its *dtype* must match *tree*'s + :attr:`boxtree.Tree.coord_dtype`. + :arg peer_lists: may either be *None* or an instance of + :class:`PeerListLookup` associated with `tree`. + :arg wait_for: may either be *None* or a list of :class:`pyopencl.Event` + instances for whose completion this command waits before starting + execution. + :returns: a tuple *(lbl, event)*, where *lbl* is an instance of + :class:`LeavesToBallsLookup`, and *event* is a :class:`pyopencl.Event` + for dependency management. + """ + + from pytools import single_valued + if single_valued(bc.dtype for bc in ball_centers) != tree.coord_dtype: + raise TypeError("ball_centers dtype must match tree.coord_dtype") + if ball_radii.dtype != tree.coord_dtype: + raise TypeError("ball_radii dtype must match tree.coord_dtype") + + logger.info("leaves-to-balls lookup: run area query") + + area_query, evt = self.area_query_builder( + queue, tree, ball_centers, ball_radii, peer_lists, wait_for) + wait_for = [evt] + + logger.info("leaves-to-balls lookup: expand starts") + + nkeys = len(area_query.leaves_near_ball_lists) + nballs = len(area_query.leaves_near_ball_starts) + + starts_expander_knl = self.get_starts_expander_kernel(tree.box_id_dtype) + expanded_starts = cl.array.empty(queue, nkeys, tree.box_id_dtype) + evt = starts_expander_knl( + expanded_starts, + area_query.leaves_near_ball_starts.with_queue(queue), + nballs + 1) + wait_for = [evt] + + logger.info("leaves-to-balls lookup: key-value sort") + + balls_near_box_starts, balls_near_box_lists, evt \ + = self.key_value_sorter( + queue, + # keys + area_query.leaves_near_ball_lists.with_queue(queue), + # values + expanded_starts, + nkeys, starts_dtype=tree.box_id_dtype, + wait_for=wait_for) + + logger.info("leaves-to-balls lookup: built") + + return LeavesToBallsLookup( + tree=tree, + balls_near_box_starts=balls_near_box_starts, + balls_near_box_lists=balls_near_box_lists).with_queue(None), evt + + +# }}} # {{{ peer list build diff --git a/boxtree/geo_lookup.py b/boxtree/geo_lookup.py deleted file mode 100644 index 7d56fcc..0000000 --- a/boxtree/geo_lookup.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import division - -__copyright__ = "Copyright (C) 2013 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. -""" - - -from pytools import memoize_method, Record -import numpy as np -import pyopencl as cl -import pyopencl.array # noqa -from mako.template import Template -from boxtree.tools import AXIS_NAMES, DeviceDataRecord - -import logging -logger = logging.getLogger(__name__) - -__doc__ = """ -Leaves -> overlapping balls ---------------------------- - -.. autoclass:: LeavesToBallsLookupBuilder - -.. autoclass:: LeavesToBallsLookup -""" - - -# {{{ output - -class LeavesToBallsLookup(DeviceDataRecord): - """ - .. attribute:: tree - - The :class:`boxtree.Tree` instance used to build this lookup. - - .. attribute:: balls_near_box_starts - - Indices into :attr:`balls_near_box_lists`. - ``balls_near_box_lists[balls_near_box_starts[ibox]: - balls_near_box_starts[ibox]+1]`` - results in a list of balls that overlap leaf box *ibox*. - - .. note:: Only leaf boxes have non-empty entries in this table. Nonetheless, - this list is indexed by the global box index. - - .. attribute:: balls_near_box_lists - - .. automethod:: get - """ - -# }}} - -# {{{ kernel templates - -BALLS_TO_LEAVES_TEMPLATE = r"""//CL// -typedef ${dtype_to_ctype(ball_id_dtype)} ball_id_t; - -void generate(LIST_ARG_DECL USER_ARG_DECL ball_id_t ball_nr) -{ - coord_vec_t ball_center; - %for i in range(dimensions): - ball_center.${AXIS_NAMES[i]} = ball_${AXIS_NAMES[i]}[ball_nr]; - %endfor - - coord_t ball_radius = ball_radii[ball_nr]; - - // To find overlapping leaves, start at the top of the tree, descend - // into overlapping boxes. - ${walk_init(0)} - - while (continue_walk) - { - box_id_t child_box_id = box_child_ids[ - walk_morton_nr * aligned_nboxes + walk_box_id]; - dbg_printf((" walk box id: %d morton: %d child id: %d level: %d\n", - walk_box_id, walk_morton_nr, child_box_id, walk_level)); - - if (child_box_id) - { - bool is_overlapping; - - ${check_l_infty_ball_overlap( - "is_overlapping", "child_box_id", "ball_radius", "ball_center")} - - if (is_overlapping) - { - if (!(box_flags[child_box_id] & BOX_HAS_CHILDREN)) - { - APPEND_ball_numbers(ball_nr); - APPEND_overlapping_leaves(child_box_id); - } - else - { - // We want to descend into this box. Put the current state - // on the stack. - - ${walk_push("child_box_id")} - continue; - } - } - } - - ${walk_advance()} - } -} -""" - - -class _KernelInfo(Record): - pass - - -class LeavesToBallsLookupBuilder(object): - """Given a set of :math:`l^\infty` "balls", this class helps build a - look-up table from leaf boxes to balls that overlap with each leaf box. - - .. automethod:: __call__ - - """ - def __init__(self, context): - self.context = context - - from pyopencl.algorithm import KeyValueSorter - self.key_value_sorter = KeyValueSorter(context) - - @memoize_method - def get_balls_to_leaves_kernel(self, dimensions, coord_dtype, box_id_dtype, - ball_id_dtype, max_levels, stick_out_factor): - from pyopencl.tools import dtype_to_ctype - from boxtree import box_flags_enum - render_vars = dict( - dimensions=dimensions, - dtype_to_ctype=dtype_to_ctype, - box_id_dtype=box_id_dtype, - particle_id_dtype=None, - ball_id_dtype=ball_id_dtype, - coord_dtype=coord_dtype, - vec_types=cl.array.vec.types, - max_levels=max_levels, - AXIS_NAMES=AXIS_NAMES, - box_flags_enum=box_flags_enum, - debug=False, - stick_out_factor=stick_out_factor, - ) - - logger.info("start building leaves-to-balls lookup kernel") - - from boxtree.traversal import TRAVERSAL_PREAMBLE_TEMPLATE - - src = Template( - TRAVERSAL_PREAMBLE_TEMPLATE - + BALLS_TO_LEAVES_TEMPLATE, - strict_undefined=True).render(**render_vars) - - from pyopencl.tools import VectorArg, ScalarArg - from pyopencl.algorithm import ListOfListsBuilder - result = ListOfListsBuilder(self.context, - [ - ("ball_numbers", ball_id_dtype), - ("overlapping_leaves", box_id_dtype), - ], - str(src), - arg_decls=[ - VectorArg(box_flags_enum.dtype, "box_flags"), - VectorArg(coord_dtype, "box_centers"), - VectorArg(box_id_dtype, "box_child_ids"), - VectorArg(np.uint8, "box_levels"), - ScalarArg(coord_dtype, "root_extent"), - ScalarArg(box_id_dtype, "aligned_nboxes"), - VectorArg(coord_dtype, "ball_radii"), - ] + [ - VectorArg(coord_dtype, "ball_"+ax) - for ax in AXIS_NAMES[:dimensions]], - name_prefix="circles_to_balls", - count_sharing={ - # /!\ This makes a promise that APPEND_ball_numbers will - # always occur *before* APPEND_overlapping_leaves. - "overlapping_leaves": "ball_numbers" - }, - complex_kernel=True) - - logger.info("done building leaves-to-balls lookup kernel") - - return result - - def __call__(self, queue, tree, ball_centers, ball_radii, wait_for=None): - """ - :arg queue: a :class:`pyopencl.CommandQueue` - :arg tree: a :class:`boxtree.Tree`. - :arg ball_centers: an object array of coordinate - :class:`pyopencl.array.Array` instances. - Their *dtype* must match *tree*'s - :attr:`boxtree.Tree.coord_dtype`. - :arg ball_radii: a - :class:`pyopencl.array.Array` - of positive numbers. - Its *dtype* must match *tree*'s - :attr:`boxtree.Tree.coord_dtype`. - :arg wait_for: may either be *None* or a list of :class:`pyopencl.Event` - instances for whose completion this command waits before starting - exeuction. - :returns: a tuple *(lbl, event)*, where *lbl* is an instance of - :class:`LeavesToBallsLookup`, and *event* is a :class:`pyopencl.Event` - for dependency management. - """ - - from pytools import single_valued - if single_valued(bc.dtype for bc in ball_centers) != tree.coord_dtype: - raise TypeError("ball_centers dtype must match tree.coord_dtype") - if ball_radii.dtype != tree.coord_dtype: - raise TypeError("ball_radii dtype must match tree.coord_dtype") - - ball_id_dtype = tree.particle_id_dtype # ? - - from pytools import div_ceil - # Avoid generating too many kernels. - max_levels = div_ceil(tree.nlevels, 10) * 10 - - b2l_knl = self.get_balls_to_leaves_kernel( - tree.dimensions, tree.coord_dtype, - tree.box_id_dtype, ball_id_dtype, - max_levels, tree.stick_out_factor) - - logger.info("leaves-to-balls lookup: prepare ball list") - - nballs = len(ball_radii) - result, evt = b2l_knl( - queue, nballs, - tree.box_flags.data, tree.box_centers.data, - tree.box_child_ids.data, tree.box_levels.data, - tree.root_extent, tree.aligned_nboxes, - ball_radii.data, *tuple(bc.data for bc in ball_centers), - wait_for=wait_for) - wait_for = [evt] - - logger.info("leaves-to-balls lookup: key-value sort") - - balls_near_box_starts, balls_near_box_lists, evt \ - = self.key_value_sorter( - queue, - # keys - result["overlapping_leaves"].lists, - # values - result["ball_numbers"].lists, - tree.nboxes, starts_dtype=tree.box_id_dtype, - wait_for=wait_for) - - logger.info("leaves-to-balls lookup: built") - - return LeavesToBallsLookup( - tree=tree, - balls_near_box_starts=balls_near_box_starts, - balls_near_box_lists=balls_near_box_lists).with_queue(None), evt - -# }}} - -# vim: filetype=pyopencl:fdm=marker diff --git a/doc/lookup.rst b/doc/lookup.rst index 6a1fca6..a457122 100644 --- a/doc/lookup.rst +++ b/doc/lookup.rst @@ -1,8 +1,6 @@ Tree-based geometric lookup =========================== -.. automodule:: boxtree.geo_lookup - .. automodule:: boxtree.area_query .. vim: sw=4 diff --git a/test/test_tree.py b/test/test_tree.py index 1540af0..18210aa 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -599,12 +599,12 @@ def test_extent_tree(ctx_getter, dims, do_plot=False): # }}} -# {{{ geometry query test +# {{{ leaves to balls query test @pytest.mark.opencl @pytest.mark.geo_lookup @pytest.mark.parametrize("dims", [2, 3]) -def test_geometry_query(ctx_getter, dims, do_plot=False): +def test_leaves_to_balls_query(ctx_getter, dims, do_plot=False): logging.basicConfig(level=logging.INFO) ctx = ctx_getter() @@ -629,7 +629,7 @@ def test_geometry_query(ctx_getter, dims, do_plot=False): ball_centers = make_normal_particle_array(queue, nballs, dims, dtype) ball_radii = cl.array.empty(queue, nballs, dtype).fill(0.1) - from boxtree.geo_lookup import LeavesToBallsLookupBuilder + from boxtree.area_query import LeavesToBallsLookupBuilder lblb = LeavesToBallsLookupBuilder(ctx) lbl, _ = lblb(queue, tree, ball_centers, ball_radii) -- GitLab From 19ac24a30767eed589e104b5d30c7562bd1e369c Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Tue, 30 Aug 2016 00:25:56 -0500 Subject: [PATCH 2/4] Acknowledge copyright from original file. --- boxtree/area_query.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/boxtree/area_query.py b/boxtree/area_query.py index 2b7c346..db6ed1a 100644 --- a/boxtree/area_query.py +++ b/boxtree/area_query.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import division -__copyright__ = """Copyright (C) 2016 Matt Wala""" +__copyright__ = """ +Copyright (C) 2013 Andreas Kloeckner +Copyright (C) 2016 Matt Wala""" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy -- GitLab From fd626b3f262fdcb4db3121f0601558572ec1aff5 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Tue, 30 Aug 2016 00:31:20 -0500 Subject: [PATCH 3/4] Fix off-by-one error. --- boxtree/area_query.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/boxtree/area_query.py b/boxtree/area_query.py index db6ed1a..738edec 100644 --- a/boxtree/area_query.py +++ b/boxtree/area_query.py @@ -611,14 +611,15 @@ class LeavesToBallsLookupBuilder(object): logger.info("leaves-to-balls lookup: expand starts") nkeys = len(area_query.leaves_near_ball_lists) - nballs = len(area_query.leaves_near_ball_starts) + nballs_p_1 = len(area_query.leaves_near_ball_starts) + assert nballs_p_1 == len(ball_radii) + 1 starts_expander_knl = self.get_starts_expander_kernel(tree.box_id_dtype) expanded_starts = cl.array.empty(queue, nkeys, tree.box_id_dtype) evt = starts_expander_knl( expanded_starts, area_query.leaves_near_ball_starts.with_queue(queue), - nballs + 1) + nballs_p_1) wait_for = [evt] logger.info("leaves-to-balls lookup: key-value sort") -- GitLab From 8a1997f24145279c914cbea9a8651839dcaea78a Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Tue, 30 Aug 2016 11:02:22 -0500 Subject: [PATCH 4/4] More off-by-one fixes. --- boxtree/area_query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boxtree/area_query.py b/boxtree/area_query.py index 738edec..85d063e 100644 --- a/boxtree/area_query.py +++ b/boxtree/area_query.py @@ -384,11 +384,11 @@ STARTS_EXPANDER_TEMPLATE = ElementwiseTemplate( if (starts[my_idx] > i) { - r_idx = my_idx; + r_idx = my_idx - 1; } else { - l_idx = my_idx; + l_idx = my_idx + 1; } } """, -- GitLab