You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@sedona.apache.org by "jiayuasu (via GitHub)" <gi...@apache.org> on 2026/03/12 06:21:01 UTC

[PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

jiayuasu opened a new pull request, #2732:
URL: https://github.com/apache/sedona/pull/2732

   ## Did you read the Contributor Guide?
   
   - Yes, I have read the [Contributor Rules](https://sedona.apache.org/latest/community/rule/) and [Contributor Development Guide](https://sedona.apache.org/latest/community/develop/)
   
   ## Is this PR related to a ticket?
   
   - Yes, and the PR name follows the format `[GH-XXX] my subject`. Closes #2731
   
   ## What changes were proposed in this PR?
   
   Implement 5 GeoSeries functions for the geopandas compatibility module. These were previously blocked by upstream bugs (#2720, #2721, #2722), now fixed in #2730.
   
   Distance metrics:
   
   - `frechet_distance(other, align, densify)` -> `ST_FrechetDistance` (densify not supported by Sedona, raises `NotImplementedError`)
   - `hausdorff_distance(other, align, densify)` -> `ST_HausdorffDistance` (supports `densify` parameter via `densityFrac`)
   
   Binary predicates:
   
   - `geom_equals(other, align)` -> `ST_Equals`
   
   Linear referencing:
   
   - `interpolate(distance, normalized)` -> `ST_LineInterpolatePoint` (when `normalized=False`, divides distance by `ST_Length` to convert to fraction)
   - `project(other, normalized, align)` -> `ST_LineLocatePoint` (when `normalized=False`, multiplies fraction by `ST_Length` to convert to absolute distance)
   
   Each function follows the established pattern from #2701, #2710, and #2726:
   
   - base.py: Docstring with examples + `_delegate_to_geometry_column` call
   - geoseries.py: ST function call via `_row_wise_operation`
   - test_geoseries.py: Unit test with expected values + GeoDataFrame delegation check
   - test_match_geopandas_series.py: Comparison test against real geopandas output
   
   ## How was this patch tested?
   
   10 new tests (5 unit tests + 5 match tests) all passing locally.
   
   ## Did this PR include necessary documentation updates?
   
   - No, this PR does not affect any public API so no need to change the documentation.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929662860


##########
common/src/main/java/org/apache/sedona/common/Functions.java:
##########
@@ -1485,7 +1486,8 @@ public static Geometry triangulatePolygon(Geometry geom) {
     return ConstrainedDelaunayTriangulator.triangulate(geom);
   }
 
-  public static double lineLocatePoint(Geometry geom, Geometry point) {
+  public static Double lineLocatePoint(Geometry geom, Geometry point) {
+    if (geom.isEmpty()) return null;
     double length = geom.getLength();
     LengthIndexedLine indexedLine = new LengthIndexedLine(geom);
     return indexedLine.indexOf(point.getCoordinate()) / length;

Review Comment:
   `lineLocatePoint` can still return `NaN`/`Infinity` (or throw) for valid-but-degenerate inputs. If `geom` is non-empty but has `length == 0` (e.g., repeated coordinates), this does `index / 0`. Also, if `point` is empty, `point.getCoordinate()` is null and `indexOf(...)` will NPE. Please add guards (e.g., return 0.0 for zero-length lines and null for empty points) to avoid producing invalid results / runtime failures and to better match PostGIS/GeoPandas behavior.
   



##########
common/src/test/java/org/apache/sedona/common/FunctionsTest.java:
##########
@@ -4407,6 +4407,21 @@ public void lineLocatePoint() {
     assertEquals(expectedResult3, actual3, FP_TOLERANCE);
   }
 
+  @Test
+  public void lineLocatePointEmpty() {
+    LineString emptyLine = GEOMETRY_FACTORY.createLineString();
+    Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1));
+    assertNull(Functions.lineLocatePoint(emptyLine, point));
+  }
+
+  @Test

Review Comment:
   The new empty-geometry tests are helpful, but they don’t cover the closely related degenerate case where a LineString is non-empty but has zero length (e.g., repeated coordinates), which currently leads to `0/0 -> NaN` in `Functions.lineLocatePoint`. Adding an assertion for that case (and for an empty `point` argument) would prevent regressions and validate the intended semantics.
   



##########
python/sedona/spark/geopandas/geoseries.py:
##########
@@ -1330,6 +1330,89 @@ def distance(self, other, align=None) -> pspd.Series:
         )
         return result
 
+    def frechet_distance(self, other, align=None, densify=None) -> pspd.Series:
+        if densify is not None:
+            raise NotImplementedError(
+                "Sedona does not support the densify parameter for frechet_distance."
+            )
+
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stf.ST_FrechetDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def hausdorff_distance(self, other, align=None, densify=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        if densify is not None:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"), densify)
+        else:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def geom_equals(self, other, align=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stp.ST_Equals(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            returns_geom=False,
+            default_val=False,
+        )
+        return _to_bool(result)
+
+    def interpolate(self, distance, normalized=False) -> "GeoSeries":
+        other_series, extended = self._make_series_of_val(distance)
+        align = not extended
+
+        if normalized:
+            spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), F.col("R"))
+        else:
+            length = stf.ST_Length(F.col("L"))
+            fraction = F.when(length == 0, F.lit(0.0)).otherwise(F.col("R") / length)
+            spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), fraction)
+        return self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align=align,
+            returns_geom=True,
+        )
+
+    def project(self, other, normalized=False, align=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        if normalized:
+            spark_expr = stf.ST_LineLocatePoint(F.col("L"), F.col("R"))
+        else:
+            spark_expr = stf.ST_LineLocatePoint(F.col("L"), F.col("R")) * stf.ST_Length(
+                F.col("L")
+            )

Review Comment:
   `project(normalized=False)` multiplies `ST_LineLocatePoint(...)` by `ST_Length(L)`. For zero-length (non-empty) lines, `ST_LineLocatePoint` currently computes `index/length` (0/0) which yields `NaN`, and `NaN * 0` remains `NaN` (instead of the expected 0.0 absolute distance). Consider explicitly handling `ST_Length(L) == 0` (return 0.0) for both normalized and absolute modes to avoid propagating `NaN`.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929580893


##########
python/sedona/spark/geopandas/geoseries.py:
##########
@@ -1330,6 +1330,89 @@ def distance(self, other, align=None) -> pspd.Series:
         )
         return result
 
