roms-tools 3.1.1__py3-none-any.whl → 3.2.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/__init__.py +8 -1
- roms_tools/analysis/cdr_analysis.py +203 -0
- roms_tools/analysis/cdr_ensemble.py +198 -0
- roms_tools/analysis/roms_output.py +80 -46
- roms_tools/data/grids/GLORYS_global_grid.nc +0 -0
- roms_tools/download.py +4 -0
- roms_tools/plot.py +131 -30
- roms_tools/regrid.py +6 -1
- roms_tools/setup/boundary_forcing.py +94 -44
- roms_tools/setup/cdr_forcing.py +123 -15
- roms_tools/setup/cdr_release.py +161 -8
- roms_tools/setup/datasets.py +709 -341
- roms_tools/setup/grid.py +167 -139
- roms_tools/setup/initial_conditions.py +113 -48
- roms_tools/setup/mask.py +63 -7
- roms_tools/setup/nesting.py +67 -42
- roms_tools/setup/river_forcing.py +45 -19
- roms_tools/setup/surface_forcing.py +16 -10
- roms_tools/setup/tides.py +1 -2
- roms_tools/setup/topography.py +4 -4
- roms_tools/setup/utils.py +134 -22
- roms_tools/tests/test_analysis/test_cdr_analysis.py +144 -0
- roms_tools/tests/test_analysis/test_cdr_ensemble.py +202 -0
- roms_tools/tests/test_analysis/test_roms_output.py +61 -3
- roms_tools/tests/test_setup/test_boundary_forcing.py +111 -52
- roms_tools/tests/test_setup/test_cdr_forcing.py +54 -0
- roms_tools/tests/test_setup/test_cdr_release.py +118 -1
- roms_tools/tests/test_setup/test_datasets.py +458 -34
- roms_tools/tests/test_setup/test_grid.py +238 -121
- roms_tools/tests/test_setup/test_initial_conditions.py +94 -41
- roms_tools/tests/test_setup/test_surface_forcing.py +28 -3
- roms_tools/tests/test_setup/test_utils.py +91 -1
- roms_tools/tests/test_setup/test_validation.py +21 -15
- roms_tools/tests/test_setup/utils.py +71 -0
- roms_tools/tests/test_tiling/test_join.py +241 -0
- roms_tools/tests/test_tiling/test_partition.py +45 -0
- roms_tools/tests/test_utils.py +224 -2
- roms_tools/tiling/join.py +189 -0
- roms_tools/tiling/partition.py +44 -30
- roms_tools/utils.py +488 -161
- {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/METADATA +15 -4
- {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/RECORD +45 -37
- {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/WHEEL +0 -0
- {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/licenses/LICENSE +0 -0
- {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/top_level.txt +0 -0
|
@@ -21,6 +21,11 @@ from roms_tools.constants import (
|
|
|
21
21
|
from roms_tools.download import download_test_data
|
|
22
22
|
from roms_tools.setup.topography import _compute_rfactor
|
|
23
23
|
|
|
24
|
+
try:
|
|
25
|
+
import xesmf # type: ignore
|
|
26
|
+
except ImportError:
|
|
27
|
+
xesmf = None
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
@pytest.fixture()
|
|
26
31
|
def counter_clockwise_rotated_grid():
|
|
@@ -116,6 +121,33 @@ def grid_that_straddles_180_degree_meridian_with_global_srtm15_data():
|
|
|
116
121
|
return grid
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
@pytest.fixture()
|
|
125
|
+
def grid_with_gshhs_coastlines():
|
|
126
|
+
iceland_fjord_kwargs = {
|
|
127
|
+
"nx": 80,
|
|
128
|
+
"ny": 40,
|
|
129
|
+
"size_x": 40,
|
|
130
|
+
"size_y": 20,
|
|
131
|
+
"center_lon": -21.76,
|
|
132
|
+
"center_lat": 64.325,
|
|
133
|
+
"rot": 0,
|
|
134
|
+
"N": 3,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Make sure all 4 L1 files are downloaded
|
|
138
|
+
_ = download_test_data("GSHHS_l_L1.dbf")
|
|
139
|
+
_ = download_test_data("GSHHS_l_L1.prj")
|
|
140
|
+
_ = download_test_data("GSHHS_l_L1.shx")
|
|
141
|
+
shapefile = download_test_data("GSHHS_l_L1.shp")
|
|
142
|
+
|
|
143
|
+
grid = Grid(
|
|
144
|
+
**iceland_fjord_kwargs,
|
|
145
|
+
mask_shapefile=shapefile,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return grid
|
|
149
|
+
|
|
150
|
+
|
|
119
151
|
def test_grid_creation(grid):
|
|
120
152
|
assert grid.nx == 1
|
|
121
153
|
assert grid.ny == 1
|
|
@@ -170,20 +202,47 @@ def test_coords_relation(grid_fixture, request):
|
|
|
170
202
|
"grid_that_straddles_180_degree_meridian_with_shifted_global_etopo_data",
|
|
171
203
|
"grid_that_straddles_dateline_with_global_srtm15_data",
|
|
172
204
|
"grid_that_straddles_180_degree_meridian_with_global_srtm15_data",
|
|
205
|
+
"grid_with_gshhs_coastlines",
|
|
173
206
|
],
|
|
174
207
|
)
|
|
175
208
|
def test_successful_initialization_with_topography(grid_fixture, request):
|
|
176
209
|
grid = request.getfixturevalue(grid_fixture)
|
|
177
|
-
assert grid is not None
|
|
178
210
|
|
|
211
|
+
expected_attrs = [
|
|
212
|
+
"nx",
|
|
213
|
+
"ny",
|
|
214
|
+
"size_x",
|
|
215
|
+
"size_y",
|
|
216
|
+
"center_lon",
|
|
217
|
+
"center_lat",
|
|
218
|
+
"rot",
|
|
219
|
+
"N",
|
|
220
|
+
"theta_s",
|
|
221
|
+
"theta_b",
|
|
222
|
+
"hc",
|
|
223
|
+
"topography_source",
|
|
224
|
+
"hmin",
|
|
225
|
+
"mask_shapefile",
|
|
226
|
+
"verbose",
|
|
227
|
+
"straddle",
|
|
228
|
+
]
|
|
179
229
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
for attr in expected_attrs:
|
|
231
|
+
assert hasattr(grid, attr), f"Missing attribute: {attr}"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_plot(grid_that_straddles_180_degree_meridian):
|
|
235
|
+
grid_that_straddles_180_degree_meridian.plot(with_dim_names=False)
|
|
236
|
+
grid_that_straddles_180_degree_meridian.plot(with_dim_names=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@pytest.mark.skipif(xesmf is None, reason="xesmf required")
|
|
240
|
+
def test_plot_along_lat_lon(grid_that_straddles_180_degree_meridian):
|
|
241
|
+
grid_that_straddles_180_degree_meridian.plot(lat=61)
|
|
242
|
+
grid_that_straddles_180_degree_meridian.plot(lon=180)
|
|
184
243
|
|
|
185
|
-
|
|
186
|
-
|
|
244
|
+
with pytest.raises(ValueError, match="Specify either `lat` or `lon`, not both"):
|
|
245
|
+
grid_that_straddles_180_degree_meridian.plot(lat=61, lon=180)
|
|
187
246
|
|
|
188
247
|
|
|
189
248
|
def test_save(tmp_path):
|
|
@@ -270,6 +329,7 @@ def test_grid_straddle_crosses_meridian():
|
|
|
270
329
|
"grid",
|
|
271
330
|
"grid_that_straddles_dateline_with_shifted_global_etopo_data",
|
|
272
331
|
"grid_that_straddles_dateline_with_global_srtm15_data",
|
|
332
|
+
"grid_with_gshhs_coastlines",
|
|
273
333
|
],
|
|
274
334
|
)
|
|
275
335
|
def test_roundtrip_netcdf(grid_fixture, tmp_path, request):
|
|
@@ -291,8 +351,8 @@ def test_roundtrip_netcdf(grid_fixture, tmp_path, request):
|
|
|
291
351
|
# Load the grid from the file
|
|
292
352
|
grid_from_file = Grid.from_file(filepath.with_suffix(".nc"))
|
|
293
353
|
|
|
294
|
-
# Assert that the initial grid and the loaded grid are equivalent (including the 'ds' attribute)
|
|
295
354
|
assert grid == grid_from_file
|
|
355
|
+
xr.testing.assert_equal(grid.ds, grid_from_file.ds)
|
|
296
356
|
|
|
297
357
|
# Clean up the .nc file
|
|
298
358
|
(filepath.with_suffix(".nc")).unlink()
|
|
@@ -304,6 +364,7 @@ def test_roundtrip_netcdf(grid_fixture, tmp_path, request):
|
|
|
304
364
|
"grid",
|
|
305
365
|
"grid_that_straddles_dateline_with_shifted_global_etopo_data",
|
|
306
366
|
"grid_that_straddles_dateline_with_global_srtm15_data",
|
|
367
|
+
"grid_with_gshhs_coastlines",
|
|
307
368
|
],
|
|
308
369
|
)
|
|
309
370
|
def test_roundtrip_yaml(grid_fixture, tmp_path, request):
|
|
@@ -322,8 +383,8 @@ def test_roundtrip_yaml(grid_fixture, tmp_path, request):
|
|
|
322
383
|
|
|
323
384
|
grid_from_file = Grid.from_yaml(filepath)
|
|
324
385
|
|
|
325
|
-
# Assert that the initial grid and the loaded grid are equivalent (including the 'ds' attribute)
|
|
326
386
|
assert grid == grid_from_file
|
|
387
|
+
xr.testing.assert_equal(grid.ds, grid_from_file.ds)
|
|
327
388
|
|
|
328
389
|
filepath = Path(filepath)
|
|
329
390
|
filepath.unlink()
|
|
@@ -335,6 +396,7 @@ def test_roundtrip_yaml(grid_fixture, tmp_path, request):
|
|
|
335
396
|
"grid",
|
|
336
397
|
"grid_that_straddles_dateline_with_shifted_global_etopo_data",
|
|
337
398
|
"grid_that_straddles_dateline_with_global_srtm15_data",
|
|
399
|
+
"grid_with_gshhs_coastlines",
|
|
338
400
|
],
|
|
339
401
|
)
|
|
340
402
|
def test_roundtrip_from_file_yaml(grid_fixture, tmp_path, request):
|
|
@@ -349,30 +411,33 @@ def test_roundtrip_from_file_yaml(grid_fixture, tmp_path, request):
|
|
|
349
411
|
filepath_yaml = Path(tmp_path / "test.yaml")
|
|
350
412
|
grid_from_file.to_yaml(filepath_yaml)
|
|
351
413
|
|
|
414
|
+
grid_from_yaml = Grid.from_yaml(filepath_yaml)
|
|
415
|
+
|
|
416
|
+
assert grid_from_yaml == grid
|
|
417
|
+
xr.testing.assert_equal(grid.ds, grid_from_yaml.ds)
|
|
418
|
+
|
|
352
419
|
filepath.unlink()
|
|
353
420
|
filepath_yaml.unlink()
|
|
354
421
|
|
|
355
422
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
hmin=5.0,
|
|
368
|
-
)
|
|
423
|
+
@pytest.mark.parametrize(
|
|
424
|
+
"grid_fixture",
|
|
425
|
+
[
|
|
426
|
+
"grid",
|
|
427
|
+
"grid_that_straddles_dateline_with_shifted_global_etopo_data",
|
|
428
|
+
"grid_that_straddles_dateline_with_global_srtm15_data",
|
|
429
|
+
"grid_with_gshhs_coastlines",
|
|
430
|
+
],
|
|
431
|
+
)
|
|
432
|
+
def test_files_have_same_hash(grid_fixture, tmp_path, request):
|
|
433
|
+
grid = request.getfixturevalue(grid_fixture)
|
|
369
434
|
|
|
370
435
|
yaml_filepath = tmp_path / "test_yaml"
|
|
371
436
|
filepath1 = tmp_path / "test1.nc"
|
|
372
437
|
filepath2 = tmp_path / "test2.nc"
|
|
373
438
|
|
|
374
|
-
|
|
375
|
-
|
|
439
|
+
grid.to_yaml(yaml_filepath)
|
|
440
|
+
grid.save(filepath1)
|
|
376
441
|
|
|
377
442
|
grid_from_file = Grid.from_yaml(yaml_filepath)
|
|
378
443
|
grid_from_file.save(filepath2)
|
|
@@ -495,6 +560,9 @@ def test_from_yaml_version_mismatch(tmp_path, caplog):
|
|
|
495
560
|
yaml_filepath.unlink()
|
|
496
561
|
|
|
497
562
|
|
|
563
|
+
# Vertical coordinate tests
|
|
564
|
+
|
|
565
|
+
|
|
498
566
|
def test_invalid_theta_s_value():
|
|
499
567
|
"""Test the validation of the theta_s value."""
|
|
500
568
|
with pytest.raises(ValueError):
|
|
@@ -598,6 +666,152 @@ def test_plot_vertical_coordinate():
|
|
|
598
666
|
grid.plot_vertical_coordinate(eta=-1, xi=0, s=-1)
|
|
599
667
|
|
|
600
668
|
|
|
669
|
+
# Topography tests
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def test_enclosed_regions():
|
|
673
|
+
"""Test that there are only two connected regions, one dry and one wet."""
|
|
674
|
+
grid = Grid(
|
|
675
|
+
nx=100,
|
|
676
|
+
ny=100,
|
|
677
|
+
size_x=1800,
|
|
678
|
+
size_y=2400,
|
|
679
|
+
center_lon=30,
|
|
680
|
+
center_lat=61,
|
|
681
|
+
rot=20,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
reg, nreg = label(grid.ds.mask_rho)
|
|
685
|
+
npt.assert_equal(nreg, 2)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def test_rmax_criterion():
|
|
689
|
+
grid = Grid(
|
|
690
|
+
nx=100,
|
|
691
|
+
ny=100,
|
|
692
|
+
size_x=1800,
|
|
693
|
+
size_y=2400,
|
|
694
|
+
center_lon=30,
|
|
695
|
+
center_lat=61,
|
|
696
|
+
rot=20,
|
|
697
|
+
)
|
|
698
|
+
r_eta, r_xi = _compute_rfactor(grid.ds.h)
|
|
699
|
+
rmax0 = np.max([r_eta.max(), r_xi.max()])
|
|
700
|
+
npt.assert_array_less(rmax0, 0.2)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def test_hmin_criterion_and_update_topography():
|
|
704
|
+
grid = Grid(
|
|
705
|
+
nx=100,
|
|
706
|
+
ny=100,
|
|
707
|
+
size_x=1800,
|
|
708
|
+
size_y=2400,
|
|
709
|
+
center_lon=30,
|
|
710
|
+
center_lat=61,
|
|
711
|
+
rot=20,
|
|
712
|
+
hmin=5.0,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
assert grid.hmin == 5.0
|
|
716
|
+
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
717
|
+
|
|
718
|
+
grid.update_topography(hmin=10.0)
|
|
719
|
+
|
|
720
|
+
assert grid.hmin == 10.0
|
|
721
|
+
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
722
|
+
|
|
723
|
+
# this should not do anything
|
|
724
|
+
grid.update_topography()
|
|
725
|
+
|
|
726
|
+
assert grid.hmin == 10.0
|
|
727
|
+
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# Mask tests
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_update_mask():
|
|
734
|
+
iceland_fjord_kwargs = {
|
|
735
|
+
"nx": 80,
|
|
736
|
+
"ny": 40,
|
|
737
|
+
"size_x": 40,
|
|
738
|
+
"size_y": 20,
|
|
739
|
+
"center_lon": -21.76,
|
|
740
|
+
"center_lat": 64.325,
|
|
741
|
+
"rot": 0,
|
|
742
|
+
"N": 3,
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
# Make sure all 4 L1 files are downloaded
|
|
746
|
+
_ = download_test_data("GSHHS_l_L1.dbf")
|
|
747
|
+
_ = download_test_data("GSHHS_l_L1.prj")
|
|
748
|
+
_ = download_test_data("GSHHS_l_L1.shx")
|
|
749
|
+
shapefile = download_test_data("GSHHS_l_L1.shp")
|
|
750
|
+
|
|
751
|
+
grid = Grid(
|
|
752
|
+
**iceland_fjord_kwargs,
|
|
753
|
+
mask_shapefile=shapefile,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
assert grid.mask_shapefile == shapefile
|
|
757
|
+
|
|
758
|
+
# Save original mask
|
|
759
|
+
mask_orig = grid.ds.mask_rho.copy()
|
|
760
|
+
|
|
761
|
+
# Update mask (switches to Natural Earth)
|
|
762
|
+
grid.update_mask()
|
|
763
|
+
|
|
764
|
+
assert grid.mask_shapefile is None
|
|
765
|
+
|
|
766
|
+
# New mask after update
|
|
767
|
+
mask_new = grid.ds.mask_rho.copy()
|
|
768
|
+
|
|
769
|
+
assert abs(mask_new - mask_orig).max() == 1, (
|
|
770
|
+
"Mask should change after update_mask()"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# Boundary tests
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_mask_topography_boundary():
|
|
778
|
+
"""Test that the mask and topography along the grid boundaries (north, south, east,
|
|
779
|
+
west) are identical to the adjacent inland cells.
|
|
780
|
+
"""
|
|
781
|
+
# Create a grid with some land along the northern boundary
|
|
782
|
+
grid = Grid(
|
|
783
|
+
nx=10, ny=10, size_x=1000, size_y=1000, center_lon=-20, center_lat=60, rot=0
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# Toopography
|
|
787
|
+
np.testing.assert_array_equal(
|
|
788
|
+
grid.ds.h.isel(eta_rho=0).data, grid.ds.h.isel(eta_rho=1).data
|
|
789
|
+
)
|
|
790
|
+
np.testing.assert_array_equal(
|
|
791
|
+
grid.ds.h.isel(eta_rho=-1).data, grid.ds.h.isel(eta_rho=-2).data
|
|
792
|
+
)
|
|
793
|
+
np.testing.assert_array_equal(
|
|
794
|
+
grid.ds.h.isel(xi_rho=0).data, grid.ds.h.isel(xi_rho=1).data
|
|
795
|
+
)
|
|
796
|
+
np.testing.assert_array_equal(
|
|
797
|
+
grid.ds.h.isel(xi_rho=-1).data, grid.ds.h.isel(xi_rho=-2).data
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Mask
|
|
801
|
+
np.testing.assert_array_equal(
|
|
802
|
+
grid.ds.mask_rho.isel(eta_rho=0).data, grid.ds.mask_rho.isel(eta_rho=1).data
|
|
803
|
+
)
|
|
804
|
+
np.testing.assert_array_equal(
|
|
805
|
+
grid.ds.mask_rho.isel(eta_rho=-1).data, grid.ds.mask_rho.isel(eta_rho=-2).data
|
|
806
|
+
)
|
|
807
|
+
np.testing.assert_array_equal(
|
|
808
|
+
grid.ds.mask_rho.isel(xi_rho=0).data, grid.ds.mask_rho.isel(xi_rho=1).data
|
|
809
|
+
)
|
|
810
|
+
np.testing.assert_array_equal(
|
|
811
|
+
grid.ds.mask_rho.isel(xi_rho=-1).data, grid.ds.mask_rho.isel(xi_rho=-2).data
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
601
815
|
# More Grid.from_file() tests
|
|
602
816
|
|
|
603
817
|
|
|
@@ -713,100 +927,3 @@ def test_from_file_partial_parameters_raises_error(grid, tmp_path):
|
|
|
713
927
|
|
|
714
928
|
with pytest.raises(ValueError, match="must provide all of"):
|
|
715
929
|
Grid.from_file(path, theta_s=5.0)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# Topography tests
|
|
719
|
-
def test_enclosed_regions():
|
|
720
|
-
"""Test that there are only two connected regions, one dry and one wet."""
|
|
721
|
-
grid = Grid(
|
|
722
|
-
nx=100,
|
|
723
|
-
ny=100,
|
|
724
|
-
size_x=1800,
|
|
725
|
-
size_y=2400,
|
|
726
|
-
center_lon=30,
|
|
727
|
-
center_lat=61,
|
|
728
|
-
rot=20,
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
reg, nreg = label(grid.ds.mask_rho)
|
|
732
|
-
npt.assert_equal(nreg, 2)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
def test_rmax_criterion():
|
|
736
|
-
grid = Grid(
|
|
737
|
-
nx=100,
|
|
738
|
-
ny=100,
|
|
739
|
-
size_x=1800,
|
|
740
|
-
size_y=2400,
|
|
741
|
-
center_lon=30,
|
|
742
|
-
center_lat=61,
|
|
743
|
-
rot=20,
|
|
744
|
-
)
|
|
745
|
-
r_eta, r_xi = _compute_rfactor(grid.ds.h)
|
|
746
|
-
rmax0 = np.max([r_eta.max(), r_xi.max()])
|
|
747
|
-
npt.assert_array_less(rmax0, 0.2)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
def test_hmin_criterion():
|
|
751
|
-
grid = Grid(
|
|
752
|
-
nx=100,
|
|
753
|
-
ny=100,
|
|
754
|
-
size_x=1800,
|
|
755
|
-
size_y=2400,
|
|
756
|
-
center_lon=30,
|
|
757
|
-
center_lat=61,
|
|
758
|
-
rot=20,
|
|
759
|
-
hmin=5.0,
|
|
760
|
-
)
|
|
761
|
-
|
|
762
|
-
assert grid.hmin == 5.0
|
|
763
|
-
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
764
|
-
|
|
765
|
-
grid.update_topography(hmin=10.0)
|
|
766
|
-
|
|
767
|
-
assert grid.hmin == 10.0
|
|
768
|
-
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
769
|
-
|
|
770
|
-
# this should not do anything
|
|
771
|
-
grid.update_topography()
|
|
772
|
-
|
|
773
|
-
assert grid.hmin == 10.0
|
|
774
|
-
assert np.less_equal(grid.hmin, grid.ds.h.min())
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
def test_mask_topography_boundary():
|
|
778
|
-
"""Test that the mask and topography along the grid boundaries (north, south, east,
|
|
779
|
-
west) are identical to the adjacent inland cells.
|
|
780
|
-
"""
|
|
781
|
-
# Create a grid with some land along the northern boundary
|
|
782
|
-
grid = Grid(
|
|
783
|
-
nx=10, ny=10, size_x=1000, size_y=1000, center_lon=-20, center_lat=60, rot=0
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
# Toopography
|
|
787
|
-
np.testing.assert_array_equal(
|
|
788
|
-
grid.ds.h.isel(eta_rho=0).data, grid.ds.h.isel(eta_rho=1).data
|
|
789
|
-
)
|
|
790
|
-
np.testing.assert_array_equal(
|
|
791
|
-
grid.ds.h.isel(eta_rho=-1).data, grid.ds.h.isel(eta_rho=-2).data
|
|
792
|
-
)
|
|
793
|
-
np.testing.assert_array_equal(
|
|
794
|
-
grid.ds.h.isel(xi_rho=0).data, grid.ds.h.isel(xi_rho=1).data
|
|
795
|
-
)
|
|
796
|
-
np.testing.assert_array_equal(
|
|
797
|
-
grid.ds.h.isel(xi_rho=-1).data, grid.ds.h.isel(xi_rho=-2).data
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
# Mask
|
|
801
|
-
np.testing.assert_array_equal(
|
|
802
|
-
grid.ds.mask_rho.isel(eta_rho=0).data, grid.ds.mask_rho.isel(eta_rho=1).data
|
|
803
|
-
)
|
|
804
|
-
np.testing.assert_array_equal(
|
|
805
|
-
grid.ds.mask_rho.isel(eta_rho=-1).data, grid.ds.mask_rho.isel(eta_rho=-2).data
|
|
806
|
-
)
|
|
807
|
-
np.testing.assert_array_equal(
|
|
808
|
-
grid.ds.mask_rho.isel(xi_rho=0).data, grid.ds.mask_rho.isel(xi_rho=1).data
|
|
809
|
-
)
|
|
810
|
-
np.testing.assert_array_equal(
|
|
811
|
-
grid.ds.mask_rho.isel(xi_rho=-1).data, grid.ds.mask_rho.isel(xi_rho=-2).data
|
|
812
|
-
)
|
|
@@ -11,7 +11,35 @@ import xarray as xr
|
|
|
11
11
|
from conftest import calculate_data_hash
|
|
12
12
|
from roms_tools import Grid, InitialConditions
|
|
13
13
|
from roms_tools.download import download_test_data
|
|
14
|
-
from roms_tools.setup.datasets import
|
|
14
|
+
from roms_tools.setup.datasets import (
|
|
15
|
+
CESMBGCDataset,
|
|
16
|
+
UnifiedBGCDataset,
|
|
17
|
+
)
|
|
18
|
+
from roms_tools.tests.test_setup.utils import download_regional_and_bigger
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import copernicusmarine # type: ignore
|
|
22
|
+
except ImportError:
|
|
23
|
+
copernicusmarine = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def example_grid():
|
|
28
|
+
grid = Grid(
|
|
29
|
+
nx=2,
|
|
30
|
+
ny=2,
|
|
31
|
+
size_x=500,
|
|
32
|
+
size_y=1000,
|
|
33
|
+
center_lon=0,
|
|
34
|
+
center_lat=55,
|
|
35
|
+
rot=10,
|
|
36
|
+
N=3, # number of vertical levels
|
|
37
|
+
theta_s=5.0, # surface control parameter
|
|
38
|
+
theta_b=2.0, # bottom control parameter
|
|
39
|
+
hc=250.0, # critical depth
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return grid
|
|
15
43
|
|
|
16
44
|
|
|
17
45
|
@pytest.mark.parametrize(
|
|
@@ -25,7 +53,9 @@ from roms_tools.setup.datasets import CESMBGCDataset, UnifiedBGCDataset
|
|
|
25
53
|
"initial_conditions_with_unified_bgc_from_climatology",
|
|
26
54
|
],
|
|
27
55
|
)
|
|
28
|
-
def
|
|
56
|
+
def test_initial_conditions_creation_with_nondefault_glorys_dataset(
|
|
57
|
+
ic_fixture, request
|
|
58
|
+
):
|
|
29
59
|
"""Test the creation of the InitialConditions object."""
|
|
30
60
|
ic = request.getfixturevalue(ic_fixture)
|
|
31
61
|
|
|
@@ -37,12 +67,65 @@ def test_initial_conditions_creation(ic_fixture, request):
|
|
|
37
67
|
}
|
|
38
68
|
assert hasattr(ic.ds, "adjust_depth_for_sea_surface_height")
|
|
39
69
|
assert isinstance(ic.ds, xr.Dataset)
|
|
40
|
-
assert "temp" in ic.ds
|
|
41
|
-
assert "salt" in ic.ds
|
|
42
|
-
assert "u" in ic.ds
|
|
43
|
-
assert "v" in ic.ds
|
|
44
|
-
assert "zeta" in ic.ds
|
|
45
70
|
assert ic.ds.coords["ocean_time"].attrs["units"] == "seconds"
|
|
71
|
+
expected_vars = {"temp", "salt", "u", "v", "zeta", "ubar", "vbar"}
|
|
72
|
+
assert set(ic.ds.data_vars).issuperset(expected_vars)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.stream
|
|
76
|
+
@pytest.mark.use_copernicus
|
|
77
|
+
@pytest.mark.use_dask
|
|
78
|
+
def test_initial_conditions_creation_with_default_glorys_dataset(example_grid: Grid):
|
|
79
|
+
"""Verify the default GLORYS dataset is loaded when a path is not provided."""
|
|
80
|
+
ic = InitialConditions(
|
|
81
|
+
grid=example_grid,
|
|
82
|
+
ini_time=datetime(2021, 6, 29),
|
|
83
|
+
source={"name": "GLORYS"},
|
|
84
|
+
use_dask=True,
|
|
85
|
+
bypass_validation=True,
|
|
86
|
+
)
|
|
87
|
+
expected_vars = {"temp", "salt", "u", "v", "zeta", "ubar", "vbar"}
|
|
88
|
+
assert set(ic.ds.data_vars).issuperset(expected_vars)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.use_copernicus
|
|
92
|
+
@pytest.mark.skipif(copernicusmarine is None, reason="copernicusmarine required")
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
"grid_fixture",
|
|
95
|
+
[
|
|
96
|
+
"tiny_grid_that_straddles_dateline",
|
|
97
|
+
"tiny_grid_that_straddles_180_degree_meridian",
|
|
98
|
+
"tiny_rotated_grid",
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
def test_invariance_to_get_glorys_bounds(tmp_path, grid_fixture, use_dask, request):
|
|
102
|
+
ini_time = datetime(2012, 1, 1)
|
|
103
|
+
grid = request.getfixturevalue(grid_fixture)
|
|
104
|
+
regional_file, bigger_regional_file = download_regional_and_bigger(
|
|
105
|
+
tmp_path, grid, ini_time
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
ic_from_regional = InitialConditions(
|
|
109
|
+
grid=grid,
|
|
110
|
+
source={"name": "GLORYS", "path": str(regional_file)},
|
|
111
|
+
ini_time=ini_time,
|
|
112
|
+
use_dask=use_dask,
|
|
113
|
+
)
|
|
114
|
+
ic_from_bigger_regional = InitialConditions(
|
|
115
|
+
grid=grid,
|
|
116
|
+
source={"name": "GLORYS", "path": str(bigger_regional_file)},
|
|
117
|
+
ini_time=ini_time,
|
|
118
|
+
use_dask=use_dask,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Use assert_allclose instead of equals: necessary for grids that straddle the 180° meridian.
|
|
122
|
+
# Copernicus returns data on [-180, 180] by default, but if you request a range
|
|
123
|
+
# like [170, 190], it remaps longitudes. That remapping introduces tiny floating
|
|
124
|
+
# point differences in the longitude coordinate, which will then propagate into further differences once you do regridding.
|
|
125
|
+
# Need to adjust the tolerances for these grids that straddle the 180° meridian.
|
|
126
|
+
xr.testing.assert_allclose(
|
|
127
|
+
ic_from_bigger_regional.ds, ic_from_regional.ds, rtol=1e-4, atol=1e-5
|
|
128
|
+
)
|
|
46
129
|
|
|
47
130
|
|
|
48
131
|
def test_initial_conditions_creation_with_duplicates(use_dask: bool) -> None:
|
|
@@ -67,6 +150,7 @@ def test_initial_conditions_creation_with_duplicates(use_dask: bool) -> None:
|
|
|
67
150
|
grid=grid,
|
|
68
151
|
ini_time=datetime(2012, 1, 1),
|
|
69
152
|
source={"path": [fname1, fname2], "name": "GLORYS"},
|
|
153
|
+
allow_flex_time=True,
|
|
70
154
|
use_dask=use_dask,
|
|
71
155
|
)
|
|
72
156
|
|
|
@@ -74,6 +158,7 @@ def test_initial_conditions_creation_with_duplicates(use_dask: bool) -> None:
|
|
|
74
158
|
grid=grid,
|
|
75
159
|
ini_time=datetime(2012, 1, 1),
|
|
76
160
|
source={"path": [fname1, fname1, fname2], "name": "GLORYS"},
|
|
161
|
+
allow_flex_time=True,
|
|
77
162
|
use_dask=use_dask,
|
|
78
163
|
)
|
|
79
164
|
|
|
@@ -134,28 +219,9 @@ def test_initial_condition_creation_with_bgc(ic_fixture, request):
|
|
|
134
219
|
assert var in ic.ds
|
|
135
220
|
|
|
136
221
|
|
|
137
|
-
@pytest.fixture
|
|
138
|
-
def example_grid():
|
|
139
|
-
grid = Grid(
|
|
140
|
-
nx=2,
|
|
141
|
-
ny=2,
|
|
142
|
-
size_x=500,
|
|
143
|
-
size_y=1000,
|
|
144
|
-
center_lon=0,
|
|
145
|
-
center_lat=55,
|
|
146
|
-
rot=10,
|
|
147
|
-
N=3, # number of vertical levels
|
|
148
|
-
theta_s=5.0, # surface control parameter
|
|
149
|
-
theta_b=2.0, # bottom control parameter
|
|
150
|
-
hc=250.0, # critical depth
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
return grid
|
|
154
|
-
|
|
155
|
-
|
|
156
222
|
# Test initialization with missing 'name' in source
|
|
157
223
|
def test_initial_conditions_missing_physics_name(example_grid, use_dask):
|
|
158
|
-
with pytest.raises(ValueError, match="`source` must include a 'name'
|
|
224
|
+
with pytest.raises(ValueError, match="`source` must include a 'name'"):
|
|
159
225
|
InitialConditions(
|
|
160
226
|
grid=example_grid,
|
|
161
227
|
ini_time=datetime(2021, 6, 29),
|
|
@@ -164,23 +230,10 @@ def test_initial_conditions_missing_physics_name(example_grid, use_dask):
|
|
|
164
230
|
)
|
|
165
231
|
|
|
166
232
|
|
|
167
|
-
# Test initialization with missing 'path' in source
|
|
168
|
-
def test_initial_conditions_missing_physics_path(example_grid, use_dask):
|
|
169
|
-
with pytest.raises(ValueError, match="`source` must include a 'path'."):
|
|
170
|
-
InitialConditions(
|
|
171
|
-
grid=example_grid,
|
|
172
|
-
ini_time=datetime(2021, 6, 29),
|
|
173
|
-
source={"name": "GLORYS"},
|
|
174
|
-
use_dask=use_dask,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
233
|
# Test initialization with missing 'name' in bgc_source
|
|
179
234
|
def test_initial_conditions_missing_bgc_name(example_grid, use_dask):
|
|
180
235
|
fname = Path(download_test_data("GLORYS_coarse_test_data.nc"))
|
|
181
|
-
with pytest.raises(
|
|
182
|
-
ValueError, match="`bgc_source` must include a 'name' if it is provided."
|
|
183
|
-
):
|
|
236
|
+
with pytest.raises(ValueError, match="`bgc_source` must include a 'name'"):
|
|
184
237
|
InitialConditions(
|
|
185
238
|
grid=example_grid,
|
|
186
239
|
ini_time=datetime(2021, 6, 29),
|
|
@@ -10,6 +10,7 @@ import xarray as xr
|
|
|
10
10
|
from conftest import calculate_data_hash
|
|
11
11
|
from roms_tools import Grid, SurfaceForcing
|
|
12
12
|
from roms_tools.download import download_test_data
|
|
13
|
+
from roms_tools.setup.datasets import RawDataSource
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@pytest.fixture
|
|
@@ -159,7 +160,7 @@ def _test_successful_initialization(
|
|
|
159
160
|
grid: Grid,
|
|
160
161
|
start_time: datetime,
|
|
161
162
|
end_time: datetime,
|
|
162
|
-
source:
|
|
163
|
+
source: RawDataSource,
|
|
163
164
|
coarse_grid_mode: str,
|
|
164
165
|
use_dask: bool,
|
|
165
166
|
caplog,
|
|
@@ -187,12 +188,12 @@ def _test_successful_initialization(
|
|
|
187
188
|
if coarse_grid_mode == "always":
|
|
188
189
|
assert sfc_forcing.use_coarse_grid
|
|
189
190
|
assert (
|
|
190
|
-
"Data will be interpolated onto grid coarsened by factor 2."
|
|
191
|
+
"Data will be interpolated onto the grid coarsened by factor 2."
|
|
191
192
|
in caplog.text
|
|
192
193
|
)
|
|
193
194
|
elif coarse_grid_mode == "never":
|
|
194
195
|
assert not sfc_forcing.use_coarse_grid
|
|
195
|
-
assert "Data will be interpolated onto fine grid." in caplog.text
|
|
196
|
+
assert "Data will be interpolated onto the fine grid." in caplog.text
|
|
196
197
|
|
|
197
198
|
assert isinstance(sfc_forcing.ds, xr.Dataset)
|
|
198
199
|
assert "uwnd" in sfc_forcing.ds
|
|
@@ -902,7 +903,9 @@ def test_from_yaml_missing_surface_forcing(tmp_path, use_dask):
|
|
|
902
903
|
yaml_filepath.unlink()
|
|
903
904
|
|
|
904
905
|
|
|
906
|
+
@pytest.mark.skip("Temporary skip until memory consumption issue is addressed. # TODO")
|
|
905
907
|
@pytest.mark.stream
|
|
908
|
+
@pytest.mark.use_dask
|
|
906
909
|
def test_surface_forcing_arco(surface_forcing_arco, tmp_path):
|
|
907
910
|
"""One big integration test for cloud-based ERA5 data because the streaming takes a
|
|
908
911
|
long time.
|
|
@@ -932,3 +935,25 @@ def test_surface_forcing_arco(surface_forcing_arco, tmp_path):
|
|
|
932
935
|
yaml_filepath.unlink()
|
|
933
936
|
Path(expected_filepath1).unlink()
|
|
934
937
|
Path(expected_filepath2).unlink()
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
@pytest.mark.skip("Temporary skip until memory consumption issue is addressed. # TODO")
|
|
941
|
+
@pytest.mark.stream
|
|
942
|
+
@pytest.mark.use_dask
|
|
943
|
+
@pytest.mark.use_gcsfs
|
|
944
|
+
def test_default_era5_dataset_loading(small_grid: Grid) -> None:
|
|
945
|
+
"""Verify the default ERA5 dataset is loaded when a path is not provided."""
|
|
946
|
+
start_time = datetime(2020, 2, 1)
|
|
947
|
+
end_time = datetime(2020, 2, 2)
|
|
948
|
+
|
|
949
|
+
sf = SurfaceForcing(
|
|
950
|
+
grid=small_grid,
|
|
951
|
+
source={"name": "ERA5"},
|
|
952
|
+
type="physics",
|
|
953
|
+
start_time=start_time,
|
|
954
|
+
end_time=end_time,
|
|
955
|
+
use_dask=True,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
expected_vars = {"uwnd", "vwnd", "swrad", "lwrad", "Tair", "rain"}
|
|
959
|
+
assert set(sf.ds.data_vars).issuperset(expected_vars)
|