2929 "max" : move_max ,
3030}
3131
32+ # Add std wrapper when bottleneck is available.
33+ if "bottleneck" in _get_available_engines ():
34+ TEST_CONVENIENCE_FUNCS ["std" ] = move_std
35+
3236
3337class TestMovingWindow :
3438 """Test moving window operations with different engines."""
@@ -40,7 +44,7 @@ def _validate_basic_result(self, result, original_data):
4044 assert result .shape == original_data .shape
4145 assert result .dtype .kind in ["f" , "i" , "c" ] # float, int, or complex
4246
43- def _test_finite_properties (self , results , data ):
47+ def _test_finite_properties (self , results , data , window ):
4448 """Test mathematical properties of operations."""
4549 # Find where all results are finite
4650 all_finite = np .ones (len (data ), dtype = bool )
@@ -50,23 +54,41 @@ def _test_finite_properties(self, results, data):
5054 if not np .any (all_finite ):
5155 return # Skip if no finite values
5256
57+ mean_vals = results ["mean" ]
58+ sum_vals = results ["sum" ]
59+ min_vals = results ["min" ]
60+ max_vals = results ["max" ]
61+
62+ # On truly interior indices, sum should equal mean * window (within tolerance).
63+ # These are indices where the full window is available without edge effects.
64+ if window >= 3 and len (data ) >= window :
65+ # True interior: start from window-1 to end-(window-1)
66+ interior_start = window - 1
67+ interior_end = len (data ) - (window - 1 )
68+ if interior_end > interior_start :
69+ interior_slice = slice (interior_start , interior_end )
70+ interior_finite = all_finite [interior_slice ]
71+ if np .any (interior_finite ):
72+ mean_interior = mean_vals [interior_slice ][interior_finite ]
73+ sum_interior = sum_vals [interior_slice ][interior_finite ]
74+ np .testing .assert_allclose (
75+ sum_interior , mean_interior * window , rtol = 1e-6 , atol = 1e-12
76+ )
77+
78+ # Min should be <= max (test on all finite indices)
5379 finite_indices = np .where (all_finite )[0 ]
54-
55- # Test properties only where values are finite
56- mean_vals = results ["mean" ][finite_indices ]
57- sum_vals = results ["sum" ][finite_indices ]
58- min_vals = results ["min" ][finite_indices ]
59- max_vals = results ["max" ][finite_indices ]
60-
61- # Sum should be >= mean (for positive window size)
62- assert np .all (sum_vals >= mean_vals )
63- # Min should be <= max
64- assert np .all (min_vals <= max_vals )
80+ if len (finite_indices ) > 0 :
81+ min_finite = min_vals [finite_indices ]
82+ max_finite = max_vals [finite_indices ]
83+ assert np .all (min_finite <= max_finite )
6584
6685 def _compare_engine_results (self , result1 , result2 , window ):
6786 """Compare results from different engines."""
68- # Compare interior values (avoiding edge effects)
69- interior_slice = slice (window // 2 , - window // 2 if window > 2 else None )
87+ # Skip comparison for tiny windows where a stable interior is ill-defined.
88+ if window < 3 :
89+ return
90+ # Symmetric interior (works for odd/even windows and avoids edges)
91+ interior_slice = slice (window // 2 , - window // 2 )
7092
7193 interior1 = result1 [interior_slice ]
7294 interior2 = result2 [interior_slice ]
@@ -80,10 +102,7 @@ def _compare_engine_results(self, result1, result2, window):
80102 vals1 = interior1 [common_finite ]
81103 vals2 = interior2 [common_finite ]
82104
83- # Check that both give reasonable results (within data range)
84- data_min , data_max = - 10 , 10 # Reasonable range
85- assert np .all ((vals1 >= data_min ) & (vals1 <= data_max ))
86- assert np .all ((vals2 >= data_min ) & (vals2 <= data_max ))
105+ # Value-range guard removed; relative agreement is checked below.
87106
88107 # For most operations, results should be similar
89108 # (allowing for different edge handling)
@@ -153,8 +172,9 @@ def test_multi_axis_operations(self, test_data):
153172 window = 2
154173
155174 for axis in [0 , 1 ]:
156- result = move_median (data , window , axis = axis )
157- assert result .shape == data .shape
175+ for func in (move_median , move_mean , move_sum , move_min , move_max ):
176+ result = func (data , window , axis = axis )
177+ assert result .shape == data .shape
158178
159179 # Input validation tests
160180 @pytest .mark .parametrize ("invalid_window" , [0 , - 1 ])
@@ -210,6 +230,9 @@ def test_different_dtypes(self, test_data):
210230 data = base_data .astype (dtype )
211231 result = move_mean (data , 3 )
212232 assert isinstance (result , np .ndarray )
233+ # Mean should promote to floating dtype for integer inputs.
234+ if dtype in [np .int32 ]:
235+ assert result .dtype .kind == "f"
213236
214237 # Numerical properties tests
215238 def test_operation_properties (self , test_data ):
@@ -222,7 +245,7 @@ def test_operation_properties(self, test_data):
222245 results [operation ] = moving_window (data , window , operation )
223246
224247 # Test properties where both results are finite
225- self ._test_finite_properties (results , data )
248+ self ._test_finite_properties (results , data , window )
226249
227250 def test_numerical_accuracy (self ):
228251 """Test numerical accuracy with known results."""
0 commit comments