+    def frechet_distance(self, other, align=None, densify=None) -> pspd.Series:
+        if densify is not None:
+            raise NotImplementedError(
+                "Sedona does not support the densify parameter for frechet_distance."
+            )
+
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stf.ST_FrechetDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def hausdorff_distance(self, other, align=None, densify=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        if densify is not None:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"), densify)
+        else:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def geom_equals(self, other, align=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stp.ST_Equals(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            returns_geom=False,
+            default_val=False,
+        )
+        return _to_bool(result)
+
+    def interpolate(self, distance, normalized=False) -> "GeoSeries":
+        other_series, extended = self._make_series_of_val(distance)
+        align = not extended
+
+        if normalized:
+            spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), F.col("R"))
+        else:
+            spark_expr = stf.ST_LineInterpolatePoint(
+                F.col("L"), F.col("R") / stf.ST_Length(F.col("L"))
+            )

Review Comment:
   Correction: my previous reply was wrong — we implemented exactly the fix you suggested: `F.when(length == 0, F.lit(0.0)).otherwise(F.col("R") / length)`. Additionally, we fixed `ST_LineInterpolatePoint` in Java to return `POINT EMPTY` for empty input, and `ST_LineLocatePoint` to return `null`. See 2d1b4ac.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929309188


##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+

Review Comment:
   This text is copied verbatim from geopandas' own docstring for `frechet_distance`. Keeping it consistent with upstream.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2922864490


##########
python/sedona/spark/geopandas/geoseries.py:
##########
@@ -1330,6 +1330,89 @@ def distance(self, other, align=None) -> pspd.Series:
         )
         return result
 
+    def frechet_distance(self, other, align=None, densify=None) -> pspd.Series:
+        if densify is not None:
+            raise NotImplementedError(
+                "Sedona does not support the densify parameter for frechet_distance."
+            )
+
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stf.ST_FrechetDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def hausdorff_distance(self, other, align=None, densify=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        if densify is not None:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"), densify)
+        else:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def geom_equals(self, other, align=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stp.ST_Equals(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            returns_geom=False,
+            default_val=False,
+        )
+        return _to_bool(result)
+
+    def interpolate(self, distance, normalized=False) -> "GeoSeries":
+        other_series, extended = self._make_series_of_val(distance)
+        align = not extended
+
+        if normalized:
+            spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), F.col("R"))
+        else:
+            spark_expr = stf.ST_LineInterpolatePoint(
+                F.col("L"), F.col("R") / stf.ST_Length(F.col("L"))
+            )

