@@ -71,10 +71,14 @@ def test_windowed_isel_backward_clock_loads_once_and_evicts():
7171 assert win .loads == ntime # each time level read exactly once, going backward
7272 assert max_cache <= 2 # only the bracketing levels resident
7373
74- def test_structured_windowed_arrays_wraps_dask_but_not_numpy ():
75- ds = simple_UV_dataset (mesh = "flat" )
76- fset_np = FieldSet .from_sgrid_conventions (ds , mesh = "flat" )
77- fset_dk = FieldSet .from_sgrid_conventions (ds .chunk ({"time" : 1 }), mesh = "flat" )
74+ @pytest .mark .parametrize ("fset_convention, ds" ,
75+ [(FieldSet .from_sgrid_conventions , simple_UV_dataset (mesh = "flat" )),
76+ (FieldSet .from_ugrid_conventions , _ux_constant_flow_face_centered_2D ())],
77+ ids = ["structured" , "unstructured" ]
78+ )
79+ def test_windowed_arrays_wraps_dask_but_not_numpy (fset_convention : callable , ds : xr .Dataset ):
80+ fset_np = fset_convention (ds , mesh = "flat" )
81+ fset_dk = fset_convention (ds .chunk ({"time" : 1 }), mesh = "flat" )
7882
7983 # construction is never windowing -- it is opt-in via the fieldset method
8084 assert not isinstance (fset_np .U .data , WindowedArray )
@@ -91,29 +95,13 @@ def test_structured_windowed_arrays_wraps_dask_but_not_numpy():
9195 assert fset_dk .U .data .dims == fset_np .U .data .dims
9296 assert fset_dk .U .data .shape == fset_np .U .data .shape
9397
94- def test_unstructured_windowed_arrays_wraps_dask_but_not_numpy ():
95- ds = _ux_constant_flow_face_centered_2D ()
96- fset_np = FieldSet .from_ugrid_conventions (ds , mesh = "flat" )
97- fset_dk = FieldSet .from_ugrid_conventions (ds .chunk ({"time" : 1 }), mesh = "flat" )
98-
99- # construction is never windowing -- it is opt-in via the fieldset method
100- assert not isinstance (fset_np .U .data , WindowedArray )
101- assert not isinstance (fset_dk .U .data , WindowedArray )
102- assert isinstance (fset_dk .U .data .data , da .Array ) # chunked input stays lazy (dask-backed)
103-
104- assert fset_np .to_windowed_arrays () is fset_np # chainable
105- fset_dk .to_windowed_arrays ()
106-
107- # numpy-backed field is left eager; dask-backed field gets wrapped
108- assert not isinstance (fset_np .U .data , WindowedArray )
109- assert isinstance (fset_dk .U .data , WindowedArray )
110- # transparency: forwarded attributes still behave like the DataArray
111- assert fset_dk .U .data .dims == fset_np .U .data .dims
112- assert fset_dk .U .data .shape == fset_np .U .data .shape
113-
114- def test_to_windowed_arrays_is_idempotent_and_forwards_max_levels ():
115- ds = simple_UV_dataset (mesh = "flat" )
116- fs = FieldSet .from_sgrid_conventions (ds .chunk ({"time" : 1 }), mesh = "flat" )
98+ @pytest .mark .parametrize ("fset_convention, ds" ,
99+ [(FieldSet .from_sgrid_conventions , simple_UV_dataset (mesh = "flat" )),
100+ (FieldSet .from_ugrid_conventions , _ux_constant_flow_face_centered_2D ())],
101+ ids = ["structured" , "unstructured" ]
102+ )
103+ def test_to_windowed_arrays_is_idempotent_and_forwards_max_levels (fset_convention : callable , ds : xr .Dataset ):
104+ fs = fset_convention (ds .chunk ({"time" : 1 }), mesh = "flat" )
117105
118106 fs .to_windowed_arrays (max_levels = 3 )
119107 first = fs .U .data
@@ -160,10 +148,10 @@ def test_maybe_windowed_passthrough_for_non_time_leading():
160148
161149@pytest .mark .parametrize ("mesh" , ["flat" , "spherical" ])
162150@pytest .mark .parametrize ("dt_minutes" , [15 , - 15 ], ids = ["forward" , "backward" ])
163- def test_dask_advection_matches_numpy (mesh , dt_minutes ):
151+ def test_dask_advection_matches_numpy_on_structured_grids (mesh , dt_minutes ):
164152 """An identical advection must give identical trajectories whether the field
165153 is numpy-backed or dask-backed (windowed) -- for both forward (dt > 0) and
166- backward (dt < 0) integration.
154+ backward (dt < 0) integration on structured grids .
167155 """
168156 ds = simple_UV_dataset (mesh = mesh )
169157 ds ["U" ].data [:] = 1.0 # steady zonal flow -> in-bounds, deterministic
@@ -181,3 +169,26 @@ def run(chunked):
181169 x_dk , y_dk = run (True )
182170 np .testing .assert_allclose (x_dk , x_np , atol = 1e-9 )
183171 np .testing .assert_allclose (y_dk , y_np , atol = 1e-9 )
172+
173+ @pytest .mark .parametrize ("mesh" , ["flat" , "spherical" ])
174+ @pytest .mark .parametrize ("dt_minutes" , [15 , - 15 ], ids = ["forward" , "backward" ])
175+ def test_dask_advection_matches_numpy_on_unstructured_grids (mesh , dt_minutes ):
176+ """An identical advection must give identical trajectories whether the field
177+ is numpy-backed or dask-backed (windowed) -- for both forward (dt > 0) and
178+ backward (dt < 0) integration on unstructured grids.
179+ """
180+ ds = _ux_constant_flow_face_centered_2D ()
181+
182+ def run (chunked ):
183+ d = ds .chunk ({"time" : 1 }) if chunked else ds
184+ fs = FieldSet .from_ugrid_conventions (d , mesh = mesh )
185+ if chunked :
186+ fs .to_windowed_arrays ()
187+ pset = ParticleSet (fs , x = 10 * np .ones (10 ), y = np .linspace (5 , 15 , 10 ))
188+ pset .execute (AdvectionRK2 , runtime = 3600 , dt = np .timedelta64 (dt_minutes , "m" ))
189+ return np .array (pset .x ), np .array (pset .y )
190+
191+ x_np , y_np = run (False )
192+ x_dk , y_dk = run (True )
193+ np .testing .assert_allclose (x_dk , x_np , atol = 1e-9 )
194+ np .testing .assert_allclose (y_dk , y_np , atol = 1e-9 )
0 commit comments