@@ -439,7 +439,13 @@ def data_focal_stats():
439439 [[0 , 1 , 2 , 3. ],
440440 [4 , 5 , 7 , 9. ],
441441 [8 , 13 , 15 , 17. ],
442- [12 , 21 , 23 , 25. ]]
442+ [12 , 21 , 23 , 25. ]],
443+ # variety -- arange(16) so every value is unique;
444+ # kernel hits center + upper-left diagonal only
445+ [[1 , 1 , 1 , 1. ],
446+ [1 , 2 , 2 , 2. ],
447+ [1 , 2 , 2 , 2. ],
448+ [1 , 2 , 2 , 2. ]]
443449 ])
444450 return data , kernel , expected_result
445451
@@ -499,6 +505,82 @@ def test_focal_stats_dask_cupy():
499505 equal_nan = True , rtol = 1e-4 )
500506
501507
508+ # --- focal variety (issue-1040) ------------------------------------------
509+
510+ def _variety_reference_data ():
511+ """Categorical 6x6 grid with known variety counts for a 3x3 box kernel."""
512+ data = np .array ([
513+ [1 , 1 , 2 , 2 , 3 , 3 ],
514+ [1 , 1 , 2 , 2 , 3 , 3 ],
515+ [4 , 4 , 5 , 5 , 6 , 6 ],
516+ [4 , 4 , 5 , 5 , 6 , 6 ],
517+ [7 , 7 , 8 , 8 , 9 , 9 ],
518+ [7 , 7 , 8 , 8 , 9 , 9 ],
519+ ], dtype = np .float64 )
520+ kernel = np .ones ((3 , 3 ))
521+ return data , kernel
522+
523+
524+ def test_variety_numpy ():
525+ data , kernel = _variety_reference_data ()
526+ agg = create_test_raster (data )
527+ result = focal_stats (agg , kernel , stats_funcs = ['variety' ])
528+ vals = result .sel (stats = 'variety' ).values
529+ # Interior pixel (2,2) sees values {1,2,4,5} -> 4
530+ assert vals [2 , 2 ] == 4.0
531+ # Corner pixel (0,0) window is 2x2 -> {1,1,1,1} -> 1
532+ assert vals [0 , 0 ] == 1.0
533+ # Edge pixel (0,2) window is 2x3 -> {1,1,2,2,2,2} -> 2
534+ assert vals [0 , 2 ] == 2.0
535+ # All interior values should be positive integers
536+ assert np .all (vals [1 :- 1 , 1 :- 1 ] >= 1 )
537+
538+
539+ @dask_array_available
540+ def test_variety_dask_numpy ():
541+ data , kernel = _variety_reference_data ()
542+ np_agg = create_test_raster (data )
543+ dk_agg = create_test_raster (data , backend = 'dask' )
544+ np_result = focal_stats (np_agg , kernel , stats_funcs = ['variety' ])
545+ dk_result = focal_stats (dk_agg , kernel , stats_funcs = ['variety' ])
546+ np .testing .assert_allclose (
547+ np_result .values , dk_result .values , equal_nan = True )
548+
549+
550+ def test_variety_nan_handling ():
551+ """NaN cells should not count as a distinct value."""
552+ data = np .array ([
553+ [1 , np .nan , 2 ],
554+ [1 , 1 , 2 ],
555+ [3 , 3 , 3 ],
556+ ], dtype = np .float64 )
557+ kernel = np .ones ((3 , 3 ))
558+ agg = create_test_raster (data )
559+ result = focal_stats (agg , kernel , stats_funcs = ['variety' ])
560+ vals = result .sel (stats = 'variety' ).values
561+ # Center pixel (1,1) sees {1,2,1,1,2,3,3,3} (NaN skipped) -> {1,2,3} -> 3
562+ assert vals [1 , 1 ] == 3.0
563+
564+
565+ def test_variety_all_nan ():
566+ """A window of all NaN should produce NaN variety."""
567+ data = np .full ((3 , 3 ), np .nan )
568+ kernel = np .ones ((3 , 3 ))
569+ agg = create_test_raster (data )
570+ result = focal_stats (agg , kernel , stats_funcs = ['variety' ])
571+ vals = result .sel (stats = 'variety' ).values
572+ assert np .all (np .isnan (vals ))
573+
574+
575+ def test_variety_single_cell ():
576+ """1x1 raster, 1x1 kernel -> variety 1."""
577+ data = np .array ([[42.0 ]])
578+ kernel = np .ones ((1 , 1 ))
579+ agg = create_test_raster (data )
580+ result = focal_stats (agg , kernel , stats_funcs = ['variety' ])
581+ assert result .sel (stats = 'variety' ).values .item () == 1.0
582+
583+
502584@pytest .fixture
503585def data_hotspots ():
504586 data = np .asarray ([
0 commit comments