Review Comment:
   For normalized=False, interpolate computes a fraction via `distance / ST_Length(L)`. If `ST_Length(L)` is 0 (e.g., empty or zero-length lines), this produces inf/NaN and can lead to errors or invalid results inside `ST_LineInterpolatePoint`. Consider explicitly handling `ST_Length(L) == 0` (and/or empty geometries) to match geopandas/shapely behavior instead of relying on division-by-zero semantics.
   ```suggestion
               len_col = stf.ST_Length(F.col("L"))
               frac_col = F.when(len_col == F.lit(0), F.lit(0.0)).otherwise(
                   F.col("R") / len_col
               )
               spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), frac_col)
   ```



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        result = s1.geom_equals(Point(0, 0))
+        expected = pd.Series([True, False, False])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(df_result, expected)
+
+    def test_interpolate(self):
+        s = GeoSeries(
+            [
+                LineString([(0, 0), (2, 0), (0, 2)]),
+                LineString([(0, 0), (2, 2)]),
+                LineString([(2, 0), (0, 2)]),
+            ]
+        )

Review Comment:
   The new unit tests don’t exercise empty or zero-length linear geometries for interpolate/project. Given the implementation can involve division by `ST_Length`, adding at least one empty `LineString()` / `LinearRing()` case (and a degenerate zero-length line) would help catch division-by-zero/edge-case behavior and ensure compatibility with geopandas.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)

Review Comment:
   These interpolate match tests filter out empty geometries (`LineString()` / `LinearRing()`), which are present in the test fixtures. This means empty-geometry behavior is untested and potential runtime errors/semantic mismatches are hidden. Please add explicit assertions for empty inputs (or ensure the implementation matches geopandas for empties) instead of skipping them.
   ```suggestion
               # Ensure behavior matches GeoPandas for both empty and non-empty geometries
               sgpd_result = GeoSeries(geom).interpolate(1.0)
               gpd_result = gpd.GeoSeries(geom).interpolate(1.0)
               self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
   
               sgpd_result = GeoSeries(geom).interpolate(0.5, normalized=True)
               gpd_result = gpd.GeoSeries(geom).interpolate(0.5, normalized=True)
   ```



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+    def test_project(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            point = Point(1, 1)
+            sgpd_result = GeoSeries(non_empty).project(point)
+            gpd_result = gpd.GeoSeries(non_empty).project(point)
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).project(point, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).project(point, normalized=True)

