blob: ce4eac9fb118794f498f1c9e3c1e5c7eb3efee81 [file]
# 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.
import math
import numpy as np
import pyspark
import pytest
from pyspark.sql.functions import col, expr, udf
from tests.test_base import TestBase
from sedona.spark.sql.types import RasterType
class TestFlexibleBands(TestBase):
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_reduce_bands_4_to_1(self):
"""Reduce from 4 bands to 1 band (NDVI-like calculation)."""
spark = self.spark
@udf(returnType=RasterType())
def ndvi_like(raster):
arr = raster.as_numpy().astype(np.float32)
nir = arr[3]
red = arr[2]
ndvi = (nir - red) / (nir + red + 1e-10)
return raster.with_bands(ndvi) # (H, W) → 1 band
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(4, 'F', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(ndvi_like(col("rast")).alias("rast2"))
.selectExpr(
"RS_NumBands(rast2) as num_bands",
"RS_BandAsArray(rast2, 1) as band",
)
.first()
)
assert result["num_bands"] == 1
band = result["band"]
assert len(band) == 4 * 3
for val in band:
assert math.isfinite(val), f"Expected finite value, got {val}"
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_increase_bands_3_to_6(self):
"""Increase from 3 bands to 6 (original + squared features)."""
spark = self.spark
@udf(returnType=RasterType())
def stack_features(raster):
arr = raster.as_numpy().astype(np.float64)
derived = arr**2
stacked = np.concatenate([arr, derived], axis=0)
return raster.with_bands(stacked)
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(3, 'D', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(stack_features(col("rast")).alias("rast2"))
.selectExpr(
"RS_NumBands(rast2) as num_bands",
"RS_BandAsArray(rast2, 1) as band1",
"RS_BandAsArray(rast2, 4) as band4",
)
.first()
)
assert result["num_bands"] == 6
# Band 1: original band 0 = 0 + y*4 + x
band1 = result["band1"]
for y in range(3):
for x in range(4):
expected = float(0 + y * 4 + x)
assert band1[y * 4 + x] == expected
# Band 4: derived = band 0 squared
band4 = result["band4"]
for y in range(3):
for x in range(4):
original = float(0 + y * 4 + x)
assert band4[y * 4 + x] == original**2
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_dtype_change_int_to_float(self):
"""Change dtype from int32 to float32 while keeping same band count."""
spark = self.spark
@udf(returnType=RasterType())
def normalize(raster):
arr = raster.as_numpy().astype(np.float32)
normalized = arr / (arr.max() + 1e-10)
return raster.with_bands(normalized)
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(1, 'I', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(normalize(col("rast")).alias("rast2"))
.selectExpr("RS_BandAsArray(rast2, 1) as band")
.first()
)
band = result["band"]
for val in band:
assert 0.0 <= val <= 1.0, f"Expected [0,1], got {val}"
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_mixed_band_and_dtype_change(self):
"""Simultaneous band count reduction and dtype change."""
spark = self.spark
@udf(returnType=RasterType())
def mean_bands(raster):
arr = raster.as_numpy().astype(np.float32)
mean = np.mean(arr, axis=0, keepdims=True)
return raster.with_bands(mean)
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(3, 'I', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(mean_bands(col("rast")).alias("rast2"))
.selectExpr(
"RS_NumBands(rast2) as num_bands",
"RS_BandAsArray(rast2, 1) as band",
)
.first()
)
assert result["num_bands"] == 1
band = result["band"]
# Mean of 3 bands: mean(b + y*4+x for b in 0,1,2) = 1 + y*4 + x
for y in range(3):
for x in range(4):
expected = 1.0 + y * 4 + x
assert abs(band[y * 4 + x] - expected) < 0.01
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_jvm_mapalgebra_after_band_change(self):
"""JVM-side RS_MapAlgebra works on a raster that had bands changed by UDF."""
spark = self.spark
@udf(returnType=RasterType())
def select_band1(raster):
arr = raster.as_numpy()
return raster.with_bands(arr[0:1].astype(np.float64))
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(3, 'D', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(select_band1(col("rast")).alias("rast2"))
.selectExpr("RS_MapAlgebra(rast2, 'D', 'out[0] = rast[0] + 100;') as rast3")
.selectExpr("RS_BandAsArray(rast3, 1) as band")
.first()
)
band = result["band"]
for y in range(3):
for x in range(4):
expected = float(y * 4 + x + 100)
assert band[y * 4 + x] == expected
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_reduce_bands_8_to_1_argmax(self):
"""8 bands to 1 band via argmax (KMeans-like cluster assignment)."""
spark = self.spark
@udf(returnType=RasterType())
def argmax_band(raster):
arr = raster.as_numpy().astype(np.float32)
result = np.argmax(arr, axis=0).astype(np.float32)
return raster.with_bands(result)
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(8, 'F', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(argmax_band(col("rast")).alias("rast2"))
.selectExpr(
"RS_NumBands(rast2) as num_bands",
"RS_BandAsArray(rast2, 1) as band",
)
.first()
)
assert result["num_bands"] == 1
band = result["band"]
# Band 7 has highest values (7 + y*4 + x), so argmax = 7.0
for val in band:
assert val == 7.0, f"Expected 7.0, got {val}"
@pytest.mark.skipif(
pyspark.__version__ < "3.4", reason="requires Spark 3.4 or higher"
)
def test_metadata_survives_band_change(self):
"""CRS, affine transform, and dimensions survive a band count change."""
spark = self.spark
@udf(returnType=RasterType())
def reduce_to_1(raster):
arr = raster.as_numpy().astype(np.float64)
return raster.with_bands(arr[0:1])
df = spark.range(1).withColumn(
"rast",
expr(
"RS_MakeRasterForTesting(3, 'D', 'BandedSampleModel', "
"4, 3, 100, 100, 10, -10, 0, 0, 3857)"
),
)
result = (
df.select(reduce_to_1(col("rast")).alias("rast2"))
.selectExpr(
"RS_NumBands(rast2) as num_bands",
"RS_Width(rast2) as width",
"RS_Height(rast2) as height",
"RS_ScaleX(rast2) as scale_x",
"RS_ScaleY(rast2) as scale_y",
"RS_SRID(rast2) as srid",
)
.first()
)
assert result["num_bands"] == 1
assert result["width"] == 4
assert result["height"] == 3
assert abs(result["scale_x"] - 10.0) < 0.001
assert abs(result["scale_y"] - (-10.0)) < 0.001
assert result["srid"] == 3857