Browse Source

Add methods to prevent allocating long arrays during child navigation on H3 api (#92099)

Ignacio Vera 2 years ago
parent
commit
2baf7c4d23

+ 6 - 0
docs/changelog/92099.yaml

@@ -0,0 +1,6 @@
+pr: 92099
+summary: Add methods to prevent allocating long arrays during child navigation on
+  H3 api
+area: Geo
+type: enhancement
+issues: []

+ 154 - 25
libs/h3/src/main/java/org/elasticsearch/h3/H3.java

@@ -229,13 +229,9 @@ public final class H3 {
      * Returns the children of the given index.
      */
     public static long[] h3ToChildren(long h3) {
-        long[] children = new long[cellToChildrenSize(h3)];
-        int res = H3Index.H3_get_resolution(h3);
-        Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, res + 1);
-        int pos = 0;
-        while (it.h != Iterator.H3_NULL) {
-            children[pos++] = it.h;
-            Iterator.iterStepChild(it);
+        final long[] children = new long[h3ToChildrenSize(h3)];
+        for (int i = 0; i < children.length; i++) {
+            children[i] = childPosToH3(h3, i);
         }
         return children;
     }
@@ -248,6 +244,39 @@ public final class H3 {
         return h3ToStringList(h3ToChildren(stringToH3(h3Address)));
     }
 
+    /**
+     * Returns the child cell at the given position
+     */
+    public static long childPosToH3(long h3, int childPos) {
+        final int childrenRes = H3Index.H3_get_resolution(h3) + 1;
+        if (childrenRes > MAX_H3_RES) {
+            throw new IllegalArgumentException("Resolution overflow");
+        }
+        final long childH = H3Index.H3_set_resolution(h3, childrenRes);
+        if (childPos == 0) {
+            return H3Index.H3_set_index_digit(childH, childrenRes, CoordIJK.Direction.CENTER_DIGIT.digit());
+        }
+        final boolean isPentagon = isPentagon(h3);
+        final int maxPos = isPentagon ? 5 : 6;
+        if (childPos < 0 || childPos > maxPos) {
+            throw new IllegalArgumentException("invalid child position");
+        }
+        if (isPentagon) {
+            // Pentagon skip digit (position) is the number 1, therefore we add one
+            // to the current position.
+            return H3Index.H3_set_index_digit(childH, childrenRes, childPos + 1);
+        } else {
+            return H3Index.H3_set_index_digit(childH, childrenRes, childPos);
+        }
+    }
+
+    /**
+     * Returns the child address at the given position
+     */
+    public static String childPosToH3(String h3Address, int childPos) {
+        return h3ToString(childPosToH3(stringToH3(h3Address), childPos));
+    }
+
     private static final int[] PEN_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 1, 6, 4, 2 };
     private static final int[] HEX_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 6, 2, 5, 1, 4 };
 