Review Comment:
   These project match tests filter out empty geometries (`LineString()` / `LinearRing()`), which are present in the fixtures. This prevents validating empty/zero-length behavior (which is a common edge case for linear referencing) against geopandas. Please add coverage for empties or adjust the implementation to safely handle them.
   ```suggestion
               sgpd_result = GeoSeries(geom).interpolate(1.0)
               gpd_result = gpd.GeoSeries(geom).interpolate(1.0)
               self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
   
               sgpd_result = GeoSeries(geom).interpolate(0.5, normalized=True)
               gpd_result = gpd.GeoSeries(geom).interpolate(0.5, normalized=True)
               self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
   
       def test_project(self):
           for geom in [self.linestrings, self.linearrings]:
               point = Point(1, 1)
               sgpd_result = GeoSeries(geom).project(point)
               gpd_result = gpd.GeoSeries(geom).project(point)
               self.check_pd_series_equal(sgpd_result, gpd_result)
   
               sgpd_result = GeoSeries(geom).project(point, normalized=True)
               gpd_result = gpd.GeoSeries(geom).project(point, normalized=True)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929309898


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+    def test_project(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            point = Point(1, 1)
+            sgpd_result = GeoSeries(non_empty).project(point)
+            gpd_result = gpd.GeoSeries(non_empty).project(point)
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).project(point, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).project(point, normalized=True)

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+    def test_project(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            point = Point(1, 1)
+            sgpd_result = GeoSeries(non_empty).project(point)
+            gpd_result = gpd.GeoSeries(non_empty).project(point)
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).project(point, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).project(point, normalized=True)

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+

Review Comment:
   The GeometryCollection skip is inherited from other binary predicate tests in this file. While `ST_Equals` now uses `equalsTopo`, the skip is kept for consistency with the existing test patterns. Can remove in a follow-up if we confirm all GeometryCollection cases pass end-to-end.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929308396


##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The densify parameter is not supported by Sedona.
+            Passing a value will raise a ``NotImplementedError``.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.frechet_distance(s2)
+        0    2.0
+        1    2.0

Review Comment:
   Fixed in 87958a18fc. Changed to `1    1.0` to match actual output.



##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The densify parameter is not supported by Sedona.
+            Passing a value will raise a ``NotImplementedError``.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.frechet_distance(s2)
+        0    2.0
+        1    2.0
+        dtype: float64
+
+        See also
+        --------
+        GeoSeries.hausdorff_distance
+        """
+        return _delegate_to_geometry_column(
+            "frechet_distance", self, other, align, densify
+        )
+
+    def hausdorff_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the Hausdorff distance to aligned `other`.
+
+        The Hausdorff distance is the largest distance consisting of any point in `self`
+        with the nearest point in `other`.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The fraction by which to densify each segment. Each segment will be
+            split into a number of equal-length subsegments whose fraction of
+            the segment length is closest to the given fraction.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.hausdorff_distance(s2)
+        0    2.0
+        1    2.0

Review Comment:
   Fixed in 87958a18fc. Changed to `1    1.0` to match actual output.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929310307


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+    def test_project(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            point = Point(1, 1)
+            sgpd_result = GeoSeries(non_empty).project(point)
+            gpd_result = gpd.GeoSeries(non_empty).project(point)
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).project(point, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).project(point, normalized=True)

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929578758


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   Correction: my previous reply was wrong. Empty geometries DO work in Sedona GeoSeries — the failures I observed were caused by a stale JAR (pre-PR #2730 fix). All skips have been removed in 2d1b4ac. The match tests now include empty geometries and pass.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   Correction: my previous reply was wrong. Empty geometries DO work in Sedona GeoSeries — the failures I observed were caused by a stale JAR (pre-PR #2730 fix). All skips have been removed in 2d1b4ac. The match tests now include empty geometries and pass.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)

Review Comment:
   Correction: my previous replies were wrong. Empty geometries work fine in Sedona GeoSeries. The actual issues were upstream Java bugs:
   
   - `ST_LineInterpolatePoint` crashed with `ArrayIndexOutOfBoundsException` on empty input — fixed to return `POINT EMPTY`
   - Added a `length == 0` guard in the Python layer for non-normalized mode (as Copilot suggested)
   
   All skips removed in 2d1b4ac.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929310794


##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   Sedona GeoSeries cannot hold empty geometries (Spark UDT limitation), so we can't add empty geometry unit tests. The match tests skip empties for the same reason.



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   Sedona GeoSeries cannot hold empty geometries (Spark UDT limitation), so we can't add empty geometry unit tests. The match tests skip empties for the same reason.



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        result = s1.geom_equals(Point(0, 0))
+        expected = pd.Series([True, False, False])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(df_result, expected)
+
+    def test_interpolate(self):
+        s = GeoSeries(
+            [
+                LineString([(0, 0), (2, 0), (0, 2)]),
+                LineString([(0, 0), (2, 2)]),
+                LineString([(2, 0), (0, 2)]),
+            ]
+        )

Review Comment:
   Sedona GeoSeries cannot hold empty geometries (Spark UDT limitation), so we can't add empty geometry unit tests. The match tests skip empties for the same reason.



##########
python/sedona/spark/geopandas/geoseries.py:
##########
@@ -1330,6 +1330,89 @@ def distance(self, other, align=None) -> pspd.Series:
         )
         return result
 
+    def frechet_distance(self, other, align=None, densify=None) -> pspd.Series:
+        if densify is not None:
+            raise NotImplementedError(
+                "Sedona does not support the densify parameter for frechet_distance."
+            )
+
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stf.ST_FrechetDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def hausdorff_distance(self, other, align=None, densify=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        if densify is not None:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"), densify)
+        else:
+            spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            default_val=None,
+        )
+        return result
+
+    def geom_equals(self, other, align=None) -> pspd.Series:
+        other_series, extended = self._make_series_of_val(other)
+        align = False if extended else align
+
+        spark_expr = stp.ST_Equals(F.col("L"), F.col("R"))
+        result = self._row_wise_operation(
+            spark_expr,
+            other_series,
+            align,
+            returns_geom=False,
+            default_val=False,
+        )
+        return _to_bool(result)
+
+    def interpolate(self, distance, normalized=False) -> "GeoSeries":
+        other_series, extended = self._make_series_of_val(distance)
+        align = not extended
+
+        if normalized:
+            spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), F.col("R"))
+        else:
+            spark_expr = stf.ST_LineInterpolatePoint(
+                F.col("L"), F.col("R") / stf.ST_Length(F.col("L"))
+            )

Review Comment:
   Empty geometries cannot be constructed in Sedona GeoSeries (Spark UDT limitation), so the division-by-zero with empty lines cannot occur in practice. For zero-length (non-empty) lines, geopandas returns `POINT(x y)` at the line's location regardless of distance, which is the same behavior we get from `ST_LineInterpolatePoint` when the fraction overflows — Sedona clamps to [0, 1].



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929310190


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2922570504


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+

Review Comment:
   This GeometryCollection skip seems unnecessary now: Sedona’s `ST_Equals` implementation has been updated to use `equalsTopo`, which supports GeometryCollection inputs. Keeping this skip means match tests won’t validate the previously-broken case; consider removing it and checking GeoPandas parity for GeometryCollection equality.
   ```suggestion
   
   ```



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The comment and skip condition here appear stale: Sedona’s Hausdorff distance implementation now returns null for empty geometries (not 0.0), which should compare equal to GeoPandas’ NaN in these match tests. Removing the skip would add coverage for empty inputs and avoid preserving an inaccurate comment.
   ```suggestion
   
   ```



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   These new distance method tests don’t cover empty-geometry behavior. Since empty inputs were a known compatibility gap (and are expected to yield NaN/null rather than 0.0), please add a small assertion for empty geometries to prevent regressions.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   This skip for empty geometries looks outdated: in the current Sedona codebase, `GeomUtils.getFrechetDistance` returns null for empty inputs, which should round-trip to NaN and match GeoPandas. Skipping empties reduces coverage of an important edge case; consider removing the skip and asserting the empty-geometry behavior matches GeoPandas.
   ```suggestion
   
   ```



##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+

Review Comment:
   The Fréchet distance definition in this docstring is incorrect: describing it as “greatest distance between any point in A and the closest point in B” matches Hausdorff distance, not Fréchet. Please update the description to reflect Fréchet’s ordered/monotone traversal (and clarify that the discrete variant uses vertices, with optional densification).
   ```suggestion
           The Fréchet distance is a measure of similarity between curves that respects
           their ordering: it is the minimal, over all continuous and monotone
           traversals of both geometries, of the maximum distance between the
           simultaneously traversed points (often illustrated as the length of the
           shortest leash needed for a person and a dog walking along the curves
           without backtracking).
   
           The *discrete* Fréchet distance is an approximation that only considers the
           vertices of the geometries when computing this traversal-based metric. A
           ``densify`` parameter (when supported) refines this approximation by
           inserting additional points along line segments between vertices before
           computing the distance.
   ```



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   `geom_equals` is now expected to work with `GeometryCollection` inputs (after the upstream `ST_Equals` fix), but this unit test only covers Point/Polygon cases. Consider adding a GeometryCollection case to ensure the method doesn’t regress on that previously-failing input type.



##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The densify parameter is not supported by Sedona.
+            Passing a value will raise a ``NotImplementedError``.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.frechet_distance(s2)
+        0    2.0
+        1    2.0

Review Comment:
   The example output here doesn’t match the behavior validated by the unit test added in this PR (which expects the second value to be 1.0 for these inputs). Please update the example output so it is consistent with the actual results.
   ```suggestion
           1    1.0
   ```



##########
python/sedona/spark/geopandas/base.py:
##########
@@ -2363,6 +2363,263 @@ def distance(self, other, align=None):
         """
         return _delegate_to_geometry_column("distance", self, other, align)
 
+    def frechet_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the discrete Fréchet distance to aligned `other`.
+
+        The Fréchet distance is a measure of similarity: it is the greatest distance
+        between any point in A and the closest point in B. The discrete distance is an
+        approximation of this metric: only vertices are considered. The parameter
+        ``densify`` makes this approximation less coarse by splitting the line segments
+        between vertices before computing the distance.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The densify parameter is not supported by Sedona.
+            Passing a value will raise a ``NotImplementedError``.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.frechet_distance(s2)
+        0    2.0
+        1    2.0
+        dtype: float64
+
+        See also
+        --------
+        GeoSeries.hausdorff_distance
+        """
+        return _delegate_to_geometry_column(
+            "frechet_distance", self, other, align, densify
+        )
+
+    def hausdorff_distance(self, other, align=None, densify=None):
+        """Returns a ``Series`` containing the Hausdorff distance to aligned `other`.
+
+        The Hausdorff distance is the largest distance consisting of any point in `self`
+        with the nearest point in `other`.
+
+        The operation works on a 1-to-1 row-wise manner.
+
+        Parameters
+        ----------
+        other : GeoSeries or geometric object
+            The GeoSeries (elementwise) or geometric object to find the
+            distance to.
+        align : bool | None (default None)
+            If True, automatically aligns GeoSeries based on their indices. None defaults to True.
+            If False, the order of elements is preserved.
+        densify : float, optional
+            The fraction by which to densify each segment. Each segment will be
+            split into a number of equal-length subsegments whose fraction of
+            the segment length is closest to the given fraction.
+
+        Returns
+        -------
+        Series (float)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s1 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 0), (1, 0), (2, 0)]),
+        ...         LineString([(0, 0), (1, 1)]),
+        ...     ]
+        ... )
+        >>> s2 = GeoSeries(
+        ...     [
+        ...         LineString([(0, 1), (1, 2), (2, 1)]),
+        ...         LineString([(1, 0), (2, 1)]),
+        ...     ]
+        ... )
+
+        >>> s1.hausdorff_distance(s2)
+        0    2.0
+        1    2.0

Review Comment:
   The example output here appears inconsistent with the unit test added in this PR (which expects the second value to be 1.0 for these inputs). Please update the example output to match the actual behavior.
   ```suggestion
           1    1.0
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929310138


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+

