Skip to content

pybind: Add S2CellId iteration (children, cells, ranges)#640

Open
deustis wants to merge 11 commits into
google:masterfrom
deustis:deustis/s2cell_id_iteration
Open

pybind: Add S2CellId iteration (children, cells, ranges)#640
deustis wants to merge 11 commits into
google:masterfrom
deustis:deustis/s2cell_id_iteration

Conversation

@deustis

@deustis deustis commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Add Python iteration support for S2CellId hierarchy traversal. Two new methods are exposed:

  • S2CellId.children(level=None) — returns a range over the immediate children, or all descendants at a given level.
  • S2CellId.cells(level) — static method; returns a range over all cells at a given level on the same face.

Both return an S2CellIdRange, which supports the Python sequence protocol: len(), integer indexing, contiguous slicing (step=1), in, for, and reversed(). Slicing and indexing are O(1) via S2CellId::advance() and distance_from_begin().

S2CellIdRange is a lazy view rather than a list because the ranges can be very large (a face cell's children(30) has ~10^9 elements). It supports len(), indexing, and slicing — not just iteration — because those are natural operations on a contiguous range of cells and can all be implemented in O(1). The range helpers live in src/python/ as an implementation detail of the bindings and are not part of the public C++ API.

deustis added 8 commits June 15, 2026 18:23
Add S2CellId.children() and S2CellId.cells() returning S2CellIdRange,
a pythonic sequence over cells at a common Hilbert-curve level with
__len__, __getitem__ (int + slice), __contains__, __iter__, and
__reversed__.

Design notes from PR google#593 review:
- __len__ narrows int64 count to Py_ssize_t and raises OverflowError
  when the range exceeds Py_ssize_t::max (can happen on 32-bit Python
  for ranges above cells(15)). Users needing the full count on such
  platforms should use S2CellIdRange.size().
- __getitem__ supports integer indexing and slicing. Slices with
  step != 1 raise ValueError since they cannot be represented as a
  contiguous range; reversed() covers the step=-1 case.
- S2CellIdForwardIter / S2CellIdReverseIter are marked module_local,
  since they are implementation details of the iteration protocol and
  should not collide across pybind modules.
Moves the S2CellIdRange, S2CellIdForwardIter, and S2CellIdReverseIter
types out of the Python bindings and into the s2 library as first-class
C++ types.  The C++ layer uses std::optional<S2CellId> instead of
throwing; all exception handling is consolidated in the bindings behind
named helpers (RangeLenOrThrow, RangeItemOrThrow, RangeSliceOrThrow,
NextOrStop).  Adds a gtest suite covering size(), at(), slice(),
contains(), and both iterator types.
- Rename S2CellIdForwardIter/ReverseIter to …Iterator
- at(): return S2CellId directly (precondition: i in [0,size())),
  add ABSL_DCHECK; bindings validate bounds before calling
- slice(): add ABSL_DCHECKs for 0<=start<=stop<=size() preconditions
- RangeLenOrThrow: add 32-bit platform hint to overflow message
- RangeItemOrThrow: check bounds directly, handle double-negative index
- Drop tests for invalid at() inputs (now DCHECK territory)
- Rename helpers to take primitive inputs and return validated values;
  range method calls (at(), slice()) stay in the binding lambdas
- RangeLenOrThrow -> LenOrThrow
- RangeItemOrThrow -> SliceIndexOrThrow(i, n) -> int64_t
- ComputeSlice + RangeSliceOrThrow -> SliceBoundsOrThrow(n, s) -> pair
- Add Python data model doc links to SliceIndexOrThrow and SliceBoundsOrThrow
- Fix RangeItemOrThrow comment to say "0-based index"
- Use short-circuit in ValidateSliceStepOrThrow
- Copyright year 2024 -> 2026 in new files
- Revert Traversal section update in README.md
- SliceIndexOrThrow -> IndexOrThrow
- ValidateSliceStepOrThrow -> StepOrThrow
- SliceBoundsOrThrow -> ClampedSlice (pure computation, no step validation);
  binding now calls StepOrThrow directly before ClampedSlice
- Update slice doc URL to #sequences
- Rename s2cellid_range.{h,cc,_test.cc} to s2cell_id_range.* to match
  the s2cell_id_* naming convention used throughout the library.
- Fix children(level) to reject level == self.level(); the lower bound
  was self.level() so child_begin(self.level()) returned self, not its
  children.
- Fix test_children_of_leaf_raises expected message to match what
  MaybeThrowIfLeaf actually emits.
- Add test_children_at_own_level_raises to cover the level-bound fix.
- Replace S2CellIdReverseIterator's done-flag design with a sentinel:
  cur starts at range.end and decrements before yielding, so empty
  ranges work without a special case and __reversed__ becomes a
  one-liner.
- Add ABSL_DCHECK in size() that begin and end are at the same level.
- Rename iterator Python classes to _S2CellId{Forward,Reverse}Iterator
  to signal they are implementation details, not public API.
Comment thread src/s2/s2cell_id_range.h Outdated
Comment thread src/python/s2cell_id_range.h

@jmr jmr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a good approach. More info in the PR would have made my life easier. problem/solution/alternatives considered/why this is best.

https://google.github.io/eng-practices/review/developer/cl-descriptions.html#informative

Comment thread src/python/s2cell_id_bindings.cc
Comment thread src/python/s2cell_id_bindings.cc Outdated
Comment thread src/s2/s2cell_id_range.h Outdated
Comment thread src/python/s2cell_id_bindings.cc
Comment thread src/s2/s2cell_id_range_test.cc Outdated
Comment thread src/s2/s2cell_id_range_test.cc Outdated
Comment thread src/s2/s2cell_id_range.h Outdated
deustis added 2 commits July 10, 2026 17:40
- Move s2cell_id_range.h/.cc and test from src/s2/ to src/python/ per
  reviewer request (implementation detail of Python bindings, not public API)
- Add file-level comment to s2cell_id_range.h
- Fix copyright to "Google LLC" (drop "All Rights Reserved")
- Update include guard and include paths
- Add comment explaining why py::slice::compute() is not used (Py_ssize_t
  is 32-bit on 32-bit platforms; S2CellIdRange needs int64_t)
- Fix alignment in ClampedSlice
- Add __eq__ and __repr__ to S2CellIdRange pybind binding
- Use std::optional<S2CellId> instead of auto in test iterators
- Rename reverse iterator variable to rit
@deustis

deustis commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

I think it's a good approach. More info in the PR would have made my life easier. problem/solution/alternatives considered/why this is best.

https://google.github.io/eng-practices/review/developer/cl-descriptions.html#informative

Ack. Updated

@deustis

deustis commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@jmr, thanks for the review! All comments should be addressed

@deustis deustis requested a review from jmr July 10, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants