Skip to content

Commit

Permalink
Add region-overlap expression decoding from base64.
Browse files Browse the repository at this point in the history
This is directly targeted at a daf_butler use case, where we
concatenate base64-encoded regions with an aggregate function to
represent an expression of the form:

    (a OVERLAPS b) OR (c OVERLAPS d) OR (e OVERLAPS f)

Moving the base64 decoding to C++ here lets us also move the split on
the concatenation delimiter and (most importantly) individual region
construction) to C++, avoiding a lot of expensive Python object
instantiations and pybind11 type wrangling.
  • Loading branch information
TallJimbo committed Dec 10, 2024
1 parent ded9e8b commit 5882947
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 1 deletion.
14 changes: 14 additions & 0 deletions include/lsst/sphgeom/Region.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ class Region {
static std::unique_ptr<Region> decodeBase64(std::string_view const & s);
///@}

///@{
/// `decodeOverlapsBase64` evaluates an encoded overlap expression.
///
/// A single overlap expression is formed by concatenating a pair of
/// base64-encoded regions (`Region::encode` then base64 encoding) with
/// '&' as the delimiter. Multiple such pairwise overlap expressions can
/// then be concatenated with '|' as the delimiter to form the logical OR.
static TriState decodeOverlapsBase64(std::string const & s) {
return decodeOverlapsBase64(s);
}

static TriState decodeOverlapsBase64(std::string_view const & s);
///@}

/// `getRegions` returns a vector of Region.
static std::vector<std::unique_ptr<Region>> getRegions(Region const &region);
///@}
Expand Down
3 changes: 3 additions & 0 deletions python/lsst/sphgeom/_region.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ void defineClass(py::class_<Region, std::unique_ptr<Region>> &cls) {
cls.def_static("decode", &python::decode<Region>, "bytes"_a);
cls.def_static("decodeBase64", py::overload_cast<std::string_view const&>(&Region::decodeBase64),
"bytes"_a);
cls.def_static("decodeOverlapsBase64",
py::overload_cast<std::string_view const&>(&Region::decodeOverlapsBase64),
"bytes"_a);
cls.def_static("getRegions", Region::getRegions, "region"_a);
}

Expand Down
29 changes: 29 additions & 0 deletions src/Region.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ std::unique_ptr<Region> Region::decodeBase64(std::string_view const & s) {
}
}

TriState Region::decodeOverlapsBase64(std::string_view const & s) {
TriState result(false);
if (s.empty()) {
// False makes the most sense as the limit of a logical OR of zero
// terms (e.g. `any([])` in Python).
return result;
}
auto begin = s.begin();
while (result != true) { // if result is known to be true, we're done.
auto mid = std::find(begin, s.end(), '&');
if (mid == s.end()) {
throw std::runtime_error("No '&' found in encoded overlap expression term.");
}
auto a = Region::decode(base64::decode_into<std::vector<std::uint8_t>>(begin, mid));
++mid;
auto end = std::find(mid, s.end(), '|');
auto b = Region::decode(base64::decode_into<std::vector<std::uint8_t>>(mid, end));
result = result | a->overlaps(*b);
if (end == s.end()) {
break;
} else {
begin = end;
++begin;
}
}
return result;
}

std::vector<std::unique_ptr<Region>> Region::getRegions(Region const &region) {
std::vector<std::unique_ptr<Region>> result;
if (auto union_region = dynamic_cast<UnionRegion const *>(&region)) {
Expand All @@ -127,4 +155,5 @@ std::vector<std::unique_ptr<Region>> Region::getRegions(Region const &region) {
return result;
}


}} // namespace lsst:sphgeom
29 changes: 28 additions & 1 deletion tests/test_CompoundRegion.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def setUp(self):
self.instance = IntersectionRegion(*self.operands)

def testEmpty(self):
"""Test zero-operand intersection which is quivalent to full sphere."""
"""Test zero-operand intersection which is equivalent to full sphere."""

Check failure on line 238 in tests/test_CompoundRegion.py

View workflow job for this annotation

GitHub Actions / call-workflow / lint

W505

doc line too long (80 > 79 characters)
region = IntersectionRegion()

self.assertTrue(region.contains(UnitVector3d(self.point_in_both)))
Expand Down Expand Up @@ -294,6 +294,33 @@ def testGetRegion(self):
# ur2 = UnionRegion(u1, i1, u2)
# self.assertEqual(Region.getRegions(ur2), [c1, b1, i1, c2, b2])

def testDecodeOverlapsBase64(self):
"""Test Region.decodeOverlapsBase64.
This test is in this test case because it can make good use of the
concrete regions defined in setUp.
"""

def run_overlaps(pairs: list[tuple[Region, Region]]) -> bool | None:
or_terms = []
for a, b in pairs:
a_str = b64encode(a.encode()).decode("ascii")
b_str = b64encode(b.encode()).decode("ascii")
or_terms.append(f"{a_str}&{b_str}")
overlap_str = "|".join(or_terms)
return Region.decodeOverlapsBase64(overlap_str)

self.assertEqual(run_overlaps([]), False)
self.assertEqual(run_overlaps([(self.box, self.circle)]), True)
self.assertEqual(run_overlaps([(self.box, self.faraway)]), False)
self.assertEqual(run_overlaps([(self.circle, self.faraway)]), False)
self.assertEqual(run_overlaps([(self.instance, self.box)]), None)
self.assertEqual(run_overlaps([(self.box, self.circle), (self.box, self.faraway)]), True)
self.assertEqual(run_overlaps([(self.faraway, self.circle), (self.box, self.faraway)]), False)
self.assertEqual(run_overlaps([(self.instance, self.box), (self.circle, self.faraway)]), None)
self.assertEqual(run_overlaps([(self.instance, self.box), (self.circle, self.box)]), True)
self.assertEqual(run_overlaps([(self.circle, self.box), (self.instance, self.box)]), True)


if __name__ == "__main__":
unittest.main()

0 comments on commit 5882947

Please sign in to comment.