@@ -256,16 +285,12 @@ public final class H3 {
      * intersects with it.
      */
     public static long[] h3ToNoChildrenIntersecting(long h3) {
-        final long[] children = new long[cellToChildrenSize(h3) - 1];
-        final Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, H3Index.H3_get_resolution(h3) + 1);
-        final int[] directions = H3.isPentagon(it.h) ? PEN_INTERSECTING_CHILDREN_DIRECTIONS : HEX_INTERSECTING_CHILDREN_DIRECTIONS;
-        int pos = 0;
-        Iterator.iterStepChild(it);
-        while (it.h != Iterator.H3_NULL) {
-            children[pos] = HexRing.h3NeighborInDirection(it.h, directions[pos++]);
-            Iterator.iterStepChild(it);
+        final boolean isPentagon = isPentagon(h3);
+        final long[] noChildren = new long[isPentagon ? 5 : 6];
+        for (int i = 0; i < noChildren.length; i++) {
+            noChildren[i] = noChildIntersectingPosToH3(h3, i);
         }
-        return children;
+        return noChildren;
     }
 
     /**
@@ -276,6 +301,39 @@ public final class H3 {
         return h3ToStringList(h3ToNoChildrenIntersecting(stringToH3(h3Address)));
     }
 
+    /**
+     * Returns the no child intersecting cell at the given position
+     */
+    public static long noChildIntersectingPosToH3(long h3, int childPos) {
+        final int childrenRes = H3Index.H3_get_resolution(h3) + 1;
+        if (childrenRes > MAX_H3_RES) {
+            throw new IllegalArgumentException("Resolution overflow");
+        }
+        final boolean isPentagon = isPentagon(h3);
+        final int maxPos = isPentagon ? 4 : 5;
+        if (childPos < 0 || childPos > maxPos) {
+            throw new IllegalArgumentException("invalid child position");
+        }
+        final long childH = H3Index.H3_set_resolution(h3, childrenRes);
+        if (isPentagon) {
+            // Pentagon skip digit (position) is the number 1, therefore we add one
+            // for the skip digit and one for the 0 (center) digit.
+            final long child = H3Index.H3_set_index_digit(childH, childrenRes, childPos + 2);
+            return HexRing.h3NeighborInDirection(child, PEN_INTERSECTING_CHILDREN_DIRECTIONS[childPos]);
+        } else {
+            // we add one for the 0 (center) digit.
+            final long child = H3Index.H3_set_index_digit(childH, childrenRes, childPos + 1);
+            return HexRing.h3NeighborInDirection(child, HEX_INTERSECTING_CHILDREN_DIRECTIONS[childPos]);
+        }
+    }
+
+    /**
+     * Returns the no child intersecting cell at the given position
+     */
+    public static String noChildIntersectingPosToH3(String h3Address, int childPos) {
+        return h3ToString(noChildIntersectingPosToH3(stringToH3(h3Address), childPos));
+    }
+
     /**
      * Returns the neighbor indexes.
      *
@@ -319,23 +377,94 @@ public final class H3 {
     }
 
     /**
-     * cellToChildrenSize returns the exact number of children for a cell at a
+     * h3ToChildrenSize returns the exact number of children for a cell at a
      * given child resolution.
      *
-     * @param h         H3Index to find the number of children of
+     * @param h3         H3Index to find the number of children of
+     * @param childRes  The child resolution you're interested in
      *
-     * @return int      Exact number of children (handles hexagons and pentagons
+     * @return long      Exact number of children (handles hexagons and pentagons
      *                  correctly)
      */
-    private static int cellToChildrenSize(long h) {
-        int n = 1;
-        if (H3Index.H3_is_pentagon(h)) {
-            return (1 + 5 * (_ipow(7, n) - 1) / 6);
+    public static long h3ToChildrenSize(long h3, int childRes) {
+        final int parentRes = H3Index.H3_get_resolution(h3);
+        if (childRes <= parentRes || childRes > MAX_H3_RES) {
+            throw new IllegalArgumentException("Invalid child resolution [" + childRes + "]");
+        }
+        final int n = childRes - parentRes;
+        if (H3Index.H3_is_pentagon(h3)) {
+            return (1L + 5L * (_ipow(7, n) - 1L) / 6L);
         } else {
             return _ipow(7, n);
         }
     }
 
+    /**
+     * h3ToChildrenSize returns the exact number of children for a h3 affress at a
+     * given child resolution.
+     *
+     * @param h3Address  H3 address to find the number of children of
+     * @param childRes  The child resolution you're interested in
+     *
+     * @return int      Exact number of children (handles hexagons and pentagons
+     *                  correctly)
+     */
+    public static long h3ToChildrenSize(String h3Address, int childRes) {
+        return h3ToChildrenSize(stringToH3(h3Address), childRes);
+    }
+
+    /**
+     * h3ToChildrenSize returns the exact number of children
+     *
+     * @param h3         H3Index to find the number of children.
+     *
+     * @return int      Exact number of children, 6 for Pentagons and 7 for hexagons,
+     */
+    public static int h3ToChildrenSize(long h3) {
+        if (H3Index.H3_get_resolution(h3) == MAX_H3_RES) {
+            throw new IllegalArgumentException("Invalid child resolution [" + MAX_H3_RES + "]");
+        }
+        return isPentagon(h3) ? 6 : 7;
+    }
+
+    /**
+     * h3ToChildrenSize returns the exact number of children
+     *
+     * @param h3Address H3 address to find the number of children.
+     *
+     * @return int      Exact number of children, 6 for Pentagons and 7 for hexagons,
+     */
+    public static int h3ToChildrenSize(String h3Address) {
+        return h3ToChildrenSize(stringToH3(h3Address));
+    }
+
+    /**
+     * h3ToNotIntersectingChildrenSize returns the exact number of children intersecting
+     * the given parent but not part of the children set.
+     *
+     * @param h3         H3Index to find the number of children.
+     *
+     * @return int      Exact number of children, 5 for Pentagons and 6 for hexagons,
+     */
+    public static int h3ToNotIntersectingChildrenSize(long h3) {
+        if (H3Index.H3_get_resolution(h3) == MAX_H3_RES) {
+            throw new IllegalArgumentException("Invalid child resolution [" + MAX_H3_RES + "]");
+        }
+        return isPentagon(h3) ? 5 : 6;
+    }
+
+    /**
+     * h3ToNotIntersectingChildrenSize returns the exact number of children intersecting
+     * the given parent but not part of the children set.
+     *
+     * @param h3Address H3 address to find the number of children.
+     *
+     * @return int      Exact number of children, 5 for Pentagons and 6 for hexagons,
+     */
+    public static int h3ToNotIntersectingChildrenSize(String h3Address) {
+        return h3ToNotIntersectingChildrenSize(stringToH3(h3Address));
+    }
+
     /**
      * _ipow does integer exponentiation efficiently. Taken from StackOverflow.
      *
@@ -344,8 +473,8 @@ public final class H3 {
      *
      * @return the exponentiated value
      */
-    private static int _ipow(int base, int exp) {
-        int result = 1;
+    private static long _ipow(int base, int exp) {
+        long result = 1;
         while (exp != 0) {
             if ((exp & 1) != 0) {
                 result *= base;

+ 0 - 306
libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java

@@ -1,306 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *
- * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License.
- *
- * Copyright 2021 Uber Technologies, Inc.
- */
-package org.elasticsearch.h3;
-
-/**
- * Iterator structures and functions for the children of a cell.
- */
-final class Iterator {
-    /**
-     * Invalid index used to indicate an error from latLngToCell and related
-     * functions or missing data in arrays of H3 indices. Analogous to NaN in
-     * floating point.
-     */
-    public static final long H3_NULL = 0;
-
-    /**
-     * The number of bits in a single H3 resolution digit.
-     */
-    private static final int H3_PER_DIGIT_OFFSET = 3;
-
-    /**
-     * IterCellsChildren: struct for iterating through the descendants of
-     * a given cell.
-     * <p>
-     * Constructors:
-     * <p>
-     * Initialize with either `iterInitParent` or `iterInitBaseCellNum`.
-     * `iterInitParent` sets up an iterator for all the children of a given
-     * parent cell at a given resolution.
-     * <p>
-     * `iterInitBaseCellNum` sets up an iterator for children cells, given
-     * a base cell number (0--121).
-     * <p>
-     * Iteration:
-     * <p>
-     * Step iterator with `iterStepChild`.
-     * During the lifetime of the `IterCellsChildren`, the current iterate
-     * is accessed via the `IterCellsChildren.h` member.
-     * When the iterator is exhausted or if there was an error in initialization,
-     * `IterCellsChildren.h` will be `H3_NULL` even after calling `iterStepChild`.
-     */
-    static class IterCellsChildren {
-        long h;
-        int _parentRes;  // parent resolution
-        int _skipDigit;  // this digit skips `1` for pentagons
-
-        IterCellsChildren(long h, int _parentRes, int _skipDigit) {
-            this.h = h;
-            this._parentRes = _parentRes;
-            this._skipDigit = _skipDigit;
-        }
-    }
-
-    /**
-     * Create a fully nulled-out child iterator for when an iterator is exhausted.
-     * This helps minimize the chance that a user will depend on the iterator
-     * internal state after it's exhausted, like the child resolution, for
-     * example.
-     */
-    private static IterCellsChildren nullIter() {
-        return new IterCellsChildren(H3_NULL, -1, -1);
-    }
-
-    /**
-     ## Logic for iterating through the children of a cell
-     We'll describe the logic for ....
-     - normal (non pentagon iteration)
-     - pentagon iteration. define "pentagon digit"
-     ### Cell Index Component Diagrams
-     The lower 56 bits of an H3 Cell Index describe the following index components:
-     - the cell resolution (4 bits)
-     - the base cell number (7 bits)
-     - the child cell digit for each resolution from 1 to 15 (3*15 = 45 bits)
-     These are the bits we'll be focused on when iterating through child cells.
-     To help describe the iteration logic, we'll use diagrams displaying the
-     (decimal) values for each component like:
-     child digit for resolution 2
-     /
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... |
-     |-----|-------------|---|---|---|---|---|---|-----|
-     |   9 |          17 | 5 | 3 | 0 | 6 | 2 | 1 | ... |
-     ### Iteration through children of a hexagon (but not a pentagon)
-     Iteration through the children of a *hexagon* (but not a pentagon)
-     simply involves iterating through all the children values (0--6)
-     for each child digit (up to the child's resolution).
-     For example, suppose a resolution 3 hexagon index has the following
-     components:
-     parent resolution
-     /
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... |
-     |-----|-------------|---|---|---|---|---|---|-----|
-     |   3 |          17 | 3 | 5 | 1 | 7 | 7 | 7 | ... |
-     The iteration through all children of resolution 6 would look like:
-     parent res  child res
-     /           /
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... |
-     |-----|-------------|---|---|---|---|---|---|---|---|-----|
-     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 0 | 7 | 7 | ... |
-     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 1 | 7 | 7 | ... |
-     | ... |             |   |   |   |   |   |   |   |   |     |
-     | 6   |          17 | 3 | 5 | 1 | 0 | 0 | 6 | 7 | 7 | ... |
-     | 6   |          17 | 3 | 5 | 1 | 0 | 1 | 0 | 7 | 7 | ... |
-     | 6   |          17 | 3 | 5 | 1 | 0 | 1 | 1 | 7 | 7 | ... |
-     | ... |             |   |   |   |   |   |   |   |   |     |
-     | 6   |          17 | 3 | 5 | 1 | 6 | 6 | 6 | 7 | 7 | ... |
-     ### Step sequence on a *pentagon* cell
-     Pentagon cells have a base cell number (e.g., 97) corresponding to a
-     resolution 0 pentagon, and have all zeros from digit 1 to the digit
-     corresponding to the cell's resolution.
-     (We'll drop the ellipses from now on, knowing that digits should contain
-     7's beyond the cell resolution.)
-     parent res      child res
-     /               /
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 0 |
-     Iteration through children of a *pentagon* is almost the same
-     as *hexagon* iteration, except that we skip the *first* 1 value
-     that appears in the "skip digit". This corresponds to the fact
-     that a pentagon only has 6 children, which are denoted with
-     the numbers {0,2,3,4,5,6}.
-     The skip digit starts at the child resolution position.
-     When iterating through children more than one resolution below
-     the parent, we move the skip digit to the left
-     (up to the next coarser resolution) each time we skip the 1 value
-     in that digit.
-     Iteration would start like:
-     parent res      child res
-     /               /
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 0 |
-     \
-     skip digit
-     Noticing we skip the 1 value and move the skip digit,
-     the next iterate would be:
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 2 |
-     \
-     skip digit
-     Iteration continues normally until we get to:
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 0 | 6 |
-     \
-     skip digit
-     which is followed by (skipping the 1):
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 2 | 0 |
-     \
-     skip digit
-     For the next iterate, we won't skip the `1` in the previous digit
-     because it is no longer the skip digit:
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 2 | 1 |
-     \
-     skip digit
-     Iteration continues normally until we're right before the next skip
-     digit:
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 0 | 6 | 6 |
-     \
-     skip digit
-     Which is followed by
-     | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 |
-     |-----|-------------|---|---|---|---|---|---|
-     |   6 |          97 | 0 | 0 | 0 | 2 | 0 | 0 |
-     \
-     skip digit
-     and so on.
-     */
-
-    /**
-     * Initialize a IterCellsChildren struct representing the sequence giving
-     * the children of cell `h` at resolution `childRes`.
-     * <p>
-     * At any point in the iteration, starting once
-     * the struct is initialized, IterCellsChildren.h gives the current child.
-     * <p>
-     * Also, IterCellsChildren.h == H3_NULL when all the children have been iterated
-     * through, or if the input to `iterInitParent` was invalid.
-     */
-    public static IterCellsChildren iterInitParent(long h, int childRes) {
-
-        int parentRes = H3Index.H3_get_resolution(h);
-
-        if (childRes < parentRes || childRes > Constants.MAX_H3_RES || h == H3_NULL) {
-            return nullIter();
-        }
-
-        long newH = zeroIndexDigits(h, parentRes + 1, childRes);
-        newH = H3Index.H3_set_resolution(newH, childRes);
-
-        int _skipDigit;
-        if (H3Index.H3_is_pentagon(newH)) {
-            // The skip digit skips `1` for pentagons.
-            // The "_skipDigit" moves to the left as we count up from the
-            // child resolution to the parent resolution.
-            _skipDigit = childRes;
-        } else {
-            // if not a pentagon, we can ignore "skip digit" logic
-            _skipDigit = -1;
-        }
-
-        return new IterCellsChildren(newH, parentRes, _skipDigit);
-    }
-
-    /**
-     * Step a IterCellsChildren to the next child cell.
-     * When the iteration is over, IterCellsChildren.h will be H3_NULL.
-     * Handles iterating through hexagon and pentagon cells.
-     */
-    public static void iterStepChild(IterCellsChildren it) {
-        // once h == H3_NULL, the iterator returns an infinite sequence of H3_NULL
-        if (it.h == H3_NULL) return;
-
-        int childRes = H3Index.H3_get_resolution(it.h);
-
-        incrementResDigit(it, childRes);
-
-        for (int i = childRes; i >= it._parentRes; i--) {
-            if (i == it._parentRes) {
-                // if we're modifying the parent resolution digit, then we're done
-                // *it = _null_iter();
-                it.h = H3_NULL;
-                return;
-            }
-
-            // PENTAGON_SKIPPED_DIGIT == 1
-            if (i == it._skipDigit && getResDigit(it, i) == CoordIJK.Direction.PENTAGON_SKIPPED_DIGIT.digit()) {
-                // Then we are iterating through the children of a pentagon cell.
-                // All children of a pentagon have the property that the first
-                // nonzero digit between the parent and child resolutions is
-                // not 1.
-                // I.e., we never see a sequence like 00001.
-                // Thus, we skip the `1` in this digit.
-                incrementResDigit(it, i);
-                it._skipDigit -= 1;
-                return;
-            }
-
-            // INVALID_DIGIT == 7
-            if (getResDigit(it, i) == CoordIJK.Direction.INVALID_DIGIT.digit()) {
-                incrementResDigit(it, i);  // zeros out it[i] and increments it[i-1] by 1
-            } else {
-                break;
-            }
-        }
-    }
-
-    // extract the `res` digit (0--7) of the current cell
-    private static int getResDigit(IterCellsChildren it, int res) {
-        return H3Index.H3_get_index_digit(it.h, res);
-    }
-
-    /**
-     * Zero out index digits from start to end, inclusive.
-     * No-op if start > end.
-     */
-    private static long zeroIndexDigits(long h, int start, int end) {
-        if (start > end) {
-            return h;
-        }
-
-        long m = 0;
-
-        m = ~m;
-        m <<= H3_PER_DIGIT_OFFSET * (end - start + 1);
-        m = ~m;
-        m <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - end);
-        m = ~m;
-
-        return h & m;
-    }
-
-    // increment the digit (0--7) at location `res`
-    private static void incrementResDigit(IterCellsChildren it, int res) {
-        long val = 1;
-        val <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - res);
-        it.h += val;
-    }
-}

+ 59 - 3
libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java

@@ -20,17 +20,70 @@ package org.elasticsearch.h3;
 
 import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 
+import org.apache.lucene.geo.Point;
 import org.apache.lucene.spatial3d.geom.GeoPoint;
 import org.apache.lucene.spatial3d.geom.GeoPolygon;
 import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
 import org.apache.lucene.spatial3d.geom.PlanetModel;
+import org.apache.lucene.tests.geo.GeoTestUtil;
+import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.test.ESTestCase;
+import org.hamcrest.Matchers;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 public class ParentChildNavigationTests extends ESTestCase {
 
+    public void testChildrenSize() {
+        Point point = GeoTestUtil.nextPoint();
+        int res = randomInt(H3.MAX_H3_RES - 1);
+        String h3Address = H3.geoToH3Address(point.getLat(), point.getLon(), res);
+        // check invalid resolutions
+        IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> H3.h3ToChildrenSize(h3Address, res));
+        assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution"));
+        ex = expectThrows(IllegalArgumentException.class, () -> H3.h3ToChildrenSize(h3Address, H3.MAX_H3_RES + 1));
+        assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution"));
+        ex = expectThrows(
+            IllegalArgumentException.class,
+            () -> H3.h3ToChildrenSize(H3.geoToH3(point.getLat(), point.getLon(), H3.MAX_H3_RES))
+        );
+        assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution"));
+        // check methods gives same answer
+        assertEquals(H3.h3ToChildrenSize(h3Address), H3.h3ToChildrenSize(h3Address, res + 1));
+        // check against brute force counting
+        int childrenRes = Math.min(H3.MAX_H3_RES, res + randomIntBetween(2, 7));
+        long numChildren = H3.h3ToChildrenSize(h3Address, childrenRes);
+        assertEquals(numChildren(h3Address, childrenRes), numChildren);
+    }
+
+    private long numChildren(String h3Address, int finalRes) {
+        if (H3.getResolution(h3Address) == finalRes) {
+            return 1;
+        }
+        long result = 0;
+        for (int i = 0; i < H3.h3ToChildrenSize(h3Address); i++) {
+            result += numChildren(H3.childPosToH3(h3Address, i), finalRes);
+        }
+        return result;
+    }
+
+    public void testNoChildrenIntersectingSize() {
+        Point point = GeoTestUtil.nextPoint();
+        int res = randomInt(H3.MAX_H3_RES - 1);
+        String h3Address = H3.geoToH3Address(point.getLat(), point.getLon(), res);
+        // check invalid resolutions
+        IllegalArgumentException ex = expectThrows(
+            IllegalArgumentException.class,
+            () -> H3.h3ToNotIntersectingChildrenSize(H3.geoToH3(point.getLat(), point.getLon(), H3.MAX_H3_RES))
+        );
+        assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution"));
+        // check against brute force counting
+        long numChildren = H3.h3ToNotIntersectingChildrenSize(h3Address);
+        assertEquals(H3.h3ToNoChildrenIntersecting(h3Address).length, numChildren);
+    }
+
     public void testParentChild() {
         String[] h3Addresses = H3.getStringRes0Cells();
         String h3Address = RandomPicks.randomFrom(random(), h3Addresses);
@@ -38,6 +91,9 @@ public class ParentChildNavigationTests extends ESTestCase {
         values[0] = h3Address;
         for (int i = 1; i < H3.MAX_H3_RES; i++) {
             h3Addresses = H3.h3ToChildren(h3Address);
+            // check all elements are unique
+            Set<String> mySet = Sets.newHashSet(h3Addresses);
+            assertEquals(mySet.size(), h3Addresses.length);
             h3Address = RandomPicks.randomFrom(random(), h3Addresses);
             values[i] = h3Address;
         }
@@ -84,9 +140,9 @@ public class ParentChildNavigationTests extends ESTestCase {
     }
 
     private void assertIntersectingChildren(String h3Address, String[] children) {
-        String[] intersectingNotChildren = H3.h3ToNoChildrenIntersecting(h3Address);
-        for (String noChild : intersectingNotChildren) {
-            GeoPolygon p = getGeoPolygon(noChild);
+        int size = H3.h3ToNotIntersectingChildrenSize(h3Address);
+        for (int i = 0; i < size; i++) {
+            GeoPolygon p = getGeoPolygon(H3.noChildIntersectingPosToH3(h3Address, i));
             int intersections = 0;
             for (String o : children) {
                 if (p.intersects(getGeoPolygon(o))) {