Review Comment:
   The skip is correct. Sedona GeoSeries cannot hold empty geometries due to a Spark UDT registration limitation — constructing a GeoSeries with `LineString()` or similar empty geometries throws `Py4JJavaError`. This is not something we can fix at the geopandas compatibility layer. The empty geometry edge case is a known limitation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929580275


##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   Correction: empty geometries DO work in Sedona GeoSeries — my previous claim about a "Spark UDT limitation" was wrong. The match tests now include empty geometries and pass. We also fixed the upstream Java bugs (`ST_LineInterpolatePoint` crash, `ST_LineLocatePoint` returning `-Infinity`) and added Java + Scala Spark tests for empty geometry handling. See 2d1b4ac.



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)

Review Comment:
   Correction: empty geometries DO work in Sedona GeoSeries — my previous claim about a "Spark UDT limitation" was wrong. The match tests now include empty geometries and pass. We also fixed the upstream Java bugs (`ST_LineInterpolatePoint` crash, `ST_LineLocatePoint` returning `-Infinity`) and added Java + Scala Spark tests for empty geometry handling. See 2d1b4ac.



##########
python/tests/geopandas/test_geoseries.py:
##########
@@ -2635,6 +2635,177 @@ def test_relate(self):
         expected = pd.Series(["FF2F11212", "212101212"])
         self.check_pd_series_equal(result, expected)
 
