Skip to content

Commit 85ae427

Browse files
committed
pybind: Add S2Interval bindings (#524)
Add pybind11 bindings for S1Interval. Also add interface notes to the python README
1 parent b18735a commit 85ae427

File tree

8 files changed

+505
-16
lines changed

8 files changed

+505
-16
lines changed

src/python/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,26 @@ py_library(
2525
pybind_extension(
2626
name = "s2geometry_bindings",
2727
srcs = ["module.cc"],
28+
copts = ["-DNDEBUG"],
2829
deps = [
30+
":s1interval_bindings",
2931
":s2point_bindings",
3032
],
3133
)
3234

35+
pybind_library(
36+
name = "s1interval_bindings",
37+
srcs = ["s1interval_bindings.cc"],
38+
copts = ["-DNDEBUG"],
39+
deps = [
40+
"//:s2",
41+
],
42+
)
43+
3344
pybind_library(
3445
name = "s2point_bindings",
3546
srcs = ["s2point_bindings.cc"],
47+
copts = ["-DNDEBUG"],
3648
deps = [
3749
"//:s2",
3850
],
@@ -42,6 +54,12 @@ pybind_library(
4254
# Python Tests
4355
# ========================================
4456

57+
py_test(
58+
name = "s1interval_test",
59+
srcs = ["s1interval_test.py"],
60+
deps = [":s2geometry_pybind"],
61+
)
62+
4563
py_test(
4664
name = "s2point_test",
4765
srcs = ["s2point_test.py"],

src/python/README.md

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,9 @@ The S2 Geometry library is transitioning from SWIG-based bindings to pybind11-ba
1313

1414
Once the pybind11 bindings are feature-complete and stable, the SWIG bindings will be deprecated and the pybind11 package will be renamed to `s2geometry` to become the primary Python API.
1515

16-
## Directory Structure
16+
## User Guide
1717

18-
```
19-
python/
20-
├── module.cc # Binding module entry point
21-
├── s2point_bindings.cc # Bindings for S2Point (add more *_bindings.cc as needed)
22-
├── s2geometry_pybind/ # Dir for Python package
23-
│ └── __init__.py # Package initialization
24-
├── s2point_test.py # Tests for S2Point (add more *_test.py as needed)
25-
└── BUILD.bazel # Build rules for bindings, library, and tests
26-
```
27-
28-
## Usage Example
18+
### Usage Example
2919

3020
```python
3121
import s2geometry_pybind as s2
@@ -36,8 +26,64 @@ sum_point = p1 + p2
3626
print(sum_point)
3727
```
3828

29+
### Interface Notes
30+
31+
The Python bindings follow the C++ API closely but with Pythonic conventions:
32+
33+
**Naming Conventions:**
34+
- **Modules** core classes exist within the top-level module; we intend to define submodules for utility classes.
35+
- **Class names** remain unchanged (e.g., `S2Point`, `S1Angle`, `R1Interval`)
36+
- **Method names** are converted to snake_case (converted from UpperCamelCase C++ function names)
37+
38+
**Properties vs. Methods:**
39+
- **Simple coordinate accessors** are properties: `point.x`, `point.y`, `interval.lo`, `interval.hi`
40+
- **Some properties are mutable**: if the C++ class has a trivial set_foo method for the propertiy, then the Python property is mutable (otherwise it is a read-only property).
41+
- **Conversions and computations** are defined as method, not properties: `angle.radians()`, `angle.degrees()`, `interval.get_length()`
42+
43+
**Invalid/Unnormalized Values:**
44+
- Some constructors and accept invalid values or unormalized values.
45+
- Examples:
46+
- `S1Interval(0.0, 4.0)` with `4.0 > π` creates interval where `is_valid()` is `False`
47+
- `S2Point(3.0, 4.0, 0.0)` creates an unnormalized point outside of the unit sphere; use `normalize()` to normalize.
48+
- In **C++** invalid values will typically trigger (`ABSL_DCHECK`) in when compiled with debug options.
49+
- In **Python bindings** debug assertions (`ABSL_DCHECK`) are disabled, matching the production optimized C++ behavior.
50+
51+
**Type Conversions:**
52+
- Python floats map to C++ `double`
53+
- Functions that accept angles as `double` expect values in **radians** (same as C++ interface)
54+
55+
**Operators:**
56+
- Standard Python operators work as expected: `+`, `-`, `*`, `==`, `!=`, `<`, `>` (for C++ classes that implement those operators)
57+
58+
**String Representations:**
59+
- `repr()` outputs developer-centric representations: `S1Angle.from_degrees(45.0)`
60+
- `str()` outputs simpler human-readable representations (e.g. `45.0 degrees`)
61+
62+
**Vector Inheritance:**
63+
- In C++, various geometry classes inherit from or expose vector types (e.g., `S2Point` inherits from `Vector3_d`, `R2Point` is a type alias for `Vector2_d`, `R1Interval` returns bounds as `Vector2_d`)
64+
- The Python bindings **do not expose** this inheritance hierarchy; it is treated as an implementation detail
65+
- Instead, classes that inherit from a vector expose key functions from the `BasicVector` interface (e.g., `norm()`, `dot_prod()`, `cross_prod()`)
66+
- C++ functions that accept or return a vector object use a Python tuple (of length matching the vector dimension)
67+
- Array indexing operators (e.g., `point[0]`) are **not supported**.
68+
69+
**Serialization:**
70+
- The C++ Encoder/Decoder serialization functions are **not currently bound**
71+
- These may be added in the future if there is a need for binary serialization
72+
3973
## Development
4074

75+
### Directory Structure
76+
77+
```
78+
python/
79+
├── module.cc # Binding module entry point
80+
├── s2point_bindings.cc # Bindings for S2Point (add more *_bindings.cc as needed)
81+
├── s2geometry_pybind/ # Dir for Python package
82+
│ └── __init__.py # Package initialization
83+
├── s2point_test.py # Tests for S2Point (add more *_test.py as needed)
84+
└── BUILD.bazel # Build rules for bindings, library, and tests
85+
```
86+
4187
### Building with Bazel (pybind11 bindings)
4288

4389
Bazel can be used for development and testing of the new pybind11-based bindings.
@@ -82,4 +128,18 @@ To add bindings for a new class:
82128
1. Create `<classname>_bindings.cc` with pybind11 bindings
83129
2. Update `BUILD.bazel` to add a new `pybind_library` target
84130
3. Update `module.cc` to call your binding function
85-
4. Create tests in `<classname>_test.py`
131+
4. Create tests in `<classname>_test.py`
132+
133+
### Binding File Organization
134+
135+
Use the following sections to organize the binding files and tests:
136+
137+
1. **Constructors** - Default constructors and constructors with parameters
138+
2. **Factory methods** - Static factory methods (e.g., `from_degrees`, `from_radians`, `zero`, `invalid`)
139+
3. **Properties** - Mutable and read-only properties (e.g., coordinate accessors like `x`, `y`, `lo`, `hi`)
140+
4. **Predicates** - Simple boolean state checks (e.g., `is_empty`, `is_valid`, `is_full`)
141+
5. **Geometric operations** - All other methods including conversions, computations, containment checks, set operations, normalization, and distance calculations
142+
6. **Vector operations** - Methods from the Vector base class (e.g., `norm`, `norm2`, `normalize`, `dot_prod`, `cross_prod`, `angle`). Only applicable to classes that inherit from `util/math/vector.h`
143+
7. **Operators** - Operator overloads (e.g., `==`, `+`, `*`, comparison operators)
144+
8. **String representation** - `__repr__`, `__str__`, and string conversion methods like `to_string_in_degrees`
145+
9. **Module-level functions** - Standalone functions (e.g., trigonometric functions for S1Angle)

src/python/module.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
namespace py = pybind11;
44

55
// Forward declarations for binding functions
6+
void bind_s1interval(py::module& m);
67
void bind_s2point(py::module& m);
78

89
PYBIND11_MODULE(s2geometry_bindings, m) {
910
m.doc() = "S2 Geometry Python bindings using pybind11";
11+
12+
// Bind core geometry classes in dependency order
13+
bind_s1interval(m);
1014
bind_s2point(m);
1115
}

src/python/s1interval_bindings.cc

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#include <pybind11/pybind11.h>
2+
#include <pybind11/operators.h>
3+
4+
#include "s2/s1interval.h"
5+
6+
namespace py = pybind11;
7+
8+
void bind_s1interval(py::module& m) {
9+
py::class_<S1Interval>(m, "S1Interval")
10+
// Constructors
11+
.def(py::init<>(), "Default constructor (empty interval)")
12+
.def(py::init<double, double>(),
13+
py::arg("lo"), py::arg("hi"),
14+
"Construct interval from lo and hi bounds (in radians)")
15+
16+
// Static factory methods
17+
.def_static("empty", &S1Interval::Empty, "Return an empty interval")
18+
.def_static("full", &S1Interval::Full, "Return a full interval")
19+
.def_static("from_point", &S1Interval::FromPoint, py::arg("p"),
20+
"Construct interval containing a single point")
21+
.def_static("from_point_pair", &S1Interval::FromPointPair,
22+
py::arg("p1"), py::arg("p2"),
23+
"Construct minimal interval containing two points")
24+
25+
// Properties
26+
.def_property("lo", &S1Interval::lo, &S1Interval::set_lo, "Lower bound")
27+
.def_property("hi", &S1Interval::hi, &S1Interval::set_hi, "Upper bound")
28+
.def("bounds", [](const S1Interval& self) {
29+
return py::make_tuple(self.lo(), self.hi());
30+
}, "Return bounds as a tuple (lo, hi)")
31+
32+
// Predicates
33+
.def("is_valid", &S1Interval::is_valid, "Check if interval is valid")
34+
.def("is_full", &S1Interval::is_full, "Check if interval is full")
35+
.def("is_empty", &S1Interval::is_empty, "Check if interval is empty")
36+
.def("is_inverted", &S1Interval::is_inverted,
37+
"Check if interval is inverted (lo > hi)")
38+
39+
// Geometric operations
40+
.def("get_center", &S1Interval::GetCenter, "Return center of interval")
41+
.def("get_length", &S1Interval::GetLength, "Return length of interval")
42+
.def("get_complement_center", &S1Interval::GetComplementCenter,
43+
"Return center of complement")
44+
.def("contains", py::overload_cast<double>(&S1Interval::Contains, py::const_),
45+
py::arg("p"), "Check if interval contains a point")
46+
.def("interior_contains", py::overload_cast<double>(
47+
&S1Interval::InteriorContains, py::const_),
48+
py::arg("p"), "Check if interval's interior contains a point")
49+
.def("contains", py::overload_cast<const S1Interval&>(
50+
&S1Interval::Contains, py::const_),
51+
py::arg("other"), "Check if interval contains another interval")
52+
.def("interior_contains", py::overload_cast<const S1Interval&>(
53+
&S1Interval::InteriorContains, py::const_),
54+
py::arg("other"),
55+
"Check if interval's interior contains another interval")
56+
.def("intersects", &S1Interval::Intersects, py::arg("other"),
57+
"Check if interval intersects another")
58+
.def("interior_intersects", &S1Interval::InteriorIntersects,
59+
py::arg("other"),
60+
"Check if interval's interior intersects another")
61+
.def("add_point", &S1Interval::AddPoint, py::arg("p"),
62+
"Expand interval to include a point")
63+
.def("project", &S1Interval::Project, py::arg("p"),
64+
"Return closest point in interval")
65+
.def("expanded", &S1Interval::Expanded, py::arg("margin"),
66+
"Return expanded interval")
67+
.def("union", &S1Interval::Union, py::arg("other"),
68+
"Return union with another interval")
69+
.def("intersection", &S1Interval::Intersection, py::arg("other"),
70+
"Return intersection with another interval")
71+
.def("complement", &S1Interval::Complement,
72+
"Return complement of this interval")
73+
.def("get_directed_hausdorff_distance",
74+
&S1Interval::GetDirectedHausdorffDistance,
75+
py::arg("other"),
76+
"Return directed Hausdorff distance to another interval")
77+
.def("approx_equals", &S1Interval::ApproxEquals,
78+
py::arg("other"), py::arg("max_error") = 1e-15,
79+
"Check if approximately equal")
80+
81+
// Operators
82+
.def(py::self == py::self, "Check equality")
83+
.def(py::self != py::self, "Check inequality")
84+
85+
// String representation
86+
.def("__repr__", [](const S1Interval& i) {
87+
return "S1Interval(" + std::to_string(i.lo()) + ", " +
88+
std::to_string(i.hi()) + ")";
89+
})
90+
.def("__str__", [](const S1Interval& i) {
91+
if (i.is_empty()) return std::string("[∅]");
92+
if (i.is_full()) return std::string("[0, 2π)");
93+
return "[" + std::to_string(i.lo()) + ", " +
94+
std::to_string(i.hi()) + "]";
95+
});
96+
}

0 commit comments

Comments
 (0)