You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/09/02 19:27:49 UTC
(sedona-db) branch main updated: chore(python/sedonadb): accept a bbox in create_random_raster_view (#1211)
This is an automated email from the ASF dual-hosted git repository.
james-willis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 76186eed9 chore(python/sedonadb): accept a bbox in create_random_raster_view (#1211)
76186eed9 is described below
commit 76186eed9c7083da677ddabb886909c6082ba0ea
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Wed Sep 2 12:27:43 2026 -0700
chore(python/sedonadb): accept a bbox in create_random_raster_view (#1211)
---
python/sedonadb/python/sedonadb/raster_testing.py | 82 +++++++++++++++++------
python/sedonadb/python/sedonadb/testing.py | 22 ++++--
python/sedonadb/tests/test_raster_testing.py | 79 ++++++++++++++++++++++
3 files changed, 155 insertions(+), 28 deletions(-)
diff --git a/python/sedonadb/python/sedonadb/raster_testing.py b/python/sedonadb/python/sedonadb/raster_testing.py
index 40e9f8cf1..0a8870d9b 100644
--- a/python/sedonadb/python/sedonadb/raster_testing.py
+++ b/python/sedonadb/python/sedonadb/raster_testing.py
@@ -36,7 +36,7 @@ then every side carries the same CRS.
"""
import math
-from dataclasses import dataclass
+from dataclasses import InitVar, dataclass
from typing import Any, List, Mapping, Optional, Tuple
import numpy as np
@@ -46,18 +46,38 @@ import numpy as np
class DecodedRaster:
"""One raster decoded to plain values.
- `pixels` is `(band, rows, cols)`, `gdal_transform` is GDAL-order
- `(origin_x, scale_x, skew_x, origin_y, skew_y, scale_y)`, and `nodata`
- holds one sentinel per band (unpacked in the band's dtype).
+ `pixels` is `(band, rows, cols)` and `nodata` holds one sentinel per band
+ (unpacked in the band's dtype). The grid is stated as exactly one of
+ `gdal_transform` — GDAL-order `(origin_x, scale_x, skew_x, origin_y,
+ skew_y, scale_y)` — or, for a north-up grid, the readable `bbox` spelling
+ `(minx, miny, maxx, maxy)` (pixel size derived from the data's shape),
+ mirroring `write_geotiff`. Either way the instance carries a
+ `gdal_transform`: decoders produce transforms and `assert_decoded_equal`
+ compares them, so `bbox` is construction sugar for anchors, not a second
+ stored representation.
`compression` is the codec name of the decoded container when one was
read (GeoTIFF decodes only); it is carried for encoder tests to assert
on and deliberately not part of `assert_decoded_equal`.
"""
pixels: "np.ndarray"
- gdal_transform: Tuple[float, ...]
- nodata: List[Any]
+ gdal_transform: Optional[Tuple[float, ...]] = None
+ nodata: Optional[List[Any]] = None
compression: Optional[str] = None
+ bbox: InitVar[Optional[Tuple[float, float, float, float]]] = None
+
+ def __post_init__(self, bbox):
+ if self.nodata is None:
+ raise ValueError("nodata is required (one entry per band)")
+ # A plain-transform construction skips _resolve_transform so decoders
+ # (which never pass bbox) stay rasterio-free.
+ if bbox is not None or self.gdal_transform is None:
+ _, height, width = self.pixels.shape
+ self.gdal_transform = tuple(
+ _resolve_transform(
+ bbox, self.gdal_transform, width=width, height=height
+ ).to_gdal()
+ )
def _is_nodata(sampled, nodata) -> bool:
@@ -127,19 +147,33 @@ def decode_geotiff_bytes(data: bytes) -> DecodedRaster:
)
+def _resolve_transform(bbox, gdal_transform, *, width, height):
+ """The rasterio `Affine` from exactly one of `bbox`/`gdal_transform`."""
+ from rasterio.transform import Affine, from_bounds
+
+ if (gdal_transform is None) == (bbox is None):
+ raise ValueError("pass exactly one of gdal_transform or bbox")
+ if bbox is not None:
+ return from_bounds(*bbox, width=width, height=height)
+ return Affine.from_gdal(*gdal_transform)
+
+
def write_geotiff(
- path, data: "np.ndarray", *, gdal_transform, nodata=None, crs=None
+ path, data: "np.ndarray", *, bbox=None, gdal_transform=None, nodata=None, crs=None
) -> None:
"""Write a `(bands, height, width)` array as a GeoTIFF.
- `gdal_transform` is GDAL-order `(origin_x, scale_x, skew_x, origin_y,
- skew_y, scale_y)`; `nodata` (optional) becomes the per-band nodata of
- every band. `crs` (optional) is any CRS rasterio accepts; parity fixtures
- stay CRS-less unless an engine requires one, and then use the same CRS
- everywhere so nothing reprojects.
+ The grid is placed by exactly one of `bbox` — `(minx, miny, maxx, maxy)`,
+ the readable spelling: north-up, no skew, pixel size derived from the
+ data's shape (rasterio's `transform.from_bounds`) — or `gdal_transform` —
+ GDAL-order `(origin_x, scale_x, skew_x, origin_y, skew_y, scale_y)`, for
+ grids a bbox cannot express (skew, south-up).
+ `nodata` (optional) becomes the per-band nodata of every band. `crs`
+ (optional) is any CRS rasterio accepts; parity fixtures stay CRS-less
+ unless an engine requires one, and then use the same CRS everywhere so
+ nothing reprojects.
"""
import rasterio
- from rasterio.transform import Affine
bands, height, width = data.shape
with rasterio.open(
@@ -150,7 +184,7 @@ def write_geotiff(
width=width,
count=bands,
dtype=str(data.dtype),
- transform=Affine.from_gdal(*gdal_transform),
+ transform=_resolve_transform(bbox, gdal_transform, width=width, height=height),
nodata=nodata,
crs=crs,
) as dst:
@@ -164,7 +198,8 @@ def write_random_geotiff(
bands,
height,
width,
- gdal_transform,
+ bbox=None,
+ gdal_transform=None,
crs=None,
nodata=None,
plants=None,
@@ -173,23 +208,28 @@ def write_random_geotiff(
Combines `random_raster_data` (dtype extremes planted in opposite corners)
with `write_geotiff` — the input-raster fixture shape shared by raster warp
- parity tests. `gdal_transform`, `crs`, and `nodata` are as `write_geotiff`;
- `plants` is as `random_raster_data`.
+ parity tests. `bbox`/`gdal_transform` (exactly one), `crs`, and `nodata`
+ are as `write_geotiff`; `plants` is as `random_raster_data`.
"""
data = random_raster_data(
dtype, bands=bands, height=height, width=width, plants=plants
)
- write_geotiff(path, data, gdal_transform=gdal_transform, nodata=nodata, crs=crs)
+ write_geotiff(
+ path, data, bbox=bbox, gdal_transform=gdal_transform, nodata=nodata, crs=crs
+ )
-def write_grid_geotiff(path, *, gdal_transform, width, height, crs=None) -> None:
+def write_grid_geotiff(
+ path, *, bbox=None, gdal_transform=None, width, height, crs=None
+) -> None:
"""Write a zeroed single-band GeoTIFF whose only role is to define a grid.
Its pixels are never read — only its extent, resolution, and CRS matter,
- e.g. as the reference grid for `RS_ReprojectMatch`.
+ e.g. as the reference grid for `RS_ReprojectMatch`. `bbox`/`gdal_transform`
+ (exactly one) are as `write_geotiff`.
"""
data = np.zeros((1, height, width), dtype="uint8")
- write_geotiff(path, data, gdal_transform=gdal_transform, crs=crs)
+ write_geotiff(path, data, bbox=bbox, gdal_transform=gdal_transform, crs=crs)
def assert_transform_and_nodata(got: DecodedRaster, expected: DecodedRaster) -> None:
diff --git a/python/sedonadb/python/sedonadb/testing.py b/python/sedonadb/python/sedonadb/testing.py
index af7e88050..a619c2aef 100644
--- a/python/sedonadb/python/sedonadb/testing.py
+++ b/python/sedonadb/python/sedonadb/testing.py
@@ -196,28 +196,36 @@ class DBEngine:
bands=2,
height=6,
width=7,
- gdal_transform=(100.0, 2.0, 0.0, 500.0, 0.0, -3.0),
+ bbox=None,
+ gdal_transform=None,
nodata=None,
plants=None,
) -> "DBEngine":
"""Write a random GeoTIFF at `path` and register it as view `name`
(see `create_raster_view`).
- The default grid is small and north-up/CRS-less so nothing reprojects
- and results stay bit-comparable across engines. The pixels come from
- `sedonadb.raster_testing.random_raster_data` with a fixed seed, so
- registering the same `path` on several engines rewrites identical
- bytes and every engine sees the same raster. `nodata` and `plants` are
- as `write_random_geotiff`.
+ The grid is placed by `bbox` (`(minx, miny, maxx, maxy)`, north-up,
+ no skew) or, for grids a bbox cannot express, a raw GDAL
+ `gdal_transform` — at most one; the default is
+ `bbox=(100, 482, 114, 500)`, whose extent divided by the default 7x6
+ grid gives 2x3 pixels. The grid is small and north-up/CRS-less so
+ nothing reprojects and results stay bit-comparable across engines.
+ The pixels come from `sedonadb.raster_testing.random_raster_data`
+ with a fixed seed, so registering the same `path` on several engines
+ rewrites identical bytes and every engine sees the same raster.
+ `nodata` and `plants` are as `write_random_geotiff`.
"""
from sedonadb.raster_testing import write_random_geotiff
+ if bbox is None and gdal_transform is None:
+ bbox = (100.0, 482.0, 114.0, 500.0)
write_random_geotiff(
path,
dtype,
bands=bands,
height=height,
width=width,
+ bbox=bbox,
gdal_transform=gdal_transform,
nodata=nodata,
plants=plants,
diff --git a/python/sedonadb/tests/test_raster_testing.py b/python/sedonadb/tests/test_raster_testing.py
new file mode 100644
index 000000000..f8e620dd7
--- /dev/null
+++ b/python/sedonadb/tests/test_raster_testing.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for the grid-placement helpers in `sedonadb.raster_testing`.
+
+The bbox spelling of grid placement is only checked here: the parity suite
+compares engines against each other on the same file (and anchors against
+bbox-constructed `DecodedRaster`s), so a wrong bbox-derived geotransform
+would agree with itself there.
+"""
+
+import numpy as np
+import pytest
+
+from sedonadb.raster_testing import (
+ DecodedRaster,
+ decode_geotiff,
+ write_geotiff,
+ write_random_geotiff,
+)
+
+# The default extent of `DBEngine.create_random_raster_view`.
+BBOX = (100.0, 482.0, 114.0, 500.0)
+
+
+def test_write_geotiff_bbox_places_the_grid(tmp_path):
+ path = tmp_path / "bbox.tif"
+ write_random_geotiff(path, "uint8", bands=1, height=6, width=7, bbox=BBOX)
+ assert decode_geotiff(path).gdal_transform == (100.0, 2.0, 0.0, 500.0, 0.0, -3.0)
+
+
+def test_decoded_raster_bbox_places_the_grid():
+ data = np.zeros((1, 6, 7), dtype="uint8")
+ by_bbox = DecodedRaster(data, nodata=[None], bbox=BBOX)
+ assert by_bbox.gdal_transform == (100.0, 2.0, 0.0, 500.0, 0.0, -3.0)
+
+
+def test_decoded_raster_requires_exactly_one_grid_placement():
+ data = np.zeros((1, 6, 7), dtype="uint8")
+ with pytest.raises(ValueError, match="exactly one"):
+ DecodedRaster(data, nodata=[None])
+ with pytest.raises(ValueError, match="exactly one"):
+ DecodedRaster(
+ data,
+ (100.0, 2.0, 0.0, 500.0, 0.0, -3.0),
+ nodata=[None],
+ bbox=BBOX,
+ )
+
+
+def test_decoded_raster_requires_nodata():
+ with pytest.raises(ValueError, match="nodata"):
+ DecodedRaster(np.zeros((1, 6, 7), dtype="uint8"), bbox=BBOX)
+
+
+def test_write_geotiff_requires_exactly_one_grid_placement(tmp_path):
+ data = np.zeros((1, 6, 7), dtype="uint8")
+ with pytest.raises(ValueError, match="exactly one"):
+ write_geotiff(tmp_path / "neither.tif", data)
+ with pytest.raises(ValueError, match="exactly one"):
+ write_geotiff(
+ tmp_path / "both.tif",
+ data,
+ bbox=BBOX,
+ gdal_transform=(100.0, 2.0, 0.0, 500.0, 0.0, -3.0),
+ )