@@ -2087,6 +2087,174 @@ def test_roundtrip_through_from_wkb(self):
20872087 assert original .equals (copy )
20882088
20892089
2090+ # ---------------------------------------------------------------------------
2091+ # Issue #2170 -- rasterize(like=...) y-axis orientation
2092+ #
2093+ # The rasterizer always burns with row 0 = ymax (top-down image
2094+ # convention). When the template's y axis is ascending, the burned
2095+ # array has to be flipped so result.sel(y=...) lines up with the
2096+ # geometry in world coordinates, and result.y still equals like.y.
2097+ # ---------------------------------------------------------------------------
2098+
2099+
2100+ def _like_2170 (y_ascending , width = 4 , height = 4 ):
2101+ """Build a 4x4 like-grid with the requested y orientation."""
2102+ x = np .linspace (0.5 , width - 0.5 , width )
2103+ if y_ascending :
2104+ y = np .linspace (0.5 , height - 0.5 , height )
2105+ else :
2106+ y = np .linspace (height - 0.5 , 0.5 , height )
2107+ return xr .DataArray (
2108+ np .zeros ((height , width ), dtype = np .float64 ),
2109+ dims = ['y' , 'x' ],
2110+ coords = {'y' : y , 'x' : x },
2111+ )
2112+
2113+
2114+ class TestLikeYOrientation2170 :
2115+ """Burning into an ascending-y like must agree with descending-y by
2116+ world coordinate, and output.y must round-trip like.y exactly."""
2117+
2118+ def test_numpy_ascending_matches_descending_by_world_y (self ):
2119+ # Box in lower-left corner of the world grid -- with descending y
2120+ # this lands in the bottom row, with ascending y it lands in the
2121+ # top row, but result.sel(y=0.5) must return 1.0 in both cases.
2122+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2123+ r_desc = rasterize (geom , like = _like_2170 (False ), fill = 0 )
2124+ r_asc = rasterize (geom , like = _like_2170 (True ), fill = 0 )
2125+
2126+ # like.y is preserved verbatim
2127+ np .testing .assert_array_equal (r_desc .y .values , [3.5 , 2.5 , 1.5 , 0.5 ])
2128+ np .testing .assert_array_equal (r_asc .y .values , [0.5 , 1.5 , 2.5 , 3.5 ])
2129+
2130+ # World-coord selection agrees
2131+ for yw in [0.5 , 1.5 , 2.5 , 3.5 ]:
2132+ for xw in [0.5 , 1.5 , 2.5 , 3.5 ]:
2133+ a = float (r_desc .sel (y = yw , x = xw ).item ())
2134+ b = float (r_asc .sel (y = yw , x = xw ).item ())
2135+ assert a == b , f"mismatch at world (y={ yw } , x={ xw } ): " \
2136+ f"desc={ a } , asc={ b } "
2137+
2138+ # The burned cell is at the lower-left corner of the world grid
2139+ assert float (r_desc .sel (y = 0.5 , x = 0.5 ).item ()) == 1.0
2140+ assert float (r_asc .sel (y = 0.5 , x = 0.5 ).item ()) == 1.0
2141+ # And nowhere near the top row
2142+ assert float (r_desc .sel (y = 3.5 , x = 0.5 ).item ()) == 0.0
2143+ assert float (r_asc .sel (y = 3.5 , x = 0.5 ).item ()) == 0.0
2144+
2145+ def test_numpy_output_array_matches_like_orientation (self ):
2146+ """Row 0 of the output must correspond to like.y[0] in world
2147+ coords, no matter which way y points."""
2148+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2149+ r_desc = rasterize (geom , like = _like_2170 (False ), fill = 0 )
2150+ r_asc = rasterize (geom , like = _like_2170 (True ), fill = 0 )
2151+ # Descending: row 0 is the top row (y=3.5), so all zeros there.
2152+ # Last row is y=0.5 with the burned 1.
2153+ assert r_desc .values [- 1 , 0 ] == 1.0
2154+ assert r_desc .values [0 , 0 ] == 0.0
2155+ # Ascending: row 0 is the bottom row (y=0.5), so the burned 1
2156+ # has to be there.
2157+ assert r_asc .values [0 , 0 ] == 1.0
2158+ assert r_asc .values [- 1 , 0 ] == 0.0
2159+
2160+ def test_numpy_round_trip_with_xr_align (self ):
2161+ """output.y must equal like.y exactly so xr.align still works."""
2162+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2163+ for ascending in (True , False ):
2164+ like = _like_2170 (ascending )
2165+ result = rasterize (geom , like = like , fill = 0 )
2166+ np .testing .assert_array_equal (result .y .values , like .y .values )
2167+ np .testing .assert_array_equal (result .x .values , like .x .values )
2168+ # xr.align is the actual downstream operation this protects
2169+ aligned_result , aligned_like = xr .align (result , like )
2170+ assert aligned_result .sizes == result .sizes
2171+
2172+ def test_numpy_points_respect_orientation (self ):
2173+ """Same check with a point geometry rather than a polygon."""
2174+ from shapely .geometry import Point
2175+ geom = [(Point (0.5 , 0.5 ), 7.0 )]
2176+ r_desc = rasterize (geom , like = _like_2170 (False ), fill = 0 )
2177+ r_asc = rasterize (geom , like = _like_2170 (True ), fill = 0 )
2178+ assert float (r_desc .sel (y = 0.5 , x = 0.5 ).item ()) == 7.0
2179+ assert float (r_asc .sel (y = 0.5 , x = 0.5 ).item ()) == 7.0
2180+
2181+ def test_numpy_lines_respect_orientation (self ):
2182+ """Same check with a line geometry along the bottom edge."""
2183+ from shapely .geometry import LineString
2184+ geom = [(LineString ([(0.5 , 0.5 ), (3.5 , 0.5 )]), 5.0 )]
2185+ r_desc = rasterize (geom , like = _like_2170 (False ), fill = 0 )
2186+ r_asc = rasterize (geom , like = _like_2170 (True ), fill = 0 )
2187+ for xw in [0.5 , 1.5 , 2.5 , 3.5 ]:
2188+ assert float (r_desc .sel (y = 0.5 , x = xw ).item ()) == 5.0
2189+ assert float (r_asc .sel (y = 0.5 , x = xw ).item ()) == 5.0
2190+
2191+ @skip_no_dask
2192+ def test_dask_numpy_ascending_matches_descending (self ):
2193+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2194+ r_desc = rasterize (
2195+ geom , like = _like_2170 (False ), fill = 0 , chunks = 2 ).compute ()
2196+ r_asc = rasterize (
2197+ geom , like = _like_2170 (True ), fill = 0 , chunks = 2 ).compute ()
2198+ np .testing .assert_array_equal (r_desc .y .values , [3.5 , 2.5 , 1.5 , 0.5 ])
2199+ np .testing .assert_array_equal (r_asc .y .values , [0.5 , 1.5 , 2.5 , 3.5 ])
2200+ for yw in [0.5 , 1.5 , 2.5 , 3.5 ]:
2201+ a = float (r_desc .sel (y = yw , x = 0.5 ).item ())
2202+ b = float (r_asc .sel (y = yw , x = 0.5 ).item ())
2203+ assert a == b
2204+ assert float (r_desc .sel (y = 0.5 , x = 0.5 ).item ()) == 1.0
2205+ assert float (r_asc .sel (y = 0.5 , x = 0.5 ).item ()) == 1.0
2206+
2207+ @skip_no_cuda
2208+ def test_cupy_ascending_matches_descending (self ):
2209+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2210+ r_desc = rasterize (geom , like = _like_2170 (False ), fill = 0 , use_cuda = True )
2211+ r_asc = rasterize (geom , like = _like_2170 (True ), fill = 0 , use_cuda = True )
2212+ # CuPy DataArrays expose .data.get() per project notes
2213+ desc_vals = r_desc .data .get () if hasattr (r_desc .data , 'get' ) \
2214+ else r_desc .values
2215+ asc_vals = r_asc .data .get () if hasattr (r_asc .data , 'get' ) \
2216+ else r_asc .values
2217+ np .testing .assert_array_equal (r_desc .y .values , [3.5 , 2.5 , 1.5 , 0.5 ])
2218+ np .testing .assert_array_equal (r_asc .y .values , [0.5 , 1.5 , 2.5 , 3.5 ])
2219+ # descending: burned row is last; ascending: burned row is first
2220+ assert desc_vals [- 1 , 0 ] == 1.0
2221+ assert asc_vals [0 , 0 ] == 1.0
2222+
2223+ @skip_no_cuda
2224+ @skip_no_dask
2225+ def test_dask_cupy_ascending_matches_descending (self ):
2226+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2227+ r_desc = rasterize (
2228+ geom , like = _like_2170 (False ), fill = 0 ,
2229+ use_cuda = True , chunks = 2 ).compute ()
2230+ r_asc = rasterize (
2231+ geom , like = _like_2170 (True ), fill = 0 ,
2232+ use_cuda = True , chunks = 2 ).compute ()
2233+ desc_vals = r_desc .data .get () if hasattr (r_desc .data , 'get' ) \
2234+ else r_desc .values
2235+ asc_vals = r_asc .data .get () if hasattr (r_asc .data , 'get' ) \
2236+ else r_asc .values
2237+ assert desc_vals [- 1 , 0 ] == 1.0
2238+ assert asc_vals [0 , 0 ] == 1.0
2239+
2240+ def test_numpy_explicit_bounds_skips_flip (self ):
2241+ """When bounds are passed explicitly, the orientation flip path
2242+ is bypassed (caller has full control of the output grid)."""
2243+ like = _like_2170 (True )
2244+ geom = [(box (0 , 0 , 1 , 1 ), 1.0 )]
2245+ result = rasterize (geom , like = like , bounds = (0 , 0 , 4 , 4 ), fill = 0 )
2246+ # With explicit bounds, output coords are rebuilt descending,
2247+ # which is the documented behaviour for any resized output.
2248+ # Lock the exact coord centres so an off-by-one in the rebuild
2249+ # path would surface here instead of silently passing.
2250+ np .testing .assert_array_equal (
2251+ result .y .values , np .array ([3.5 , 2.5 , 1.5 , 0.5 ]))
2252+ np .testing .assert_array_equal (
2253+ result .x .values , np .array ([0.5 , 1.5 , 2.5 , 3.5 ]))
2254+ # And world-coord selection still works correctly.
2255+ assert float (result .sel (y = 0.5 , x = 0.5 , method = 'nearest' ).item ()) == 1.0
2256+
2257+
20902258# ---------------------------------------------------------------------------
20912259# Issue #2168: reject non-uniformly spaced `like` grids
20922260# ---------------------------------------------------------------------------
0 commit comments