@@ -215,19 +215,18 @@ def qgis_ndmi():
215215
216216@pytest .fixture
217217def qgis_savi ():
218- # this result is obtained by using NIR, and red band data
219- # running through QGIS Raster Calculator with formula:
220- # savi = (nir - red) / ((nir + red + soil_factor) * (1 + soil_factor))
218+ # Correct SAVI (Huete 1988):
219+ # savi = ((nir - red) / (nir + red + L)) * (1 + L)
221220 # with default value of soil_factor=1
222221 result = np .array ([
223- [0. , 0.10726268 , 0.10682587 , 0.09168259 ],
224- [0.10089815 , 0.10729991 , 0.10749393 , np .nan ],
225- [0.11363809 , 0.11995638 , 0.11994251 , 0.10915995 ],
226- [0.10226355 , 0.11864913 , 0.12966092 , 0.11774762 ],
227- [0.09810041 , 0.10675804 , 0.12213238 , 0.11514599 ],
228- [0.09377059 , 0.10416108 , 0.11123802 , 0.10735555 ],
229- [0.09097284 , 0.0988547 , 0.10404798 , 0.10413785 ],
230- [0.0870268 , 0.09878284 , 0.105046 , 0.10514525 ]], dtype = np .float32 )
222+ [0. , 0.4290507 , 0.4273035 , 0.3667304 ],
223+ [0.4035926 , 0.4291996 , 0.4299757 , np .nan ],
224+ [0.4545524 , 0.4798255 , 0.4797700 , 0.4366398 ],
225+ [0.4090542 , 0.4745965 , 0.5186437 , 0.4709905 ],
226+ [0.3924016 , 0.4270322 , 0.4885295 , 0.4605840 ],
227+ [0.3750824 , 0.4166443 , 0.4449521 , 0.4294222 ],
228+ [0.3638914 , 0.3954188 , 0.4161919 , 0.4165514 ],
229+ [0.3481072 , 0.3951314 , 0.4201840 , 0.4205810 ]], dtype = np .float32 )
231230 return result
232231
233232
@@ -314,7 +313,8 @@ def data_uint_dtype_evi(dtype):
314313def data_uint_dtype_savi (dtype ):
315314 nir = xr .DataArray (np .array ([[1 , 1 ], [1 , 1 ]], dtype = dtype ))
316315 red = xr .DataArray (np .array ([[0 , 1 ], [0 , 2 ]], dtype = dtype ))
317- result = np .array ([[0.25 , 0. ], [0.25 , - 0.125 ]], dtype = np .float32 )
316+ # Correct SAVI with L=1: ((nir-red)/(nir+red+1)) * 2
317+ result = np .array ([[1.0 , 0. ], [1.0 , - 0.5 ]], dtype = np .float32 )
318318 return nir , red , result
319319
320320
@@ -424,6 +424,50 @@ def test_savi_gpu(nir_data, red_data, qgis_savi):
424424 general_output_checks (nir_data , result , qgis_savi )
425425
426426
427+ @pytest .mark .parametrize ("backend" , ["numpy" , "cupy" , "dask+numpy" , "dask+cupy" ])
428+ def test_savi_formula_1094 (backend ):
429+ """Verify SAVI against the Huete (1988) formula directly.
430+
431+ Regression test for #1094: (1+L) was in the denominator instead
432+ of the numerator.
433+ """
434+ from xrspatial .tests .general_checks import has_cuda_and_cupy , dask_array_available as _dask_avail
435+ if 'cupy' in backend and not has_cuda_and_cupy ():
436+ pytest .skip ("Requires CUDA and CuPy" )
437+ try :
438+ import dask .array
439+ except ImportError :
440+ if 'dask' in backend :
441+ pytest .skip ("Requires Dask" )
442+
443+ nir_arr = np .array ([[0.8 , 0.6 ], [0.4 , 0.0 ]])
444+ red_arr = np .array ([[0.2 , 0.3 ], [0.4 , 0.0 ]])
445+
446+ nir_agg = create_test_raster (nir_arr , backend = backend , chunks = (2 , 2 ))
447+ red_agg = create_test_raster (red_arr , backend = backend , chunks = (2 , 2 ))
448+
449+ L = 0.5
450+ result = savi (nir_agg , red_agg , soil_factor = L )
451+ data = result .data
452+ if hasattr (data , 'compute' ):
453+ data = data .compute ()
454+ if hasattr (data , 'get' ):
455+ data = data .get ()
456+ data = np .asarray (data )
457+
458+ # Correct: ((NIR - Red) / (NIR + Red + L)) * (1 + L)
459+ expected = np .where (
460+ (nir_arr + red_arr + L ) != 0 ,
461+ ((nir_arr - red_arr ) / (nir_arr + red_arr + L )) * (1 + L ),
462+ np .nan ,
463+ ).astype (np .float32 )
464+
465+ np .testing .assert_allclose (data , expected , rtol = 1e-5 , equal_nan = True )
466+
467+ # Spot check: NIR=0.8, Red=0.2, L=0.5 -> (0.6/1.5)*1.5 = 0.6
468+ assert abs (float (data [0 , 0 ]) - 0.6 ) < 1e-5
469+
470+
427471# arvi -------------
428472@pytest .mark .parametrize ("backend" , ["numpy" , "dask+numpy" ])
429473def test_arvi_cpu_against_qgis (nir_data , red_data , blue_data , qgis_arvi ):
0 commit comments