Skip to content

Core: Add spherical geography bounds builder - #17788

Open
huan233usc wants to merge 9 commits into
apache:mainfrom
huan233usc:geo-spherical-geography-bounds
Open

Core: Add spherical geography bounds builder#17788
huan233usc wants to merge 9 commits into
apache:mainfrom
huan233usc:geo-spherical-geography-bounds

Conversation

@huan233usc

@huan233usc huan233usc commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Geography bounds metrics need an XY box that covers geodesic edges on a sphere, where an edge can bulge in latitude beyond its endpoints and where longitude is periodic. This adds a package-private SphericalGeographyBoundsBuilder in core that accumulates the bounds of geography points and minor great-circle edges through addPoint / addEdge / build, modeling each geography as a sphere.

  • Compute the interior latitude extremum of each minor edge and conservatively expand it, so floating-point error cannot produce an under-covering latitude bound
  • Merge the covered longitude intervals by removing the largest uncovered circular gap, producing an antimeridian-wrapping interval (west > east) when that is the tighter box
  • Treat a vertex at a pole as having no longitude: it contributes latitude only, and a geography made up solely of pole vertices leaves longitude unconstrained and reports the full range
  • Keep a coordinate on the antimeridian as given, so +180 and -180 are preserved rather than folded onto one sign and the same coordinate yields the same interval whether it arrives as a point or a degenerate edge
  • Use world bounds for an ambiguous antipodal edge, and return no bounds for empty input or once an out-of-range coordinate is observed

The builder exposes only the spherical addPoint / addEdge / build kernel. Pole handling and the spherical interior-latitude extrema are kept separate internally so a follow-up can compose this kernel behind geography WKB traversal and algorithm dispatch, without introducing a single-implementation abstraction here.

Scope: geography, spherical only

This is the spherical algorithm kernel for a follow-up geography metrics integration. It deliberately does not add a generic GeographyBoundsBuilder, WKB traversal, Parquet writer wiring, polygon pole-containment detection, or spheroidal interpolation strategies such as Karney. Those belong in the integration layer, once another interpolation strategy exists, so this PR does not introduce a single-implementation abstraction prematurely.

Test Plan

  • Cover interior latitude bulges north and south, endpoint-only latitudes, and 2,000 deterministic random edges sampled along their minor arcs
  • Cover antimeridian wrapping, longitude interval union across disconnected lines, and largest-circular-gap selection
  • Cover poles: pole-only geographies that use the full longitude range, poles mixed with finite vertices, both poles, and antipodal and coincident endpoints
  • Cover invalid and empty input, where out-of-range, NaN, and infinite coordinates suppress the box
  • Run the targeted core test class, formatting checks, and style checks

Verification Commands

./gradlew :iceberg-core:test --tests org.apache.iceberg.TestSphericalGeographyBoundsBuilder
./gradlew :iceberg-core:spotlessCheck
./gradlew :iceberg-core:checkstyleMain :iceberg-core:checkstyleTest

AI Disclosure

  • Model: GPT-5
  • Platform/Tool: Codex
  • Human Oversight: partially reviewed
  • Prompt Summary: Implement and refine a focused spherical geography bounding-box builder and correctness tests.

@github-actions github-actions Bot added the core label Aug 24, 2026
@huan233usc huan233usc changed the title Core: Add spherical geography bounds collector Core: Add spherical geography bounds builder Aug 24, 2026
Xin Huang added 7 commits August 24, 2026 13:23
- Cover reference edge bounds in both endpoint orders across antimeridian, pole, and latitude-extremum cases.
- Cover multi-edge and point-set aggregation over circular longitude intervals.

Generated-by: Codex
Replace raw vector arrays with a typed private helper and use independent Haversine calculations in randomized tests.

Generated-by: Codex
Document the numerical thresholds and conservative latitude expansion with the associated vector tests and an ASCII arc diagram.

Generated-by: Codex
Replace independent coordinate, validity, and full-world flags with one explicit builder state machine.

Generated-by: Codex
Replace repeated latitude, longitude, and full-span literals with descriptive constants.

Generated-by: Codex
addPoint kept a coordinate on the antimeridian as given, but minimumLongitudeInterval
folded an edge with both endpoints there onto -180, so the same physical point produced
different bounds as a point versus a degenerate edge. Drop the sign-folding so +180 and
-180 are preserved on both paths. Also document that a pole vertex contributes latitude
only, since all meridians meet there.

@szehon-ho szehon-ho 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.

The spherical kernel looks sound — I couldn't find a case where the latitude extrema or the longitude union under-cover an arc, and I traced the sweep by hand through the antimeridian, pole-only, and wrapping-interval cases. Two things seem worth resolving before merge: the pole convention, which can produce an X bound that excludes an X value present in the file, and the per-vertex interval accumulation, which makes writer memory scale with a file's vertex count.

