roms-tools 3.4.0__py3-none-any.whl → 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roms_tools/datasets/lat_lon_datasets.py +12 -0
- roms_tools/datasets/roms_dataset.py +140 -53
- roms_tools/datasets/utils.py +14 -2
- roms_tools/regrid.py +76 -0
- roms_tools/setup/boundary_forcing.py +2 -2
- roms_tools/setup/grid.py +17 -3
- roms_tools/setup/initial_conditions.py +314 -55
- roms_tools/setup/mask.py +2 -5
- roms_tools/setup/nesting.py +6 -3
- roms_tools/setup/surface_forcing.py +1 -2
- roms_tools/setup/tides.py +6 -5
- roms_tools/setup/utils.py +220 -142
- roms_tools/tests/test_datasets/test_roms_dataset.py +225 -21
- roms_tools/tests/test_regrid.py +120 -1
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/c/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/zarr.json +47 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/c/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/zarr.json +47 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/abs_time/zarr.json +47 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/c/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/zarr.json +47 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/zarr.json +54 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/zarr.json +54 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/w/zarr.json +57 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zarr.json +2481 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/zarr.json +54 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/c/0/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/zarr.json +57 -0
- roms_tools/tests/test_setup/test_grid.py +24 -0
- roms_tools/tests/test_setup/test_initial_conditions.py +128 -11
- roms_tools/tests/test_setup/test_validation.py +15 -0
- roms_tools/tests/test_utils.py +287 -0
- roms_tools/utils.py +177 -72
- {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/METADATA +2 -3
- {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/RECORD +111 -24
- {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/WHEEL +1 -1
- {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/top_level.txt +0 -0
roms_tools/tests/test_utils.py
CHANGED
|
@@ -10,6 +10,7 @@ import xarray as xr
|
|
|
10
10
|
from roms_tools.datasets.download import download_test_data
|
|
11
11
|
from roms_tools.datasets.lat_lon_datasets import ERA5Correction
|
|
12
12
|
from roms_tools.utils import (
|
|
13
|
+
_interpolate_generic,
|
|
13
14
|
_path_list_from_input,
|
|
14
15
|
generate_focused_coordinate_range,
|
|
15
16
|
get_dask_chunks,
|
|
@@ -18,7 +19,13 @@ from roms_tools.utils import (
|
|
|
18
19
|
has_gcsfs,
|
|
19
20
|
interpolate_cyclic_time,
|
|
20
21
|
interpolate_from_climatology,
|
|
22
|
+
interpolate_from_rho_to_u,
|
|
23
|
+
interpolate_from_rho_to_v,
|
|
24
|
+
interpolate_from_u_to_rho,
|
|
25
|
+
interpolate_from_v_to_rho,
|
|
21
26
|
load_data,
|
|
27
|
+
rotate_velocities,
|
|
28
|
+
wrap_longitudes,
|
|
22
29
|
)
|
|
23
30
|
|
|
24
31
|
|
|
@@ -319,3 +326,283 @@ def test_interpolate_from_real_climatology(use_dask):
|
|
|
319
326
|
|
|
320
327
|
interpolated_field = interpolate_from_climatology(field, "time", "time", era5_times)
|
|
321
328
|
assert len(interpolated_field.time) == len(era5_times)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_wrap_longitudes_staggered():
|
|
332
|
+
# Dimensions
|
|
333
|
+
eta_rho, xi_rho, xi_u = 3, 4, 5
|
|
334
|
+
eta_v = 2
|
|
335
|
+
|
|
336
|
+
# Create 2D coordinates
|
|
337
|
+
lon_rho = xr.DataArray(
|
|
338
|
+
np.linspace(0, 360, eta_rho * xi_rho).reshape(eta_rho, xi_rho),
|
|
339
|
+
dims=("eta_rho", "xi_rho"),
|
|
340
|
+
attrs={"units": "degrees_east"},
|
|
341
|
+
)
|
|
342
|
+
lon_u = xr.DataArray(
|
|
343
|
+
np.linspace(-190, 190, eta_rho * xi_u).reshape(eta_rho, xi_u),
|
|
344
|
+
dims=("eta_rho", "xi_u"),
|
|
345
|
+
attrs={"units": "degrees_east"},
|
|
346
|
+
)
|
|
347
|
+
lon_v = xr.DataArray(
|
|
348
|
+
np.linspace(-180, 180, eta_v * xi_rho).reshape(eta_v, xi_rho),
|
|
349
|
+
dims=("eta_v", "xi_rho"),
|
|
350
|
+
attrs={"units": "degrees_east"},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Dummy variables
|
|
354
|
+
ds = xr.Dataset(
|
|
355
|
+
{
|
|
356
|
+
"dummy_rho": (("eta_rho", "xi_rho"), np.zeros((eta_rho, xi_rho))),
|
|
357
|
+
"dummy_u": (("eta_rho", "xi_u"), np.zeros((eta_rho, xi_u))),
|
|
358
|
+
"dummy_v": (("eta_v", "xi_rho"), np.zeros((eta_v, xi_rho))),
|
|
359
|
+
},
|
|
360
|
+
coords={"lon_rho": lon_rho, "lon_u": lon_u, "lon_v": lon_v},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Wrap to [-180, 180]
|
|
364
|
+
ds_wrapped = wrap_longitudes(ds, straddle=True)
|
|
365
|
+
|
|
366
|
+
# Check values: all >180 should be shifted
|
|
367
|
+
assert ds_wrapped.lon_rho.max().values <= 180
|
|
368
|
+
assert ds_wrapped.lon_u.max().values <= 180
|
|
369
|
+
assert ds_wrapped.lon_v.max().values <= 180
|
|
370
|
+
|
|
371
|
+
# Wrap to [0, 360]
|
|
372
|
+
ds_wrapped2 = wrap_longitudes(ds, straddle=False)
|
|
373
|
+
assert ds_wrapped2.lon_rho.min().values >= 0
|
|
374
|
+
assert ds_wrapped2.lon_u.min().values >= 0
|
|
375
|
+
assert ds_wrapped2.lon_v.min().values >= 0
|
|
376
|
+
|
|
377
|
+
# Check attributes preserved
|
|
378
|
+
for name in ["lon_rho", "lon_u", "lon_v"]:
|
|
379
|
+
assert ds.coords[name].attrs == ds_wrapped.coords[name].attrs
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# test _interpolate_generic and its wrappers
|
|
383
|
+
|
|
384
|
+
# -------------------------
|
|
385
|
+
# Fixtures
|
|
386
|
+
# -------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@pytest.fixture
|
|
390
|
+
def sample_rho_field() -> xr.DataArray:
|
|
391
|
+
"""Create a simple rho-point field for testing."""
|
|
392
|
+
data = np.arange(12, dtype=float).reshape(3, 4)
|
|
393
|
+
eta = np.arange(3)
|
|
394
|
+
xi = np.arange(4)
|
|
395
|
+
|
|
396
|
+
return xr.DataArray(
|
|
397
|
+
data,
|
|
398
|
+
dims=("eta_rho", "xi_rho"),
|
|
399
|
+
coords={
|
|
400
|
+
"lat_rho": (("eta_rho", "xi_rho"), eta[:, None] * np.ones((1, 4))),
|
|
401
|
+
"lon_rho": (("eta_rho", "xi_rho"), np.ones((3, 1)) * xi[None, :]),
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@pytest.fixture
|
|
407
|
+
def sample_u_field() -> xr.DataArray:
|
|
408
|
+
"""Create a simple u-point field for testing."""
|
|
409
|
+
data = np.arange(9, dtype=float).reshape(3, 3)
|
|
410
|
+
eta = np.arange(3)
|
|
411
|
+
xi = np.arange(3)
|
|
412
|
+
|
|
413
|
+
return xr.DataArray(
|
|
414
|
+
data,
|
|
415
|
+
dims=("eta_rho", "xi_u"),
|
|
416
|
+
coords={
|
|
417
|
+
"lat_u": (("eta_rho", "xi_u"), eta[:, None] * np.ones((1, 3))),
|
|
418
|
+
"lon_u": (("eta_rho", "xi_u"), np.ones((3, 1)) * xi[None, :]),
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@pytest.fixture
|
|
424
|
+
def sample_v_field() -> xr.DataArray:
|
|
425
|
+
"""Create a simple v-point field for testing."""
|
|
426
|
+
data = np.arange(8, dtype=float).reshape(2, 4)
|
|
427
|
+
eta = np.arange(2)
|
|
428
|
+
xi = np.arange(4)
|
|
429
|
+
|
|
430
|
+
return xr.DataArray(
|
|
431
|
+
data,
|
|
432
|
+
dims=("eta_v", "xi_rho"),
|
|
433
|
+
coords={
|
|
434
|
+
"lat_v": (("eta_v", "xi_rho"), eta[:, None] * np.ones((1, 4))),
|
|
435
|
+
"lon_v": (("eta_v", "xi_rho"), np.ones((2, 1)) * xi[None, :]),
|
|
436
|
+
},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# -------------------------
|
|
441
|
+
# Generic interpolation tests
|
|
442
|
+
# -------------------------
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_interpolate_from_rho_to_u_additive(sample_rho_field: xr.DataArray):
|
|
446
|
+
result = _interpolate_generic(
|
|
447
|
+
sample_rho_field, dim_in="xi_rho", dim_out="xi_u", method="additive"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# One fewer point along xi
|
|
451
|
+
assert result.shape[1] == sample_rho_field.shape[1] - 1
|
|
452
|
+
|
|
453
|
+
expected = 0.5 * (sample_rho_field.values[:, 1:] + sample_rho_field.values[:, :-1])
|
|
454
|
+
np.testing.assert_allclose(result.values, expected)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_interpolate_from_rho_to_u_multiplicative(sample_rho_field: xr.DataArray):
|
|
458
|
+
result = _interpolate_generic(
|
|
459
|
+
sample_rho_field, dim_in="xi_rho", dim_out="xi_u", method="multiplicative"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
expected = sample_rho_field.values[:, 1:] * sample_rho_field.values[:, :-1]
|
|
463
|
+
np.testing.assert_allclose(result.values, expected)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# -------------------------
|
|
467
|
+
# Wrapper tests
|
|
468
|
+
# -------------------------
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_rho_to_u_wrapper_additive(sample_rho_field: xr.DataArray):
|
|
472
|
+
result = interpolate_from_rho_to_u(sample_rho_field, method="additive")
|
|
473
|
+
|
|
474
|
+
# Dimension swap
|
|
475
|
+
assert "xi_u" in result.dims
|
|
476
|
+
assert "xi_rho" not in result.dims
|
|
477
|
+
|
|
478
|
+
# Coordinates dropped
|
|
479
|
+
for coord in ("lat_rho", "lon_rho"):
|
|
480
|
+
assert coord not in result.coords
|
|
481
|
+
|
|
482
|
+
# Shape check
|
|
483
|
+
assert result.sizes["xi_u"] == sample_rho_field.sizes["xi_rho"] - 1
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_rho_to_v_wrapper_additive(sample_rho_field: xr.DataArray):
|
|
487
|
+
result = interpolate_from_rho_to_v(sample_rho_field, method="additive")
|
|
488
|
+
|
|
489
|
+
# Dimension swap
|
|
490
|
+
assert "eta_v" in result.dims
|
|
491
|
+
assert "eta_rho" not in result.dims
|
|
492
|
+
|
|
493
|
+
# Coordinates dropped
|
|
494
|
+
for coord in ("lat_rho", "lon_rho"):
|
|
495
|
+
assert coord not in result.coords
|
|
496
|
+
|
|
497
|
+
# Shape check
|
|
498
|
+
assert result.sizes["eta_v"] == sample_rho_field.sizes["eta_rho"] - 1
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_u_to_rho_wrapper_additive(sample_u_field: xr.DataArray):
|
|
502
|
+
result = interpolate_from_u_to_rho(sample_u_field, method="additive")
|
|
503
|
+
|
|
504
|
+
# Dimension swap
|
|
505
|
+
assert "xi_rho" in result.dims
|
|
506
|
+
assert "xi_u" not in result.dims
|
|
507
|
+
|
|
508
|
+
# Coordinates dropped
|
|
509
|
+
for coord in ("lat_u", "lon_u"):
|
|
510
|
+
assert coord not in result.coords
|
|
511
|
+
|
|
512
|
+
# Shape: one more along xi due to padding
|
|
513
|
+
assert result.sizes["xi_rho"] == sample_u_field.sizes["xi_u"] + 1
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def test_v_to_rho_wrapper_additive(sample_v_field: xr.DataArray):
|
|
517
|
+
result = interpolate_from_v_to_rho(sample_v_field, method="additive")
|
|
518
|
+
|
|
519
|
+
# Dimension swap
|
|
520
|
+
assert "eta_rho" in result.dims
|
|
521
|
+
assert "eta_v" not in result.dims
|
|
522
|
+
|
|
523
|
+
# Coordinates dropped
|
|
524
|
+
for coord in ("lat_v", "lon_v"):
|
|
525
|
+
assert coord not in result.coords
|
|
526
|
+
|
|
527
|
+
# Shape: one more along eta due to padding
|
|
528
|
+
assert result.sizes["eta_rho"] == sample_v_field.sizes["eta_v"] + 1
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# -------------------------
|
|
532
|
+
# Error handling
|
|
533
|
+
# -------------------------
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_invalid_method_raises(
|
|
537
|
+
sample_rho_field: xr.DataArray,
|
|
538
|
+
sample_u_field: xr.DataArray,
|
|
539
|
+
sample_v_field: xr.DataArray,
|
|
540
|
+
):
|
|
541
|
+
with pytest.raises(NotImplementedError):
|
|
542
|
+
interpolate_from_rho_to_u(sample_rho_field, method="unsupported")
|
|
543
|
+
|
|
544
|
+
with pytest.raises(NotImplementedError):
|
|
545
|
+
interpolate_from_rho_to_v(sample_rho_field, method="unsupported")
|
|
546
|
+
|
|
547
|
+
with pytest.raises(NotImplementedError):
|
|
548
|
+
interpolate_from_u_to_rho(sample_u_field, method="unsupported")
|
|
549
|
+
|
|
550
|
+
with pytest.raises(NotImplementedError):
|
|
551
|
+
interpolate_from_v_to_rho(sample_v_field, method="unsupported")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# Test rotate_velocities
|
|
555
|
+
@pytest.fixture
|
|
556
|
+
def sample_velocities_centered():
|
|
557
|
+
"""Create a centered-grid velocity field with random values and grid angle."""
|
|
558
|
+
np.random.seed(42) # For reproducibility
|
|
559
|
+
|
|
560
|
+
eta_rho, xi_rho = 10, 15
|
|
561
|
+
|
|
562
|
+
u = xr.DataArray(
|
|
563
|
+
np.random.rand(eta_rho, xi_rho),
|
|
564
|
+
dims=("eta_rho", "xi_rho"),
|
|
565
|
+
coords={
|
|
566
|
+
"eta_rho": np.arange(eta_rho),
|
|
567
|
+
"xi_rho": np.arange(xi_rho),
|
|
568
|
+
},
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
v = xr.DataArray(
|
|
572
|
+
np.random.rand(eta_rho, xi_rho),
|
|
573
|
+
dims=("eta_rho", "xi_rho"),
|
|
574
|
+
coords={
|
|
575
|
+
"eta_rho": np.arange(eta_rho),
|
|
576
|
+
"xi_rho": np.arange(xi_rho),
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
angle = xr.DataArray(
|
|
581
|
+
np.random.rand(eta_rho, xi_rho) * np.pi / 2
|
|
582
|
+
- np.pi / 4, # random angles in [-45°, 45°]
|
|
583
|
+
dims=("eta_rho", "xi_rho"),
|
|
584
|
+
coords={
|
|
585
|
+
"eta_rho": np.arange(eta_rho),
|
|
586
|
+
"xi_rho": np.arange(xi_rho),
|
|
587
|
+
},
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
return u, v, angle
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_rotate_velocities_roundtrip(sample_velocities_centered):
|
|
594
|
+
"""Test rotation to grid and back recovers original velocities."""
|
|
595
|
+
u, v, angle = sample_velocities_centered
|
|
596
|
+
|
|
597
|
+
# Rotate forward: lat-lon → model grid
|
|
598
|
+
u_rot, v_rot = rotate_velocities(
|
|
599
|
+
u, v, angle, interpolate_before=False, interpolate_after=False
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Rotate backward: model grid → lat-lon
|
|
603
|
+
u_back, v_back = rotate_velocities(
|
|
604
|
+
u_rot, v_rot, -angle, interpolate_before=False, interpolate_after=False
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
np.testing.assert_allclose(u.values, u_back.values)
|
|
608
|
+
np.testing.assert_allclose(v.values, v_back.values)
|
roms_tools/utils.py
CHANGED
|
@@ -477,96 +477,199 @@ def load_data(
|
|
|
477
477
|
return ds
|
|
478
478
|
|
|
479
479
|
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
480
|
+
def _interpolate_generic(
|
|
481
|
+
field: xr.DataArray,
|
|
482
|
+
dim_in: str,
|
|
483
|
+
dim_out: str,
|
|
484
|
+
method: str = "additive",
|
|
485
|
+
drop_coords: Sequence[str] | None = None,
|
|
486
|
+
pad_end: bool = False,
|
|
487
|
+
) -> xr.DataArray:
|
|
488
|
+
"""
|
|
489
|
+
Generic interpolation along one horizontal dimension.
|
|
488
490
|
|
|
489
491
|
Parameters
|
|
490
492
|
----------
|
|
491
493
|
field : xr.DataArray
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
494
|
+
Input array to interpolate.
|
|
495
|
+
dim_in : str
|
|
496
|
+
Dimension along which to interpolate (e.g., "xi_rho").
|
|
497
|
+
dim_out : str
|
|
498
|
+
New dimension name after interpolation (e.g., "xi_u").
|
|
499
|
+
method : str, default "additive"
|
|
500
|
+
Interpolation method:
|
|
501
|
+
- "additive": average adjacent points
|
|
502
|
+
- "multiplicative": multiply adjacent points (useful for masks)
|
|
503
|
+
drop_coords : Sequence[str] or None, optional
|
|
504
|
+
Coordinate variables to drop (e.g., ["lat_rho", "lon_rho"]).
|
|
505
|
+
pad_end : bool, default False
|
|
506
|
+
Whether to pad the last point with NaN (useful when interpolating back to rho grid).
|
|
500
507
|
|
|
501
508
|
Returns
|
|
502
509
|
-------
|
|
503
|
-
|
|
504
|
-
|
|
510
|
+
xr.DataArray
|
|
511
|
+
Interpolated array with dimension `dim_out`.
|
|
505
512
|
"""
|
|
506
|
-
if
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
if not isinstance(field, xr.DataArray):
|
|
514
|
+
raise TypeError(
|
|
515
|
+
"_interpolate_generic expects an xarray.DataArray, "
|
|
516
|
+
f"got {type(field).__name__}"
|
|
509
517
|
)
|
|
518
|
+
|
|
519
|
+
if drop_coords:
|
|
520
|
+
for coord in drop_coords:
|
|
521
|
+
if coord in field.coords:
|
|
522
|
+
field = field.drop_vars(coord)
|
|
523
|
+
|
|
524
|
+
if method == "additive":
|
|
525
|
+
interp = 0.5 * (field + field.shift(**{dim_in: 1}))
|
|
510
526
|
elif method == "multiplicative":
|
|
511
|
-
|
|
527
|
+
interp = field * field.shift(**{dim_in: 1})
|
|
512
528
|
else:
|
|
513
529
|
raise NotImplementedError(f"Unsupported method '{method}' specified.")
|
|
514
530
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
531
|
+
if pad_end:
|
|
532
|
+
pad_shape = {d: field.sizes[d] for d in field.dims}
|
|
533
|
+
pad_shape[dim_in] = 1
|
|
534
|
+
pad = xr.DataArray(
|
|
535
|
+
np.nan * np.ones(tuple(pad_shape[d] for d in field.dims)),
|
|
536
|
+
dims=field.dims,
|
|
537
|
+
)
|
|
538
|
+
interp = xr.concat([interp, pad], dim=dim_in)
|
|
539
|
+
else:
|
|
540
|
+
interp = interp.isel({dim_in: slice(1, None)})
|
|
541
|
+
|
|
542
|
+
interp = interp.swap_dims({dim_in: dim_out})
|
|
543
|
+
|
|
544
|
+
return interp
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def interpolate_from_rho_to_u(
|
|
548
|
+
field: xr.DataArray, method: str = "additive"
|
|
549
|
+
) -> xr.DataArray:
|
|
550
|
+
"""Interpolate a field from rho points to u points (xi direction)."""
|
|
551
|
+
return _interpolate_generic(
|
|
552
|
+
field,
|
|
553
|
+
dim_in="xi_rho",
|
|
554
|
+
dim_out="xi_u",
|
|
555
|
+
method=method,
|
|
556
|
+
drop_coords=["lat_rho", "lon_rho", "eta_rho", "xi_rho"],
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def interpolate_from_rho_to_v(
|
|
561
|
+
field: xr.DataArray, method: str = "additive"
|
|
562
|
+
) -> xr.DataArray:
|
|
563
|
+
"""Interpolate a field from rho points to v points (eta direction)."""
|
|
564
|
+
return _interpolate_generic(
|
|
565
|
+
field,
|
|
566
|
+
dim_in="eta_rho",
|
|
567
|
+
dim_out="eta_v",
|
|
568
|
+
method=method,
|
|
569
|
+
drop_coords=["lat_rho", "lon_rho", "eta_rho", "xi_rho"],
|
|
570
|
+
)
|
|
571
|
+
|
|
519
572
|
|
|
520
|
-
|
|
573
|
+
def interpolate_from_u_to_rho(
|
|
574
|
+
field: xr.DataArray, method: str = "additive"
|
|
575
|
+
) -> xr.DataArray:
|
|
576
|
+
"""Interpolate a field from u points back to rho points (xi direction)."""
|
|
577
|
+
return _interpolate_generic(
|
|
578
|
+
field,
|
|
579
|
+
dim_in="xi_u",
|
|
580
|
+
dim_out="xi_rho",
|
|
581
|
+
method=method,
|
|
582
|
+
drop_coords=["lat_u", "lon_u", "eta_rho", "xi_u"],
|
|
583
|
+
pad_end=True,
|
|
584
|
+
)
|
|
521
585
|
|
|
522
|
-
return field_interpolated
|
|
523
586
|
|
|
587
|
+
def interpolate_from_v_to_rho(
|
|
588
|
+
field: xr.DataArray, method: str = "additive"
|
|
589
|
+
) -> xr.DataArray:
|
|
590
|
+
"""Interpolate a field from v points back to rho points (eta direction)."""
|
|
591
|
+
return _interpolate_generic(
|
|
592
|
+
field,
|
|
593
|
+
dim_in="eta_v",
|
|
594
|
+
dim_out="eta_rho",
|
|
595
|
+
method=method,
|
|
596
|
+
drop_coords=["lat_v", "lon_v", "eta_v", "xi_rho"],
|
|
597
|
+
pad_end=True,
|
|
598
|
+
)
|
|
524
599
|
|
|
525
|
-
def interpolate_from_rho_to_v(field, method="additive"):
|
|
526
|
-
"""Interpolates the given field from rho points to v points.
|
|
527
600
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
def rotate_velocities(
|
|
602
|
+
u: xr.DataArray,
|
|
603
|
+
v: xr.DataArray,
|
|
604
|
+
angle: xr.DataArray,
|
|
605
|
+
interpolate_before: bool = False,
|
|
606
|
+
interpolate_after: bool = False,
|
|
607
|
+
) -> tuple[xr.DataArray, xr.DataArray]:
|
|
608
|
+
"""
|
|
609
|
+
Rotate horizontal velocity components to align with a rotated grid.
|
|
610
|
+
|
|
611
|
+
This function rotates zonal (u) and meridional (v) velocity components
|
|
612
|
+
using a grid angle field. It can be used to:
|
|
613
|
+
|
|
614
|
+
1. Rotate model velocities from the ROMS model grid to a lat-lon reference frame.
|
|
615
|
+
2. Rotate lat-lon velocities onto the ROMS model grid.
|
|
616
|
+
|
|
617
|
+
Optionally, velocities can be interpolated between staggered C-grid
|
|
618
|
+
locations (u-, v-, and rho-points) before and/or after rotation.
|
|
533
619
|
|
|
534
620
|
Parameters
|
|
535
621
|
----------
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
622
|
+
u : xarray.DataArray
|
|
623
|
+
Zonal (east-west) velocity component defined on u-points.
|
|
624
|
+
v : xarray.DataArray
|
|
625
|
+
Meridional (north-south) velocity component defined on v-points.
|
|
626
|
+
angle : xarray.DataArray
|
|
627
|
+
Grid orientation angle in radians, defined at rho-points. This is the
|
|
628
|
+
ROMS grid angle: the angle between the model xi-direction and true east.
|
|
629
|
+
Positive values indicate that the model grid is rotated counterclockwise
|
|
630
|
+
relative to east (which is mathematically equivalent to rotating velocity
|
|
631
|
+
vectors clockwise).
|
|
632
|
+
The rotation transforms velocity components between earth-relative
|
|
633
|
+
(east/north) and grid-relative (xi/eta) coordinates. To reverse the
|
|
634
|
+
transformation (e.g., model → lat-lon), provide ``-angle``.
|
|
635
|
+
interpolate_before : bool, optional
|
|
636
|
+
If True, interpolate ``u`` and ``v`` to rho-points before rotation.
|
|
637
|
+
Default is False.
|
|
638
|
+
interpolate_after : bool, optional
|
|
639
|
+
If True, interpolate the rotated velocities back to u- and v-points
|
|
640
|
+
after rotation. Default is True.
|
|
545
641
|
|
|
546
642
|
Returns
|
|
547
643
|
-------
|
|
548
|
-
|
|
549
|
-
|
|
644
|
+
u_rot, v_rot : tuple of xarray.DataArray
|
|
645
|
+
Rotated velocity components. If ``interpolate_after`` is True, ``u_rot``
|
|
646
|
+
is defined on u-points and ``v_rot`` on v-points; otherwise, both are
|
|
647
|
+
defined at rho-points.
|
|
648
|
+
|
|
649
|
+
Notes
|
|
650
|
+
-----
|
|
651
|
+
The rotation follows the standard ROMS convention:
|
|
652
|
+
- ``u_rot = u * cos(angle) + v * sin(angle)``
|
|
653
|
+
- ``v_rot = v * cos(angle) - u * sin(angle)``
|
|
654
|
+
This function is versatile and can be used for both directions of rotation:
|
|
655
|
+
- Lat-lon → model grid: provide the grid angle.
|
|
656
|
+
- Model grid → lat-lon: provide the negative of the grid angle.
|
|
550
657
|
"""
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
)
|
|
555
|
-
elif method == "multiplicative":
|
|
556
|
-
field_interpolated = (field * field.shift(eta_rho=1)).isel(
|
|
557
|
-
eta_rho=slice(1, None)
|
|
558
|
-
)
|
|
559
|
-
else:
|
|
560
|
-
raise NotImplementedError(f"Unsupported method '{method}' specified.")
|
|
658
|
+
# Interpolate to rho-points
|
|
659
|
+
if interpolate_before:
|
|
660
|
+
u = interpolate_from_u_to_rho(u)
|
|
661
|
+
v = interpolate_from_v_to_rho(v)
|
|
561
662
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
field_interpolated = field_interpolated.drop_vars(var)
|
|
663
|
+
# Rotate velocities to grid orientation
|
|
664
|
+
u_rot = u * np.cos(angle) + v * np.sin(angle)
|
|
665
|
+
v_rot = v * np.cos(angle) - u * np.sin(angle)
|
|
566
666
|
|
|
567
|
-
|
|
667
|
+
# Interpolate to u- and v-points
|
|
668
|
+
if interpolate_after:
|
|
669
|
+
u_rot = interpolate_from_rho_to_u(u_rot)
|
|
670
|
+
v_rot = interpolate_from_rho_to_v(v_rot)
|
|
568
671
|
|
|
569
|
-
return
|
|
672
|
+
return u_rot, v_rot
|
|
570
673
|
|
|
571
674
|
|
|
572
675
|
def transpose_dimensions(da: xr.DataArray) -> xr.DataArray:
|
|
@@ -1036,13 +1139,13 @@ def get_pkg_error_msg(purpose: str, package_name: str, option_name: str) -> str:
|
|
|
1036
1139
|
|
|
1037
1140
|
def wrap_longitudes(ds: xr.Dataset, straddle: bool) -> xr.Dataset:
|
|
1038
1141
|
"""
|
|
1039
|
-
Safely adjust longitude
|
|
1040
|
-
the dateline. Only modifies longitude-like
|
|
1142
|
+
Safely adjust longitude coordinates for datasets that may or may not cross
|
|
1143
|
+
the dateline. Only modifies longitude-like coordinates that are present.
|
|
1041
1144
|
|
|
1042
1145
|
Parameters
|
|
1043
1146
|
----------
|
|
1044
1147
|
ds : xr.Dataset
|
|
1045
|
-
Dataset containing longitude
|
|
1148
|
+
Dataset containing longitude coordinates (e.g., lon_rho, lon_u, lon_v).
|
|
1046
1149
|
straddle : bool
|
|
1047
1150
|
- True: force longitudes into [-180, 180]
|
|
1048
1151
|
- False: force longitudes into [0, 360]
|
|
@@ -1050,15 +1153,15 @@ def wrap_longitudes(ds: xr.Dataset, straddle: bool) -> xr.Dataset:
|
|
|
1050
1153
|
Returns
|
|
1051
1154
|
-------
|
|
1052
1155
|
xr.Dataset
|
|
1053
|
-
A new dataset with adjusted longitude
|
|
1156
|
+
A new dataset with adjusted longitude coordinates.
|
|
1054
1157
|
"""
|
|
1055
|
-
|
|
1158
|
+
lon_coords = ["lon_rho", "lon_u", "lon_v"]
|
|
1056
1159
|
|
|
1057
|
-
for
|
|
1058
|
-
if
|
|
1160
|
+
for lon_name in lon_coords:
|
|
1161
|
+
if lon_name not in ds.coords:
|
|
1059
1162
|
continue # skip missing coordinate
|
|
1060
1163
|
|
|
1061
|
-
lon = ds[
|
|
1164
|
+
lon = ds.coords[lon_name]
|
|
1062
1165
|
|
|
1063
1166
|
if straddle:
|
|
1064
1167
|
# wrap into [-180, 180]
|
|
@@ -1068,8 +1171,10 @@ def wrap_longitudes(ds: xr.Dataset, straddle: bool) -> xr.Dataset:
|
|
|
1068
1171
|
lon_wrapped = xr.where(lon < 0, lon + 360, lon)
|
|
1069
1172
|
|
|
1070
1173
|
# preserve attributes
|
|
1071
|
-
lon_wrapped.attrs
|
|
1072
|
-
|
|
1174
|
+
lon_wrapped.attrs = lon.attrs.copy()
|
|
1175
|
+
|
|
1176
|
+
# reassign explicitly as a coordinate
|
|
1177
|
+
ds = ds.assign_coords({lon_name: lon_wrapped})
|
|
1073
1178
|
|
|
1074
1179
|
return ds
|
|
1075
1180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roms-tools
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Tools for running and analysing UCLA-ROMS simulations
|
|
5
5
|
Author-email: Nora Loose <nora.loose@gmail.com>, Thomas Nicholas <tom@cworthy.org>, Scott Eilerman <scott.eilerman@cworthy.org>
|
|
6
6
|
License: Apache-2
|
|
@@ -64,7 +64,7 @@ Dynamic: license-file
|
|
|
64
64
|
|
|
65
65
|
## Overview
|
|
66
66
|
|
|
67
|
-
A suite of Python tools for setting up and analyzing a [UCLA-ROMS](https://github.com/
|
|
67
|
+
A suite of Python tools for setting up and analyzing a [UCLA-ROMS](https://github.com/CWorthy-ocean/ucla-roms) simulation with or without [MARBL biogeochemistry](https://marbl-ecosys.github.io/versions/latest_release/index.html).
|
|
68
68
|
|
|
69
69
|
## Installation
|
|
70
70
|
|
|
@@ -152,5 +152,4 @@ We also accept contributions in the form of Pull Requests.
|
|
|
152
152
|
|
|
153
153
|
## See also
|
|
154
154
|
|
|
155
|
-
- [ROMS source code](https://github.com/CESR-lab/ucla-roms)
|
|
156
155
|
- [C-Star](https://github.com/CWorthy-ocean/C-Star)
|