diff --git a/.claude/sweep-test-coverage-state.csv b/.claude/sweep-test-coverage-state.csv index 04f90cd09..9bc639431 100644 --- a/.claude/sweep-test-coverage-state.csv +++ b/.claude/sweep-test-coverage-state.csv @@ -12,7 +12,7 @@ mcda,2026-06-10,3149,HIGH,1;2;5,"Pass 1 (2026-06-10, deep-sweep test-coverage): polygon_clip,2026-06-10,3197,MEDIUM,1;2;3;5,"deep-sweep test-coverage 2026-06-10 on a CUDA host. Existing file covered numpy well + one parity test per dask/cupy/dask+cupy backend. Filed #3197 (test-only) and added 13 tests: Cat1 GPU param/NaN coverage (cupy + dask+cupy each get custom nodata, all_touched=True, and NaN-input preservation vs numpy; previously only crop=False inner polygon ran on GPU); Cat2 Inf/-Inf preserved, all-NaN input -> all-NaN, int32 + sentinel nodata=-1; Cat3 Nx1 + 1xN strip rasters; Cat5 coords preserved (crop=False) + crop coords are a contiguous subset of input coords (crop=True). All 13 RAN+PASSED on GPU (6 GPU tests not skipped); full file 36 passed 0 skipped. LOW (documented, NOT fixed): rasterize_kw forwarding never tested; non-square cellsize never tested. SOURCE NOTE (out of scope, not filed): clip_polygon docstring says 'named y and x dims' but rasterize() hard-requires literal y/x, so lat/lon-dim rasters raise -- dim-name preservation (Cat5) is therefore unsupported by contract, not a test gap. SOURCE NOTE 2: polygon_clip.py:216 still passes rasterize(use_cuda=True) on dask+cupy (renamed to gpu= in #3089); harmless deprecation alias today, candidate for an api-consistency follow-up." polygonize,2026-06-12,3299,MEDIUM,1,"Pass 4 (2026-06-12): added test_polygonize_mask_chunk_mismatch_3299.py (25 tests, all passing on a CUDA host incl. dask+cupy). Closes Cat 1 MEDIUM: the _polygonize_dask mask-rechunk branch (mask_data.chunks != dask_data.chunks -> rechunk) was never exercised; every prior dask masked test used mask chunks identical to the raster's. Mismatched layouts pinned against same-backend aligned-mask reference: (6,7) same-grid-shape misalignment (the silent-corruption layout for int rasters), single-chunk (15,18), more-blocks (4,5); int+float rasters, connectivity 4/8, dask+numpy and dask+cupy; plus exact-geometry single-masked-pixel hole anchor. Mutation (delete the rechunk guard) flips all 25 red; clean md5 restore. Full polygonize suite 486 passed / 16 skipped. Test-only; source untouched. Issue #3299. Audit re-confirmed Cat 2/3/4 closed by passes 1-3 and post-2026-05-29 changes (#2913 float-mask fix flipped prior xfails, #3041 has issue-2677 test file, #2673/#2817 covered by batch-invariance and heap tests); Cat 5 N/A (no DataArray output; CRS/transform propagation already tested). | Pass 3 (2026-05-29): added test_polygonize_mask_dtype_coverage_2026_05_29.py (41 passed, 8 xfailed on a CUDA host). Closes Cat 4 MEDIUM parameter-coverage gap: mask= is documented to accept bool/integer/float values but every prior test passed only a bool mask. Integer masks (int32/int64) now pinned against the same-backend bool-mask output on all four backends x both raster dtypes x connectivity 4/8; float-mask-on-integer-raster also pinned. Each backend is compared to its OWN bool reference to isolate mask-dtype from the unrelated numpy-vs-dask hole-vs-single-ring representation difference. Mutation (drop the not-mask[ij] exclusion in _calculate_regions) flips 11 tests red incl. the pixel-exclusion sanity anchor; clean md5 restore. Surfaced source bug #2623: a float-dtype mask on a float-dtype raster raises TypeError at polygonize.py:918 (mask & nan_mask; bitwise_and undefined for float&bool; cupy/dask route floats through _polygonize_numpy so they crash too; int masks coerce fine). 8 float-mask cases marked xfail(strict, raises=TypeError) referencing #2623. Test-only; source untouched. | Pass 2 (2026-05-27): added test_polygonize_atol_rtol_backend_coverage_2026_05_27.py with 15 tests, all passing on a CUDA host. Closes Cat 4 MEDIUM parameter-coverage gap on atol/rtol forwarding through the cupy and dask+cupy backends. atol/rtol were exposed by #2173 / #2194 and thread through _polygonize_cupy (polygonize.py:808) and _polygonize_dask (polygonize.py:1719); the dask path further plumbs them into dask.delayed(_polygonize_chunk)(...) at lines 1748-1754 and into _bucket_key_for_value for cross-chunk merge bucketing at lines 1757-1758. Pre-existing tests covered non-default atol/rtol only on numpy and dask+numpy. The cupy and dask+cupy dispatchers were untested -- a regression dropping the kwargs there would silently change the float polygon count and would not be caught. Same dispatcher-silently-drops-kwarg pattern fixed by #1561 / #1605 / #1685 / #1810 / #1974 on adjacent GeoTIFF surfaces. 15 tests: cupy strict-equality + default-tolerance pin on _REPRO_2173, dask+cupy strict-equality single-chunk + multi-chunk (engages cross-chunk merge bucket) + default-tolerance multi-chunk pin, cupy intermediate-atol small/large pair, dask+cupy intermediate-atol single/multi-chunk small + single-chunk large, cupy integer atol-ignored matrix, dask+cupy integer atol-ignored single-chunk + multi-chunk, cupy rtol-only large/small matrix. Mutation against _polygonize_cupy float branch (drop atol/rtol kwargs in the _polygonize_numpy forward call at polygonize.py:823-825) flips 3 of 5 cupy tests red; mutation against dask.delayed(_polygonize_chunk)(...) at polygonize.py:1748-1754 (drop atol, rtol args) flips 2 of 6 dask+cupy tests red. Confirmed clean restore via md5sum. Source untouched. Filed issue #2537 (test-only). Cat 4 MEDIUM (parameter coverage on cupy + dask+cupy atol/rtol forwarding). Pass 1 (2026-05-19): added test_polygonize_coverage_2026_05_19.py with 58 tests, all passing on a CUDA host. Closes Cat 3 HIGH 1x1 / Nx1 single-column geometric gaps (Nx1 exercises the nx==1 padding path at polygonize.py:565 and the cupy nx==1 numpy-fallback at polygonize.py:671), Cat 3 MEDIUM 1xN single-row and all-equal-value rasters on all four backends. Closes Cat 2 HIGH NaN parity for cupy + dask+cupy (numpy/dask were already covered by test_polygonize_nan_pixels_excluded*), Cat 2 MEDIUM all-NaN raster on all four backends, Cat 2 HIGH +/-Inf pins on all four backends. Filed source-bug issue #2155: numpy/dask/dask+cupy backends silently absorb Inf cells into adjacent finite polygons because _is_close reduces abs(inf-inf) to nan; cupy backend handles Inf correctly. Pins lock the asymmetric behaviour so the fix is visible. Closes Cat 1 MEDIUM simplify_tolerance + mask= parity gaps on dask+cupy backend (numpy/cupy/dask were already covered). Closes Cat 4 MEDIUM column_name non-default value across geopandas/spatialpandas/geojson return types and Cat 4 MEDIUM validation error paths (bad connectivity, bad transform length, mask shape mismatch, mask underlying-type mismatch). Cat 5 N/A: polygonize returns lists/dataframes, not a DataArray with attrs to propagate." proximity,2026-06-09,2692;3139,MEDIUM,1;2;3,"Pass 3 (2026-06-09, deep-sweep test-coverage): module grew since Pass 2 (#2807 metric validation, #2812 GREAT_CIRCLE brute force, #2850/#2851 input validation, #2854/#2908 halo fixes, tie-break routing) and each landed with its own tests; Pass 2's stale LOW (invalid distance_metric fallback) is FIXED and tested (#2807). Found 3 MEDIUM gaps, filed #3139, added 40 tests (all RUN and PASS on a CUDA host; full file 450 passed): (1) Cat 2 integer-dtype raster untested on any backend -- bounded dask pads int arrays with boundary=np.nan which casts to INT_MIN phantom targets, only neutralized because the coordinate-grid pads are real NaNs; pinned int32 x 3 funcs x 4 backends x bounded/unbounded vs float64 numpy baseline + explicit target_values; (2) Cat 1 bounded dask+cupy (_process_dask_cupy) only ever ran EUCLIDEAN; pinned MANHATTAN+GREAT_CIRCLE x 3 funcs with a routing spy; mutation (pad=0) flips all 6 red, clean md5 restore; (3) Cat 3 empty 0-row/0-col raster unpinned; fails fast with IndexError, pinned raises. All behaviors verified correct before tests were added -- no source bug, source untouched. LOW (documented, not fixed): -inf pixel input never tested (+inf is; isfinite is symmetric). || Pass 2 (2026-06-02): added 18 tests to test_proximity.py closing the two MEDIUM gaps Pass 1 left open, all RUN and passing on a CUDA host across numpy/cupy/dask+numpy/dask+cupy (15 cross-backend + 3 error-path). Source untouched. Cat 4 MEDIUM (error path): _process raises ValueError when raster.dims != (y, x) (proximity.py:1043) but no test exercised the swapped x/y guard; test_wrong_dim_order_raises pins it for proximity/allocation/direction. Cat 2 MEDIUM (all-NaN input): Pass 1 noted all-NaN/all-zero on eager numpy+cupy was unpinned; test_all_nan_raster_all_nan_output pins an all-NaN 6x6 raster -> all-NaN float32 output on all four backends x three functions. Remaining LOW (documented): invalid distance_metric string silently falls back to EUCLIDEAN (proximity.py:1049-1051). || PREVIOUS: Pass 1 (2026-05-29): added 65 tests to test_proximity.py closing three coverage gaps, all RUN and passing on a CUDA host (numpy/cupy/dask+numpy/dask+cupy). Issue #2692, PR opened. Source untouched. Cat 3 HIGH: degenerate raster shapes (1x1 single pixel, Nx1 column strip, 1xN row strip) had zero coverage for proximity/allocation/direction on any backend; they stress the line-sweep kernel boundaries (_process_proximity_line) and the GPU brute-force kernel grid sizing (_proximity_cuda_kernel via cuda_args). Pinned all three shapes x three functions x four backends against hand-checked expected values; mutation of a pinned direction expectation confirms teeth. Cat 1/4 HIGH: allocation and direction only ran EUCLIDEAN across backends; MANHATTAN and GREAT_CIRCLE were cross-backend-tested for proximity only. Pinned both metrics x two functions x four backends against the numpy baseline (all match). Cat 5 MEDIUM: no test set non-empty res/crs attrs so the attrs-preservation assertion in general_output_checks compared two empty dicts. proximity reads attrs['res'] via get_dataarray_resolution for bounded-dask chunk padding, so added attrs round-trip tests on four backends plus a bounded-dask test where a res attr matching the coordinate spacing must equal the numpy baseline. A res attr that lies about the spacing mis-sizes the map_overlap depth; source fragility, not a test gap, left for a separate accuracy issue. Cat 2 (NaN/Inf input) already covered by the shared test_raster fixture (embeds np.inf and np.nan, runs on four backends). Remaining LOW: all-NaN / all-zero input on eager numpy+cupy not directly pinned." -rasterize,2026-06-12,2614;3102;3105;3296,MEDIUM,1,"Pass 6 (2026-06-12, deep-sweep test-coverage): added test_rasterize_mixed_type_ordered_merge_3296.py (17 passed on a CUDA host after review follow-up added dask+numpy all_touched ordered-merge parity; full rasterize suite 757 passed + 1 xfail pre-follow-up). One Cat 1 MEDIUM gap: merge='first'/'last' cross-geometry-type input-order semantics (#2064) were pinned on numpy + dask+numpy only; the GPU implements ordering via a separate two-pass atomic scheme (pass 1 cuda.atomic.min/max on order across scanline/line/point kernels, pass 2 stamps the winner) and no cupy/dask+cupy test ever had two geometry types competing for one pixel under an ordered merge (gpu_race_2167 scenarios are single-type; GC parity tests are non-overlapping single-value). Probed first: behavior matches numpy, pure coverage gap. New tests: known-winner pins on all 4 backends, cupy + dask+cupy(chunks=2, 4 tiles) parity vs numpy for first/last, all_touched=True variant (supercover pass-2 kernel with cross-type competition), fixture non-degeneracy guard. Mutation (atomic.min->max in _apply_merge_gpu 'first' branch) flips 5 tests red; md5-clean restore. Issue #3296. LOW (documented, not fixed): _check_uniform_axis non-finite expected_step early-return untested; gpu=True-without-cupy ImportError guard only reachable via monkeypatch; _check_gpu_edge_cap tested at unit level only, no end-to-end >2048-edge row through rasterize(gpu=True). | PREVIOUS: Pass 5 (2026-06-09, deep-sweep test-coverage): added test_rasterize_coverage_2026_06_09.py (11 passed + 1 strict xfail on a CUDA host; full rasterize suite 652 passed). Two Cat 4 MEDIUM gaps. (1) all_touched x LineString interaction had zero coverage on any backend; probed and found all_touched=True is a NO-OP for lines (only polygon boundaries get the supercover burn in _run_numpy step 1b; lines always Bresenham via _burn_lines_cpu), so output matches rasterio's DEFAULT mode not its all_touched mode despite the docstring's pixel-for-pixel parity claim -- source bug filed #3102 (not fixed here, test-only sweep). Pinned the no-op on numpy/cupy/dask+numpy/dask+cupy (cupy+dask_cupy RAN on CUDA host), pinned equality with rasterio default mode, and added a strict xfail for rasterio all_touched parity that flips when #3102 lands. (2) _parse_input non-iterable geometries TypeError ('geometries must be a GeoDataFrame or iterable') had no test; pinned over int/float/None/object. Issue #3105. LOW (documented, not fixed): _like_crs rio.crs fallback (path 4) never exercised (attrs crs/crs_wkt/spatial_ref paths are covered in test_rasterize_crs_mismatch_3058.py); NaN-coordinate Point with explicit bounds silently dropped (probed, works, untested); non-numeric fill attrs try/except at rasterize.py:3729 untested. | PREVIOUS: Pass 4 (2026-05-29): added test_rasterize_coverage_2026_05_29.py with 11 tests, all passing (pure-Python validation paths, no CUDA needed); filed issue #2614 and opened a test-only PR. Closes Cat 4 MEDIUM error-path gaps that all three prior passes left untouched. (1) Partial width/height: the (width is None) != (height is None) guard in rasterize() raises ValueError naming the given and missing dimension, documented in the docstring, but neither the width-only nor height-only branch had a test; pin both directions plus the width-only+resolution case proving the guard fires before the resolution branch. (2) resolution= input type/shape validation: the type/shape branches (non-number/non-sequence string|dict; wrong-ndim numpy array; wrong-length sequence len 1|3|4; non-numeric elements) had no coverage -- test_rasterize.py's test_invalid_resolution_scalar/tuple only exercise non-finite/non-positive VALUES, not these type/shape guards, so a regression loosening or reordering them would ship silently; pin each branch to its message plus a positive control that a 1-D length-2 numpy array is still accepted. Source untouched." +rasterize,2026-06-18,2614;3102;3105;3296;3383,HIGH,4,"Pass 7 (2026-06-18, deep-sweep test-coverage): #3383 found a Cat 4 (error-path) gap -- rasterize() input-validation guards had no tests. Added 4 test classes (18 tests, all RAN+PASSED on a CUDA host; full module 233 passed/2 skipped): TestFillRepresentableGuard (NaN/out-of-range int + non-False bool fill rejected #2504/#3054, + valid int/bool/float-NaN negatives), TestBurnValueSafeIntegerGuard (|burn|>2**53-1 into int rejected #3056, + boundary 2**53-1 and float-dtype exemption), TestNonFiniteBurnValueGuard (NaN/inf burn into int/bool rejected #3085, + merge='count' exemption and float-dtype negatives), TestNonFiniteGeometryCoordsDropped (NaN/inf geometry coords dropped with UserWarning #3295, list+gdf paths). All guards run pre-dispatch on the CPU path so no GPU needed. Test-only PR, no source change." reproject,2026-06-09,2618;3050;3100;3101;3141,MEDIUM,1,"CI follow-up same day: first CI run of the threaded streaming branch hard-crashed macos-arm64 py3.14 (SIGABRT in numba call_cfunc, two ThreadPoolExecutor threads concurrently inside try_numba_transform/tmerc_inverse) -- the projection kernels are @njit(parallel=True) and numba's workqueue threading layer aborts on concurrent entry; filed source bug #3141. Test fix: threaded parity test now uses transform_precision=0 (per-thread pyproj Transformer, no numba), the NaN multi-tile test and 3-D xfail forced serial (max_memory=1) so the numba fast path stays covered without concurrent entry. windows-3.14 failure was fail-fast collateral (its suite fully passed). Pass 2026-06-09 (deep-sweep test-coverage): delta re-sweep one day after the 2026-06-08 pass; module modified today by #3077 (datum-probe warning silencing) and #3081 (merge output-size guard backend-aware) -- both landed WITH their own tests (TestDatumProbeNoProjWarning; TestSecurityGuards merge-guard trio incl. the monkeypatched in-memory raise), so the delta added no gap; the guard branching is is_dask-only, so cupy eager shares the tested numpy branch (no per-backend guard test needed). Found one MEDIUM Cat 1 gap every prior pass missed: the 5th dispatch branch of reproject() -- the streaming fallback (_reproject_streaming / _process_tile_batch / _parse_max_memory, taken when source >512MB and dask is not importable) -- had zero coverage anywhere; _parse_max_memory only runs on that branch so the existing max_memory kwarg tests never reached it. Filed #3101, added test_reproject_streaming_3101.py (15 tests: parity vs in-memory numpy for threaded / serial(max_memory=1) / single-tile / nearest+NaN, plus 10 _parse_max_memory unit cases). Probe surfaced source bug #3100: streaming assembly allocates a 2-D output buffer but 3-D sources yield (h,w,b) tiles -> ValueError broadcast in both assembly loops; pinned with strict xfail, source fix left to #3100 (test-only PR, source untouched). CPU-only path so no GPU tests needed (CUDA host; file ran 14 passed + 1 xfailed). LOW carried (documented, not fixed): reproject(name=) / merge(name=) override values untested (only merge name fallback covered); non-square-cellsize successful anisotropic run; dask.bag distributed branch of _reproject_streaming still unexercised (needs a live distributed client). || PREVIOUS: Pass 2026-06-08 (deep-sweep test-coverage): #3050 closes the one live gap found this pass. reproject()'s dask+cupy backend was parity-tested only with resampling='cubic' (TestCupyPyprojFallbackParity::test_projected_to_projected_dask_cupy_match); nearest/bilinear were covered on numpy (end-to-end) and eager cupy (parametrized test_projected_to_projected_numpy_cupy_match) but never on the dask+cupy chunk-assembly path. Parametrized that test over ['nearest','bilinear','cubic']; all 3 RUN+PASS on a CUDA host. Cat 4 MEDIUM (resampling-mode parameter coverage on the dask+cupy backend). Test-only, source untouched. Re-confirmed _merge.merge() has NO genuine cupy/dask+cupy backend (_merge_inmemory/_merge_dask use _merge_arrays_numpy + raster.values; _merge_arrays_cupy is imported but never dispatched = dead code, not a test gap) matching the prior pass's observation. reproject() otherwise saturated across all 4 backends, NaN/Inf/all-NaN, degenerate shapes, metadata, vertical, bounds_policy, integer nodata. LOW (documented, not filed): dask+cupy resampling-mode parity is the only per-mode-per-backend cell that had been missing. || PREVIOUS: Pass 2026-05-29: reproject already has a deep suite (369 tests in test_reproject.py + coverage/gate files) covering all 4 backends, NaN/Inf/all-NaN/all-Inf, 1x1/2x2, metadata, vertical shift, bounds_policy x backends, integer nodata x backends. Gaps found: Cat 3 HIGH single-row (1xN) and single-col (Nx1) strip rasters never tested (hit size<2 branch of _validate_regular_axis + degenerate resampling axis); Cat 3 MEDIUM constant-value/zero-gradient raster never reprojected. Added TestDegenerateShapeReproject (12 tests): 1xN+Nx1 strips x numpy/dask/cupy/dask+cupy, constant raster numpy value-preservation + cross-backend parity. All 12 executed and passed on a CUDA host. Test-only, no source change (#2618). LOW (documented only): _merge._merge_arrays_cupy imported but never called by merge() (host-bounces via _merge_arrays_numpy) - dead-code source observation not a test gap; non-square cellsize reproject only covered via resolution-tuple validation errors not a successful anisotropic run." resample,2026-05-29,2547;2615,HIGH,1;2;3;5,"Pass 2 (2026-05-29): added test_resample_cupy_agg_fallback_2615.py (6 tests, all passing on CUDA host). Closes Cat 1 MEDIUM backend-coverage gap: the cupy eager aggregate CPU fallback for average/min/max at a NON-integer downsample factor (_run_cupy fy==int(fy) branch in resample.py ~L957-973) was never exercised; existing TestCuPyParity used 12x12 scale 0.5 (integer factor 2 -> GPU reshape path) and only median/mode hit the host fallback. New tests use 10x10 scale 0.3 (factor 3.33) for average/min/max parity vs numpy plus a NaN-masked variant. Issue #2615. Module is otherwise very thoroughly covered (test_resample.py + 3 supplementary files); no remaining HIGH gaps found. Pass 1 (2026-05-27): added test_resample_coverage_2026_05_27.py with 70 tests (68 passing, 2 skipped). Closes Cat 3 HIGH Nx1 single-column gap across numpy/cupy/dask+numpy/dask+cupy x 8 methods (nearest/bilinear/cubic/average/min/max/median/mode) plus Nx1 upsample-nearest parity and Nx1 cross-backend aggregate parity. Closes Cat 2 MEDIUM NaN-parity gap on cupy and dask+cupy (existing TestCuPyParity/TestDaskCuPyParity used random data without NaN; the weight-mask gate and spline-prepad had no GPU NaN coverage). Closes Cat 3 MEDIUM all-equal-value raster across 8 methods (downsample) and 3 interp methods (upsample) plus a constant-with-NaN aggregate variant. Closes Cat 5 MEDIUM non-default dim-name propagation: lat/lon, latitude/longitude, and (channel, lat, lon) 3D round-trip without being renamed to y/x; per-dim attrs (units) preserved. Closes Cat 3 MEDIUM empty-raster behaviour pin: 0-row and 0-col rasters raise (currently IndexError) -- contract covered. Filed source-bug issue #2547: cubic on dask backends fails for Nx1 / arrays smaller than depth=16; the 2 skipped tests in this file gate on that fix landing. Source untouched." slope,2026-05-29,2697,MEDIUM,3,"PR #2703: added degenerate-shape tests (1x1/1xN/Nx1) for all 4 planar backends + geodesic; no live bug, pins all-NaN+shape contract. CUDA host: cupy/dask+cupy ran. Backend/NaN/param/metadata coverage already complete." diff --git a/xrspatial/tests/test_rasterize.py b/xrspatial/tests/test_rasterize.py index 54d8b0b45..baa033b77 100644 --- a/xrspatial/tests/test_rasterize.py +++ b/xrspatial/tests/test_rasterize.py @@ -2529,3 +2529,168 @@ def test_single_row_or_column_like_passes(self): [(box(0, 0, 1, 10), 1.0)], like=like_2168, fill=0, ) + + +# --------------------------------------------------------------------------- +# Validation guards: fill / burn-value / non-finite geometry coords (#3383) +# +# These guards all run on the CPU path before backend dispatch, so the tests +# need no GPU. They assert the documented raise/warn behavior and pair each +# guard with a valid-input (or merge='count' exemption) negative case so the +# guards stay narrow rather than rejecting legitimate input. +# --------------------------------------------------------------------------- + +class TestFillRepresentableGuard: + """fill must be representable in an integer/bool output dtype (#2504/#3054). + + A fill the dtype cannot hold exactly would land on a different value after + the final ``astype`` while the emitted nodata/_FillValue attrs still store + the original fill, so the array and its mask metadata disagree. + """ + + def test_nan_fill_into_integer_dtype_raises(self): + with pytest.raises(ValueError, match="cannot be represented"): + rasterize([(box(0, 0, 5, 5), 1.0)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=np.nan, dtype='int32') + + def test_out_of_range_integer_fill_raises(self): + # -9999 wraps to 241 in uint8. + with pytest.raises(ValueError, match="cannot be represented"): + rasterize([(box(0, 0, 5, 5), 1.0)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=-9999, dtype='uint8') + + def test_non_false_fill_into_bool_raises(self): + # Any non-False fill collapses to True under astype(bool). + with pytest.raises(ValueError, match="cannot be represented"): + rasterize([(box(0, 0, 5, 5), 1.0)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=5, dtype=bool) + + def test_valid_integer_fill_accepted(self): + result = rasterize([(box(0, 0, 5, 5), 1.0)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=-9999, dtype='int32') + assert result.dtype == np.int32 + + def test_valid_bool_fill_accepted(self): + result = rasterize([(box(0, 0, 5, 5), True)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=False, dtype=bool) + assert result.dtype == np.bool_ + + def test_nan_fill_into_float_dtype_accepted(self): + # Float dtypes can hold NaN; the guard must not reject them. The + # small box leaves the grid corners unburned so they keep the fill. + result = rasterize([(box(2, 2, 3, 3), 1.0)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=np.nan, dtype='float32') + assert np.isnan(result.values[0, 0]) + + +class TestBurnValueSafeIntegerGuard: + """Integer burn values above the float64 safe-integer range (#3056). + + rasterize computes in float64, so a value with |value| > 2**53 - 1 cannot + be cast back to an exact integer; it is rejected rather than silently + rounded. + """ + + def test_burn_value_above_safe_integer_range_raises(self): + with pytest.raises(ValueError, match="exceeds the range"): + rasterize([(box(0, 0, 5, 5), float(2 ** 53 + 1))], + width=5, height=5, bounds=(0, 0, 5, 5), + fill=0, dtype='int64') + + def test_burn_value_at_safe_integer_boundary_accepted(self): + # 2**53 - 1 is exactly representable in float64. + safe = float(2 ** 53 - 1) + result = rasterize([(box(0, 0, 5, 5), safe)], + width=5, height=5, bounds=(0, 0, 5, 5), + fill=0, dtype='int64') + assert result.values[2, 2] == np.int64(2 ** 53 - 1) + + def test_large_burn_value_accepted_for_float_dtype(self): + # Float output is exempt: the value is what the user asked to store. + big = float(2 ** 53 + 1) + result = rasterize([(box(0, 0, 5, 5), big)], + width=5, height=5, bounds=(0, 0, 5, 5), + fill=np.nan, dtype='float64') + assert result.values[2, 2] == big + + +class TestNonFiniteBurnValueGuard: + """Non-finite burn values into integer/bool dtypes (#3085). + + A NaN/inf burn value against an integer dtype lands on a platform sentinel + after the final cast (NaN -> -2147483648 for int32) and against bool + collapses to True. ``merge='count'`` never reads the property value, so it + is exempt. + """ + + @pytest.mark.parametrize("bad", [np.nan, np.inf, -np.inf]) + def test_nonfinite_burn_into_integer_raises(self, bad): + with pytest.raises(ValueError, match="is not finite"): + rasterize([(box(0, 0, 5, 5), bad)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=0, dtype='int32') + + def test_nan_burn_into_bool_raises(self): + with pytest.raises(ValueError, match="is not finite"): + rasterize([(box(0, 0, 5, 5), np.nan)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=False, dtype=bool) + + def test_nonfinite_burn_with_count_merge_accepted(self): + # count burns the overlap count, never the NaN property, so the + # non-finite attribute is allowed through. + result = rasterize([(box(0, 0, 5, 5), np.nan)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=0, dtype='int32', + merge='count') + assert result.dtype == np.int32 + assert result.values[2, 2] == 1 + + def test_nonfinite_burn_into_float_dtype_accepted(self): + # NaN/inf are representable in float; no guard should fire. + result = rasterize([(box(0, 0, 5, 5), np.nan)], width=5, height=5, + bounds=(0, 0, 5, 5), fill=0.0, dtype='float64') + assert np.isnan(result.values[2, 2]) + + +class TestNonFiniteGeometryCoordsDropped: + """Geometries with non-finite coordinates are dropped with a warning. + + A NaN/inf coordinate has no defined location on the raster grid (#3295). + The bad geometry is dropped and a UserWarning is emitted; the remaining + geometries still burn. + """ + + def test_nonfinite_point_dropped_with_warning(self): + from shapely.geometry import Point + pairs = [(box(0, 0, 5, 5), 1.0), (Point(float('nan'), 2.0), 9.0)] + with pytest.warns(UserWarning, match="non-finite coordinates"): + result = rasterize(pairs, width=5, height=5, bounds=(0, 0, 5, 5), + fill=0) + # The good polygon still burned; the NaN point did not. + assert result.values[2, 2] == 1.0 + assert not np.any(result.values == 9.0) + + def test_inf_polygon_dropped_with_warning(self): + bad_poly = Polygon([(0, 0), (np.inf, 0), (np.inf, 5), (0, 5)]) + good_poly = box(0, 0, 5, 5) + with pytest.warns(UserWarning, match="non-finite coordinates"): + result = rasterize([(good_poly, 1.0), (bad_poly, 9.0)], + width=5, height=5, bounds=(0, 0, 5, 5), fill=0) + assert result.values[2, 2] == 1.0 + assert not np.any(result.values == 9.0) + + @skip_no_geopandas + @pytest.mark.filterwarnings( + "ignore:invalid value encountered in bounds:RuntimeWarning") + def test_nonfinite_geometry_in_gdf_dropped_with_warning(self): + # shapely emits a RuntimeWarning while computing bounds over the NaN + # point; that is incidental to what this test asserts, so it is + # filtered to keep the warnings summary clean. + from shapely.geometry import Point + gdf = gpd.GeoDataFrame( + {'value': [1.0, 9.0]}, + geometry=[box(0, 0, 5, 5), Point(np.nan, 2.0)], + ) + with pytest.warns(UserWarning, match="non-finite coordinates"): + result = rasterize(gdf, width=5, height=5, bounds=(0, 0, 5, 5), + column='value', fill=0) + assert result.values[2, 2] == 1.0 + assert not np.any(result.values == 9.0)