Skip to content

fix(rust/sedona-raster): align RS_Rotation with Sedona Spark's formula - #1223

Merged
james-willis merged 5 commits into
apache:mainfrom
james-willis:james/db-573-rs_rotation-diverges-from-sedona-spark-under-shear
Sep 4, 2026
Merged

fix(rust/sedona-raster): align RS_Rotation with Sedona Spark's formula#1223
james-willis merged 5 commits into
apache:mainfrom
james-willis:james/db-573-rs_rotation-diverges-from-sedona-spark-under-shear

Conversation

@james-willis

@james-willis james-willis commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

RS_Rotation parity coverage that turned into a kernel fix, in three commits:

  1. Probe-first coverage of the divergence: SedonaDB computed atan2(-skewX, scaleX) where Sedona Spark computes acos(scaleX / sqrt(scaleX² + skewY²)), negated when skewY > 0. They coincide only while |skewX| == |skewY|.
  2. One parity module per RS_ function: test_rs_scalar.pytest_rs_bandnodatavalue.py, RS_Rotation cases into test_rs_rotation.py (the suite's layout convention going forward).
  3. The fix: GeoTransformEx::rotation now uses Spark's formula — the angle of the grid's actual column-axis direction (scaleX, skewY).

Why align rather than document the divergence

  • The old formula didn't just diverge under shear: it misreported a plain rigid rotation of non-square (2x3) pixels as −0.7137 for a true −π/6 (the new formula recovers −π/6 exactly, for any pixel shape).
  • RS_GeoTransform (feat(rust/sedona-raster-functions): add RS_GeoTransform composite accessor #1221) ports Spark's decomposition, so after this change its thetaI agrees with RS_Rotation by construction — no internal inconsistency.
  • acos incidentally fixes a zero-sign nit: the old atan2 returned IEEE -0.0 for axis-aligned rasters where Spark returns +0.0.

Blast radius: GeoTransformEx::rotation feeds only the RS_Rotation UDF; rust unit tests in geo_transform.rs, affine_transformation.rs, and rs_geotransform.rs are rewritten around derived cases (axis-aligned asserts the positive zero's sign; sheared expectation matches Spark bit-for-bit in probing).

The parity tests (all passing, anchored)

  • Axis-aligned (north-up, south-up) → exactly 0.0, sign included.
  • Rigid ±30° rotations, square and non-square pixels → round(-theta, 12).
  • Shear → round(-acos(scaleX/hypot(scaleX, skewY)), 12).

Rotated/sheared queries compare through ROUND(..., 12), with the evidence in a comment above the tests: neither runtime promises a bit-exact acos (Java Math.acos is specified to within 1 ulp of correctly rounded; Rust f64::acos is the platform libm with no accuracy contract), and a 72-angle sweep on macOS vs Temurin 17 measured 2 angles differing by exactly 1 ulp — π/6 among them — with the unlucky set being libm-dependent, so a raw comparison would be cross-platform flaky rather than strict. The anchored constants sit thousands of ulps from a 12-digit rounding boundary, so ROUND collapses 1-ulp pairs without ever splitting them. Axis-aligned cases compare raw and exact.

Verification

cargo test -p sedona-raster and -p sedona-raster-functions green locally; integration/spark-parity at 28 passed, 14 xfailed (RS_Rotation contributes 7 anchored passes, no xfails). This branch carries the #1217 lane so CI runs the parity suite.

Aside, noted while probing and out of scope: SedonaDB evaluates -0.0 = 0.0 as false in SQL where Spark says true.

SedonaDB computes atan2(-skewX, scaleX); Sedona Spark
acos(scaleX / hypot(scaleX, skewY)), negated when skewY > 0. The formulas
coincide exactly when |skewX| == |skewY| — axis-aligned grids and rigid
rotations of square pixels — and separate everywhere else. Anchored
passing coverage for the agreeing cases (zero-sign normalized with + 0.0,
the 1-ulp libm-vs-JVM noise rounded to 12 digits), xfails for the
divergences: IEEE -0 vs 0 on axis-aligned grids, a rigid rotation of
non-square pixels misreported by SedonaDB (-0.7137 for a true -pi/6), and
shear.
@github-actions
github-actions Bot requested a review from prantogg September 2, 2026 20:31
test_rs_scalar.py becomes test_rs_bandnodatavalue.py, and the RS_Rotation
cases move to their own test_rs_rotation.py — the parity suite keeps one
module per function.
RS_Rotation now computes acos(scaleX / hypot(scaleX, skewY)), negated
when skewY > 0 — the angle of the grid's column-axis direction, Sedona
Spark's formula. The previous atan2(-skewX, scaleX) mixed the row axis's
skew into the answer: it agreed with Spark only while |skewX| == |skewY|,
misreported a rigid 30-degree rotation of non-square 2x3 pixels as
-0.7137 (true -pi/6, which the new formula recovers exactly), diverged
under shear, and returned IEEE -0.0 for axis-aligned rasters where Spark
returns +0.0 (acos also fixes that). Once RS_GeoTransform lands its
thetaI agrees with RS_Rotation by construction.

The parity xfails flip to anchored passing tests: axis-aligned compares
exactly including the zero's sign; rotated and sheared grids round to 12
digits since the raw doubles can differ by one ulp across runtimes.
@james-willis james-willis changed the title test(integration): RS_Rotation parity between SedonaDB and Sedona Spark fix(rust/sedona-raster): align RS_Rotation with Sedona Spark's formula Sep 2, 2026
@@ -14,7 +14,9 @@
# KIND, either express or implied. See the License for the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

drivey by: clean up the parity harness to more comprehensively model 1 file for 1 function

@james-willis
james-willis marked this pull request as ready for review September 2, 2026 21:50
@james-willis
james-willis requested review from paleolimbot and removed request for prantogg September 2, 2026 21:50
Neither runtime promises a bit-exact acos (Java Math.acos is specified to
1 ulp of correctly rounded; Rust f64::acos is the platform libm with no
contract). Measured on macOS vs Temurin 17: 2 of 72 swept angles differ
by exactly 1 ulp, pi/6 among them, and which angles are unlucky is
libm-dependent — a raw comparison would be cross-platform flaky, not
strict. The anchored constants sit thousands of ulps from a 12-digit
rounding boundary, so ROUND collapses the 1-ulp pairs safely.
Comment thread integration/spark-parity/test_rs_bandnodatavalue.py Outdated

@paleolimbot paleolimbot 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.

Thank you!

@james-willis
james-willis merged commit cf1342c into apache:main Sep 4, 2026
17 checks passed
@james-willis
james-willis deleted the james/db-573-rs_rotation-diverges-from-sedona-spark-under-shear branch September 4, 2026 20:29
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