+    def test_frechet_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.frechet_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test that densify raises NotImplementedError
+        with pytest.raises(NotImplementedError):
+            s1.frechet_distance(s2, densify=0.5)
+
+    def test_hausdorff_distance(self):
+        s1 = GeoSeries(
+            [
+                LineString([(0, 0), (1, 0), (2, 0)]),
+                LineString([(0, 0), (1, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                LineString([(0, 1), (1, 2), (2, 1)]),
+                LineString([(1, 0), (2, 1)]),
+            ]
+        )
+
+        result = s1.hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        line = LineString([(0, 1), (1, 2), (2, 1)])
+        result = s1.hausdorff_distance(line)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(df_result, expected)
+
+        # Test with densify parameter
+        result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+        expected = pd.Series([2.0, 1.0])
+        self.check_pd_series_equal(result, expected)
+
+    def test_geom_equals(self):
+        s1 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(1, 1),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        s2 = GeoSeries(
+            [
+                Point(0, 0),
+                Point(2, 2),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+
+        result = s1.geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(result, expected)
+
+        # Test with single geometry
+        result = s1.geom_equals(Point(0, 0))
+        expected = pd.Series([True, False, False])
+        self.check_pd_series_equal(result, expected)
+
+        # Test that GeoDataFrame works too
+        df_result = s1.to_geoframe().geom_equals(s2, align=False)
+        expected = pd.Series([True, False, True])
+        self.check_pd_series_equal(df_result, expected)
+
+    def test_interpolate(self):
+        s = GeoSeries(
+            [
+                LineString([(0, 0), (2, 0), (0, 2)]),
+                LineString([(0, 0), (2, 2)]),
+                LineString([(2, 0), (0, 2)]),
+            ]
+        )

Review Comment:
   Correction: empty geometries DO work in Sedona GeoSeries — my previous claim about a "Spark UDT limitation" was wrong. The match tests now include empty geometries and pass. We also fixed the upstream Java bugs (`ST_LineInterpolatePoint` crash, `ST_LineLocatePoint` returning `-Infinity`) and added Java + Scala Spark tests for empty geometry handling. See 2d1b4ac.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu commented on code in PR #2732:
URL: https://github.com/apache/sedona/pull/2732#discussion_r2929579557


##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,100 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).geom_equals(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2), align=False)
+                gpd_result = gpd.GeoSeries(geom).geom_equals(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_interpolate(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            sgpd_result = GeoSeries(non_empty).interpolate(1.0)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(1.0)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).interpolate(0.5, normalized=True)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+    def test_project(self):
+        for geom in [self.linestrings, self.linearrings]:
+            # Skip empty geometries
+            non_empty = [g for g in geom if not g.is_empty]
+            if not non_empty:
+                continue
+
+            point = Point(1, 1)
+            sgpd_result = GeoSeries(non_empty).project(point)
+            gpd_result = gpd.GeoSeries(non_empty).project(point)
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            sgpd_result = GeoSeries(non_empty).project(point, normalized=True)
+            gpd_result = gpd.GeoSeries(non_empty).project(point, normalized=True)

Review Comment:
   Correction: my previous reply was wrong. Empty geometries work fine. `ST_LineLocatePoint` was returning `-Infinity` for empty input (division by zero in Java) — fixed to return `null` by changing the return type from `double` to `Double`. Skip removed in 2d1b4ac.



##########
python/tests/geopandas/test_match_geopandas_series.py:
##########
@@ -1285,6 +1285,104 @@ def test_relate(self):
                 )
                 self.check_pd_series_equal(sgpd_result, gpd_result)
 
+    def test_frechet_distance(self):
+        line_pairs = [
+            (self.linestrings, self.linestrings),
+            (self.linearrings, self.linearrings),
+            (self.linestrings, self.linearrings),
+        ]
+        for geom, geom2 in line_pairs:
+            # Skip pairs containing empty geometries
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2), align=True)
+            gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).frechet_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).frechet_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_hausdorff_distance(self):
+        for geom, geom2 in self.pairs:
+            # Skip pairs with empty geometries — Sedona returns 0.0 instead of NaN
+            if any(g.is_empty for g in geom) or any(g.is_empty for g in geom2):
+                continue
+
+            sgpd_result = GeoSeries(geom).hausdorff_distance(
+                GeoSeries(geom2), align=True
+            )
+            gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                gpd.GeoSeries(geom2), align=True
+            )
+            self.check_pd_series_equal(sgpd_result, gpd_result)
+
+            if len(geom) == len(geom2):
+                sgpd_result = GeoSeries(geom).hausdorff_distance(
+                    GeoSeries(geom2), align=False
+                )
+                gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+                    gpd.GeoSeries(geom2), align=False
+                )
+                self.check_pd_series_equal(sgpd_result, gpd_result)
+
+    def test_geom_equals(self):
+        for geom, geom2 in self.pairs:
+            if self.contains_any_geom_collection(geom, geom2):
+                continue
+

Review Comment:
   Correction: my previous reply was wrong. The GeometryCollection skip has been removed in 2d1b4ac. All GeometryCollection tests now pass with `equalsTopo`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance, geom_equals, interpolate, project [sedona]

Posted by "jiayuasu (via GitHub)" <gi...@apache.org>.
jiayuasu merged PR #2732:
URL: https://github.com/apache/sedona/pull/2732


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@sedona.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org