Two questions: will the follow-up integration compute the query-side geography bounding box with this same builder? That decides whether the pole convention is an internal consistency choice or a cross-implementation hazard. And with no production caller yet, the pole and NaN conventions can't be validated end-to-end, which is exactly where a mismatch would surface.

Comment on lines +73 to +78
// All meridians meet at a pole, so a vertex there has no single longitude that constrains
// the box; it contributes latitude only. A geography consisting solely of pole vertices
// leaves longitude unconstrained, which build() reports as the full range.
if (!isPole(latitude)) {
longitudeIntervals.add(new LongitudeInterval(longitude, longitude));
}

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.

Add the pole vertex's stored longitude to longitudeIntervals rather than dropping it. The spec's geography X rule is numeric — an object matches if x >= xmin OR x <= xmax — and GeographyEvaluator.intersects compares the raw x values with no notion that the pole lies on every meridian. So addPoint(-120, 90) followed by addPoint(40, 10) gives x=[40, 40], a bound that excludes an X value present in the file, and a predicate whose literal bound keeps the pole's longitude (POINT(-120 90)x=[-120, -120]) won't intersect it, so the file is pruned even though it holds a matching row. Including the longitude only widens the box, so it stays valid under the spherical reading too. addEdgeWithPole drops the pole endpoint's longitude the same way.

// under-covering bound; the result is then clamped to [-90, 90].
private static final double LATITUDE_SCALING_FACTOR = 1.0000001;

private final List<LongitudeInterval> longitudeIntervals = Lists.newArrayList();

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.

Merge or compact these intervals as they accumulate instead of appending one per vertex. Nothing is ever removed, so a builder accumulating a whole data file holds one LongitudeInterval per vertex, and longitudeBounds() then allocates up to four LongitudeEvents per interval and sorts them — a file with 1M polygons at 100 vertices each is 100M edges. GeometryBoundsBuilder keeps O(1) state for the same job.

Comment on lines +255 to +262
private static boolean coordinatesAreValid(double longitude, double latitude) {
return Double.isFinite(longitude)
&& Double.isFinite(latitude)
&& longitude >= MIN_LONGITUDE
&& longitude <= MAX_LONGITUDE
&& latitude >= MIN_LATITUDE
&& latitude <= MAX_LATITUDE;
}

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.

Skip a NaN coordinate instead of latching INVALID for the whole file. POINT EMPTY is encoded as POINT(NaN NaN), so a single empty geography costs every other value in the file its bounds. The spec says NaN ordinates are skipped per dimension, and GeometryBoundsBuilder.DimensionBounds.add already does that. Suppressing on out-of-range coordinates still makes sense, since the sphere math has no meaning there.

import org.apache.iceberg.geospatial.GeospatialBound;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;

/** Builds an XY bounding box from geography points and minor great-circle edges on a sphere. */

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.

Document the contract here: build() returns null for empty input, one invalid coordinate turns bounds off permanently, an ambiguous antipodal edge yields world bounds, the box may wrap with west > east, and latitude extrema are deliberately widened so bounds are not tight. These are the details the follow-up integration has to reason about, and GeometryBoundsBuilder spells out the equivalent.

Comment on lines +216 to +219
double longitude1 = random.nextDouble() * 360.0 - 180.0;
double latitude1 = random.nextDouble() * 140.0 - 70.0;
double longitude2 = normalizeLongitude(longitude1 + random.nextDouble() * 240.0 - 120.0);
double latitude2 = random.nextDouble() * 140.0 - 70.0;

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.

Consider widening these ranges to reach the poles and near-180 longitude separations. Latitudes are drawn from ±70 and the separation from ±120, so no sampled arc passes near or over a pole — which is where "the minor arc spans the shorter endpoint longitude difference" is hardest to verify by inspection, since longitude sweeps almost 180° over a short arc there. Only (5, 10)-(175, 10) and (5, 10)-(-175.1, 10) cover that today.

edgeCase(10.0, 0.0, 120.0, 0.0, 10.0, 0.0, 120.0, 0.0),
edgeCase(10.0, 0.0, 120.0, 1.0, 10.0, 0.0, 120.0, 1.06416356550489),
edgeCase(10.0, 10.0, 20.0, 20.0, 10.0, 10.0, 20.0, 20.0),
edgeCase(10.0, 60.0, 70.0, 70.0, 10.0, 60.0, 70.0, 70.20558550568438),

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.

Assert that the bound is at least the true extremum rather than pinning the widened value. 70.20558550568438 is the exact vertex latitude times LATITUDE_SCALING_FACTOR, and the 1e-9 tolerance is far tighter than that 7e-6 offset, so tuning the margin means rewriting about ten expectations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants