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.
Files changed (45) hide show
  1. roms_tools/__init__.py +8 -1
  2. roms_tools/analysis/cdr_analysis.py +203 -0
  3. roms_tools/analysis/cdr_ensemble.py +198 -0
  4. roms_tools/analysis/roms_output.py +80 -46
  5. roms_tools/data/grids/GLORYS_global_grid.nc +0 -0
  6. roms_tools/download.py +4 -0
  7. roms_tools/plot.py +131 -30
  8. roms_tools/regrid.py +6 -1
  9. roms_tools/setup/boundary_forcing.py +94 -44
  10. roms_tools/setup/cdr_forcing.py +123 -15
  11. roms_tools/setup/cdr_release.py +161 -8
  12. roms_tools/setup/datasets.py +709 -341
  13. roms_tools/setup/grid.py +167 -139
  14. roms_tools/setup/initial_conditions.py +113 -48
  15. roms_tools/setup/mask.py +63 -7
  16. roms_tools/setup/nesting.py +67 -42
  17. roms_tools/setup/river_forcing.py +45 -19
  18. roms_tools/setup/surface_forcing.py +16 -10
  19. roms_tools/setup/tides.py +1 -2
  20. roms_tools/setup/topography.py +4 -4
  21. roms_tools/setup/utils.py +134 -22
  22. roms_tools/tests/test_analysis/test_cdr_analysis.py +144 -0
  23. roms_tools/tests/test_analysis/test_cdr_ensemble.py +202 -0
  24. roms_tools/tests/test_analysis/test_roms_output.py +61 -3
  25. roms_tools/tests/test_setup/test_boundary_forcing.py +111 -52
  26. roms_tools/tests/test_setup/test_cdr_forcing.py +54 -0
  27. roms_tools/tests/test_setup/test_cdr_release.py +118 -1
  28. roms_tools/tests/test_setup/test_datasets.py +458 -34
  29. roms_tools/tests/test_setup/test_grid.py +238 -121
  30. roms_tools/tests/test_setup/test_initial_conditions.py +94 -41
  31. roms_tools/tests/test_setup/test_surface_forcing.py +28 -3
  32. roms_tools/tests/test_setup/test_utils.py +91 -1
  33. roms_tools/tests/test_setup/test_validation.py +21 -15
  34. roms_tools/tests/test_setup/utils.py +71 -0
  35. roms_tools/tests/test_tiling/test_join.py +241 -0
  36. roms_tools/tests/test_tiling/test_partition.py +45 -0
  37. roms_tools/tests/test_utils.py +224 -2
  38. roms_tools/tiling/join.py +189 -0
  39. roms_tools/tiling/partition.py +44 -30
  40. roms_tools/utils.py +488 -161
  41. {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/METADATA +15 -4
  42. {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/RECORD +45 -37
  43. {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/WHEEL +0 -0
  44. {roms_tools-3.1.1.dist-info → roms_tools-3.2.0.dist-info}/licenses/LICENSE +0 -0
  45. {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
- def test_plot():
181
- grid = Grid(
182
- nx=20, ny=20, size_x=100, size_y=100, center_lon=-20, center_lat=0, rot=0
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
- grid.plot(with_dim_names=False)
186
- grid.plot(with_dim_names=True)
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
- def test_files_have_same_hash(tmp_path):
357
- # Initialize a Grid object using the initializer
358
- grid_init = Grid(
359
- nx=10,
360
- ny=15,
361
- size_x=100.0,
362
- size_y=150.0,
363
- center_lon=0.0,
364
- center_lat=0.0,
365
- rot=0.0,
366
- topography_source={"name": "ETOPO5"},
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
- grid_init.to_yaml(yaml_filepath)
375
- grid_init.save(filepath1)
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 CESMBGCDataset, UnifiedBGCDataset
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 test_initial_conditions_creation(ic_fixture, request):
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: dict[str, str],
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)