|
4 | 4 | import pytest |
5 | 5 | import xarray as xr |
6 | 6 |
|
7 | | -from access_moppy.derivations.calc_land import calc_snc |
| 7 | +from access_moppy.derivations.calc_land import calc_rootd, calc_snc |
8 | 8 |
|
9 | 9 | # --------------------------------------------------------------------------- |
10 | 10 | # Helpers |
@@ -161,3 +161,146 @@ def test_mismatched_pseudo_level_name_handled(self): |
161 | 161 | result = calc_snc(tf, sn, lf) |
162 | 162 |
|
163 | 163 | np.testing.assert_allclose(result.values, 100.0) |
| 164 | + |
| 165 | + |
| 166 | +# --------------------------------------------------------------------------- |
| 167 | +# Helpers for calc_rootd |
| 168 | +# --------------------------------------------------------------------------- |
| 169 | + |
| 170 | +NT_R = 2 # time steps |
| 171 | +N_TILES = 17 # tiles 1-17 (pseudo_level_1 coordinate values) |
| 172 | +NJ_R = 3 |
| 173 | +NI_R = 4 |
| 174 | + |
| 175 | + |
| 176 | +def _make_tilefrac(data): |
| 177 | + """Build a tilefrac DataArray with pseudo_level_1 coords 1-17.""" |
| 178 | + times = xr.date_range("2000-01-01", periods=NT_R, freq="ME") |
| 179 | + return xr.DataArray( |
| 180 | + data, |
| 181 | + dims=["time", "pseudo_level_1", "lat", "lon"], |
| 182 | + coords={ |
| 183 | + "time": times, |
| 184 | + "pseudo_level_1": np.arange(1, N_TILES + 1), |
| 185 | + }, |
| 186 | + attrs={"units": "1"}, |
| 187 | + ) |
| 188 | + |
| 189 | + |
| 190 | +# --------------------------------------------------------------------------- |
| 191 | +# Tests |
| 192 | +# --------------------------------------------------------------------------- |
| 193 | + |
| 194 | + |
| 195 | +class TestCalcRootd: |
| 196 | + """Tests for calc_rootd() — issue #335.""" |
| 197 | + |
| 198 | + def test_vegetated_returns_4p6(self): |
| 199 | + """Grid cells with any vegetated tile (1-13) get 4.6 m.""" |
| 200 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 201 | + data[:, 0, :, :] = 0.5 # tile 1 (index 0) is vegetated |
| 202 | + tilefrac = _make_tilefrac(data) |
| 203 | + |
| 204 | + result = calc_rootd(tilefrac) |
| 205 | + |
| 206 | + assert result.dims == ("lat", "lon") |
| 207 | + np.testing.assert_allclose(result.values, 4.6) |
| 208 | + |
| 209 | + def test_non_veg_land_returns_zero(self): |
| 210 | + """Land cells with only non-veg tiles (14-17) get 0 m.""" |
| 211 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 212 | + data[:, 13, :, :] = 1.0 # tile 14 (index 13) — ice/urban/barren/lake |
| 213 | + tilefrac = _make_tilefrac(data) |
| 214 | + |
| 215 | + result = calc_rootd(tilefrac) |
| 216 | + |
| 217 | + assert result.dims == ("lat", "lon") |
| 218 | + np.testing.assert_allclose(result.values, 0.0) |
| 219 | + |
| 220 | + def test_ocean_returns_nan(self): |
| 221 | + """Cells with all-zero tile fractions (ocean) get NaN (missing).""" |
| 222 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 223 | + tilefrac = _make_tilefrac(data) |
| 224 | + |
| 225 | + result = calc_rootd(tilefrac) |
| 226 | + |
| 227 | + assert result.dims == ("lat", "lon") |
| 228 | + assert np.all(np.isnan(result.values)) |
| 229 | + |
| 230 | + def test_mixed_grid(self): |
| 231 | + """Spatial mix: veg / non-veg land / ocean all in the same array.""" |
| 232 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 233 | + # lat 0: vegetated |
| 234 | + data[:, 0, 0, :] = 0.8 # tile 1 |
| 235 | + # lat 1: non-veg land only (tile 14) |
| 236 | + data[:, 13, 1, :] = 1.0 |
| 237 | + # lat 2: ocean — all zeros |
| 238 | + |
| 239 | + tilefrac = _make_tilefrac(data) |
| 240 | + result = calc_rootd(tilefrac) |
| 241 | + |
| 242 | + np.testing.assert_allclose(result.values[0, :], 4.6) # veg |
| 243 | + np.testing.assert_allclose(result.values[1, :], 0.0) # non-veg land |
| 244 | + assert np.all(np.isnan(result.values[2, :])) # ocean |
| 245 | + |
| 246 | + def test_veg_appears_in_only_one_timestep(self): |
| 247 | + """Vegetation present in any timestep is enough to report 4.6 m.""" |
| 248 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 249 | + data[0, 2, :, :] = 0.3 # tile 3, timestep 0 only |
| 250 | + tilefrac = _make_tilefrac(data) |
| 251 | + |
| 252 | + result = calc_rootd(tilefrac) |
| 253 | + |
| 254 | + np.testing.assert_allclose(result.values, 4.6) |
| 255 | + |
| 256 | + def test_no_time_dimension_removed(self): |
| 257 | + """Output has no time dimension regardless of input.""" |
| 258 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 259 | + data[:, 0, :, :] = 0.5 |
| 260 | + tilefrac = _make_tilefrac(data) |
| 261 | + |
| 262 | + result = calc_rootd(tilefrac) |
| 263 | + |
| 264 | + assert "time" not in result.dims |
| 265 | + |
| 266 | + def test_dask_compatible(self): |
| 267 | + """Function returns a lazy dask-backed array without calling compute.""" |
| 268 | + pytest.importorskip("dask") |
| 269 | + |
| 270 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 271 | + data[:, 0, :, :] = 0.5 |
| 272 | + tilefrac = _make_tilefrac(data).chunk({"time": 1}) |
| 273 | + |
| 274 | + result = calc_rootd(tilefrac) |
| 275 | + |
| 276 | + assert result.chunks is not None, "result should be dask-backed (lazy)" |
| 277 | + np.testing.assert_allclose(result.compute().values, 4.6) |
| 278 | + |
| 279 | + def test_pseudo_level_0_without_coord_is_supported(self): |
| 280 | + """Regression: works when tile dimension is pseudo_level_0 with no coord.""" |
| 281 | + times = xr.date_range("2000-01-01", periods=NT_R, freq="ME") |
| 282 | + data = np.zeros((NT_R, N_TILES, NJ_R, NI_R)) |
| 283 | + data[:, 0, :, :] = 0.5 |
| 284 | + tilefrac = xr.DataArray( |
| 285 | + data, |
| 286 | + dims=["time", "pseudo_level_0", "lat", "lon"], |
| 287 | + coords={"time": times}, |
| 288 | + ) |
| 289 | + |
| 290 | + result = calc_rootd(tilefrac) |
| 291 | + |
| 292 | + assert result.dims == ("lat", "lon") |
| 293 | + np.testing.assert_allclose(result.values, 4.6) |
| 294 | + |
| 295 | + def test_missing_pseudo_level_dimension_raises(self): |
| 296 | + """Defensive path: raise when tilefrac has no pseudo_level dimension.""" |
| 297 | + times = xr.date_range("2000-01-01", periods=NT_R, freq="ME") |
| 298 | + data = np.zeros((NT_R, NJ_R, NI_R)) |
| 299 | + tilefrac = xr.DataArray( |
| 300 | + data, |
| 301 | + dims=["time", "lat", "lon"], |
| 302 | + coords={"time": times}, |
| 303 | + ) |
| 304 | + |
| 305 | + with pytest.raises(ValueError, match="No pseudo_level dimension found"): |
| 306 | + calc_rootd(tilefrac) |
0 commit comments