roms-tools 2.3.0__py3-none-any.whl → 2.4.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 (143) hide show
  1. ci/environment.yml +1 -0
  2. roms_tools/__init__.py +1 -0
  3. roms_tools/analysis/roms_output.py +10 -6
  4. roms_tools/setup/boundary_forcing.py +178 -193
  5. roms_tools/setup/datasets.py +58 -1
  6. roms_tools/setup/grid.py +31 -97
  7. roms_tools/setup/initial_conditions.py +172 -126
  8. roms_tools/setup/nesting.py +2 -23
  9. roms_tools/setup/river_forcing.py +34 -67
  10. roms_tools/setup/surface_forcing.py +111 -61
  11. roms_tools/setup/tides.py +7 -30
  12. roms_tools/setup/utils.py +24 -70
  13. roms_tools/tests/test_setup/test_boundary_forcing.py +220 -57
  14. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zattrs +5 -3
  15. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zmetadata +156 -121
  16. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zarray +2 -2
  17. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zattrs +2 -1
  18. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/0 +0 -0
  19. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zarray +2 -2
  20. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zattrs +1 -1
  21. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/.zarray +4 -4
  23. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/0.0.0 +0 -0
  24. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/.zarray +4 -4
  25. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/0.0.0 +0 -0
  26. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/.zarray +4 -4
  27. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/0.0.0 +0 -0
  28. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/.zarray +4 -4
  29. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/0.0.0 +0 -0
  30. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/.zarray +4 -4
  31. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/0.0.0 +0 -0
  32. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/.zarray +4 -4
  33. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/0.0.0 +0 -0
  34. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/.zarray +4 -4
  35. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/0.0.0 +0 -0
  36. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/.zarray +4 -4
  37. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/.zarray +4 -4
  39. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/.zarray +4 -4
  41. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/.zarray +4 -4
  43. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/.zarray +4 -4
  45. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/0.0.0 +0 -0
  46. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/.zarray +4 -4
  47. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/0.0 +0 -0
  48. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/.zarray +4 -4
  49. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/0.0 +0 -0
  50. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/.zarray +4 -4
  51. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/0.0 +0 -0
  52. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/.zarray +4 -4
  53. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/0.0 +0 -0
  54. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/.zarray +4 -4
  55. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/0.0.0 +0 -0
  56. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/.zarray +4 -4
  57. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/0.0.0 +0 -0
  58. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/.zarray +4 -4
  59. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/0.0.0 +0 -0
  60. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/.zarray +4 -4
  61. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/0.0.0 +0 -0
  62. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/.zarray +4 -4
  63. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/0.0 +0 -0
  64. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/.zarray +4 -4
  65. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/0.0 +0 -0
  66. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/.zarray +4 -4
  67. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/0.0 +0 -0
  68. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/.zarray +4 -4
  69. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/0.0 +0 -0
  70. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zarray +4 -4
  71. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zattrs +8 -0
  72. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/0.0 +0 -0
  73. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zarray +4 -4
  74. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zattrs +8 -0
  75. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/0.0 +0 -0
  76. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zarray +4 -4
  77. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zattrs +8 -0
  78. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/0.0 +0 -0
  79. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zarray +4 -4
  80. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zattrs +8 -0
  81. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/0.0 +0 -0
  82. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +4 -4
  83. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +4 -4
  84. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle/0.0 +0 -0
  85. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle_coarse/0.0 +0 -0
  86. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/f/0.0 +0 -0
  87. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  88. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_coarse/0.0 +0 -0
  89. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_rho/0.0 +0 -0
  90. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_u/0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_v/0.0 +0 -0
  92. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_coarse/0.0 +0 -0
  93. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_rho/0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_u/0.0 +0 -0
  95. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_v/0.0 +0 -0
  96. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_coarse/0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_rho/0.0 +0 -0
  98. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_u/0.0 +0 -0
  99. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_v/0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pm/0.0 +0 -0
  101. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pn/0.0 +0 -0
  102. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zattrs +2 -1
  103. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zmetadata +6 -4
  104. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_r/.zattrs +1 -1
  105. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_w/.zattrs +1 -1
  106. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NH4/0.0.0.0 +0 -0
  107. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NO3/0.0.0.0 +0 -0
  108. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/PO4/0.0.0.0 +0 -0
  109. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/abs_time/.zattrs +1 -0
  110. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatSi/0.0.0.0 +0 -0
  111. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ocean_time/.zattrs +1 -1
  112. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spC/0.0.0.0 +0 -0
  113. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spCaCO3/0.0.0.0 +0 -0
  114. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spFe/0.0.0.0 +0 -0
  115. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/0.0.0.0 +0 -0
  116. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/0.0.0.0 +0 -0
  117. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/0.0.0 +0 -0
  118. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/0.0.0.0 +0 -0
  119. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/0.0.0 +0 -0
  120. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +30 -0
  121. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zarray +22 -0
  122. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs +8 -0
  123. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/0.0 +0 -0
  124. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +30 -0
  125. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zarray +22 -0
  126. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs +8 -0
  127. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/0.0 +0 -0
  128. roms_tools/tests/test_setup/test_grid.py +0 -13
  129. roms_tools/tests/test_setup/test_initial_conditions.py +204 -66
  130. roms_tools/tests/test_setup/test_nesting.py +0 -16
  131. roms_tools/tests/test_setup/test_river_forcing.py +8 -36
  132. roms_tools/tests/test_setup/test_surface_forcing.py +102 -73
  133. roms_tools/tests/test_setup/test_tides.py +4 -16
  134. roms_tools/tests/test_setup/test_utils.py +1 -0
  135. roms_tools/tests/{test_utils.py → test_tiling/test_partition.py} +1 -1
  136. roms_tools/tiling/partition.py +338 -0
  137. roms_tools/utils.py +66 -333
  138. roms_tools/vertical_coordinate.py +54 -133
  139. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/METADATA +1 -1
  140. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/RECORD +143 -136
  141. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/LICENSE +0 -0
  142. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/WHEEL +0 -0
  143. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/top_level.txt +0 -0
@@ -9,13 +9,13 @@ from pathlib import Path
9
9
  import matplotlib.pyplot as plt
10
10
  from roms_tools import Grid
11
11
  from roms_tools.plot import _get_projection, _add_field_to_ax
12
+ from roms_tools.utils import save_datasets
12
13
  from roms_tools.setup.datasets import DaiRiverDataset
13
14
  from roms_tools.setup.utils import (
14
15
  get_target_coords,
15
16
  gc_dist,
16
17
  substitute_nans_by_fillvalue,
17
18
  convert_to_roms_time,
18
- save_datasets,
19
19
  _to_yaml,
20
20
  _from_yaml,
21
21
  get_variable_metadata,
@@ -91,8 +91,9 @@ class RiverForcing:
91
91
  object.__setattr__(self, "original_indices", original_indices)
92
92
 
93
93
  if len(original_indices["station"]) > 0:
94
- self._move_rivers_to_closest_coast(target_coords, data)
95
94
  ds = self._create_river_forcing(data)
95
+ self._move_rivers_to_closest_coast(target_coords, data)
96
+ ds = self._write_indices_into_dataset(ds)
96
97
  self._validate(ds)
97
98
 
98
99
  for var_name in ds.data_vars:
@@ -345,49 +346,44 @@ class RiverForcing:
345
346
  "xi_rho": indices[2],
346
347
  "name": names,
347
348
  }
348
- self._write_indices_into_grid_file(indices)
349
349
  object.__setattr__(self, "updated_indices", indices)
350
350
 
351
- def _write_indices_into_grid_file(self, indices):
352
- """Writes river location indices into the grid dataset as the "river_flux"
353
- variable.
351
+ def _write_indices_into_dataset(self, ds):
352
+ """Adds river location indices to the dataset as the "river_location" variable.
354
353
 
355
- This method checks if the variable "river_flux" already exists in the grid dataset.
356
- If it does, the method removes it. Then, it creates a new variable called "river_flux"
357
- based on the provided `indices` and assigns it to the dataset. The `indices` dictionary
358
- provides information about the river station locations and their corresponding grid
359
- cell indices (eta_rho and xi_rho).
354
+ This method checks if the "river_location" variable already exists in the dataset.
355
+ If it does, the method removes it. Then, it creates a new "river_location" variable
356
+ using river station indices from `self.updated_indices` and assigns it to the dataset.
357
+ The indices specify the river station locations in terms of eta_rho and xi_rho grid cell indices.
360
358
 
361
- Parameters:
362
- -----------
363
- indices : dict
364
- A dictionary containing information about river station locations.
365
- It must contain the following keys:
366
- - "name" (list or array): Names of the river stations.
367
- - "station" (list or array): River station identifiers.
368
- - "eta_rho" (list or array): The eta (row) index for each river location.
369
- - "xi_rho" (list or array): The xi (column) index for each river location.
359
+ Parameters
360
+ ----------
361
+ ds : xarray.Dataset
362
+ The dataset to which the "river_location" variable will be added.
370
363
 
371
- Returns:
372
- --------
373
- None
374
- This method modifies the grid's dataset in-place by adding the "river_flux" variable.
364
+ Returns
365
+ -------
366
+ xarray.Dataset
367
+ The modified dataset with the "river_location" variable added.
375
368
  """
376
369
 
377
- if "river_flux" in self.grid.ds:
378
- ds = self.grid.ds.drop_vars("river_flux")
379
- object.__setattr__(self.grid, "ds", ds)
370
+ if "river_location" in ds:
371
+ ds = ds.drop_vars("river_location")
380
372
 
381
373
  river_locations = xr.zeros_like(self.grid.ds.mask_rho)
382
- for i in range(len(indices["name"])):
383
- station = indices["station"][i]
384
- eta_index = indices["eta_rho"][i]
385
- xi_index = indices["xi_rho"][i]
374
+ for i in range(len(self.updated_indices["name"])):
375
+ station = self.updated_indices["station"][i]
376
+ eta_index = self.updated_indices["eta_rho"][i]
377
+ xi_index = self.updated_indices["xi_rho"][i]
386
378
  river_locations[eta_index, xi_index] = station + 2
387
379
 
388
- river_locations.attrs["long_name"] = "River volume flux partition"
380
+ river_locations.attrs["long_name"] = "River index plus local volume fraction"
389
381
  river_locations.attrs["units"] = "none"
390
- self.grid.ds["river_flux"] = river_locations
382
+ ds["river_location"] = river_locations
383
+
384
+ ds = ds.drop_vars(["lat_rho", "lon_rho"])
385
+
386
+ return ds
391
387
 
392
388
  def _validate(self, ds):
393
389
  """Validates the dataset by checking for NaN values in river forcing data.
@@ -592,37 +588,13 @@ class RiverForcing:
592
588
  def save(
593
589
  self,
594
590
  filepath: Union[str, Path],
595
- filepath_grid: Union[str, Path],
596
- np_eta: int = None,
597
- np_xi: int = None,
598
591
  ) -> None:
599
- """Save the river forcing and grid file to netCDF4 files. The grid file is
600
- required because a new field `river_flux` has been added.
601
-
602
- This method allows saving the river forcing and grid data either each as a single file or each partitioned into multiple files, based on the provided options. The dataset can be saved in two modes:
603
-
604
- 1. **Single File Mode (default)**:
605
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
606
- - The file is named based on the provided `filepath`, with `.nc` automatically appended to the filename.
607
-
608
- 2. **Partitioned Mode**:
609
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned spatially along the `eta` and `xi` axes into tiles.
610
- - Each tile is saved as a separate netCDF4 file. Filenames will be modified with an index to represent each partition, e.g., `"filepath_YYYYMM.0.nc"`, `"filepath_YYYYMM.1.nc"`, etc.
592
+ """Save the river forcing to netCDF4 file.
611
593
 
612
594
  Parameters
613
595
  ----------
614
596
  filepath : Union[str, Path]
615
- The base path and filename for the output files. The filenames will include the specified path and the `.nc` extension.
616
- If partitioning is used, additional indices will be appended to the filenames, e.g., `"filepath.0.nc"`, `"filepath.1.nc"`, etc.
617
-
618
- filepath_grid : Union[str, Path]
619
- The base path and filename for saving the grid file.
620
-
621
- np_eta : int, optional
622
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed along the `eta` axis.
623
-
624
- np_xi : int, optional
625
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed along the `xi` axis.
597
+ The base path and filename for the output files.
626
598
 
627
599
  Returns
628
600
  -------
@@ -632,20 +604,15 @@ class RiverForcing:
632
604
 
633
605
  # Ensure filepath is a Path object
634
606
  filepath = Path(filepath)
635
- filepath_grid = Path(filepath_grid)
636
607
 
637
608
  # Remove ".nc" suffix if present
638
609
  if filepath.suffix == ".nc":
639
610
  filepath = filepath.with_suffix("")
640
- if filepath_grid.suffix == ".nc":
641
- filepath_grid = filepath_grid.with_suffix("")
642
611
 
643
- dataset_list = [self.ds, self.grid.ds]
644
- output_filenames = [str(filepath), str(filepath_grid)]
612
+ dataset_list = [self.ds]
613
+ output_filenames = [str(filepath)]
645
614
 
646
- saved_filenames = save_datasets(
647
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
648
- )
615
+ saved_filenames = save_datasets(dataset_list, output_filenames)
649
616
 
650
617
  return saved_filenames
651
618
 
@@ -5,8 +5,10 @@ from datetime import datetime
5
5
  import numpy as np
6
6
  import matplotlib.pyplot as plt
7
7
  from pathlib import Path
8
+ import logging
8
9
  from typing import Dict, Union, List
9
10
  from roms_tools import Grid
11
+ from roms_tools.utils import save_datasets
10
12
  from roms_tools.regrid import LateralRegrid
11
13
  from roms_tools.plot import _plot
12
14
  from roms_tools.setup.datasets import (
@@ -21,7 +23,6 @@ from roms_tools.setup.utils import (
21
23
  interpolate_from_climatology,
22
24
  get_variable_metadata,
23
25
  group_dataset,
24
- save_datasets,
25
26
  rotate_velocities,
26
27
  convert_to_roms_time,
27
28
  _to_yaml,
@@ -59,9 +60,14 @@ class SurfaceForcing:
59
60
  - "bgc": for biogeochemical forcing.
60
61
 
61
62
  correct_radiation : bool
62
- Whether to correct shortwave radiation. Default is False.
63
- use_coarse_grid: bool
64
- Whether to interpolate to coarsened grid. Default is False.
63
+ Whether to correct shortwave radiation. Default is True.
64
+ coarse_grid_mode : str, optional
65
+ Specifies whether to interpolate onto grid coarsened by a factor of two. Options are:
66
+
67
+ - "auto" (default): Automatically decide based on the comparison of source and target spatial resolutions.
68
+ - "always": Always interpolate onto the coarse grid.
69
+ - "never": Never use the coarse grid; interpolate onto the fine grid instead.
70
+
65
71
  model_reference_date : datetime, optional
66
72
  Reference date for the model. Default is January 1, 2000.
67
73
  use_dask: bool, optional
@@ -88,8 +94,8 @@ class SurfaceForcing:
88
94
  end_time: datetime
89
95
  source: Dict[str, Union[str, Path, List[Union[str, Path]]]]
90
96
  type: str = "physics"
91
- correct_radiation: bool = False
92
- use_coarse_grid: bool = False
97
+ correct_radiation: bool = True
98
+ coarse_grid_mode: str = "auto"
93
99
  model_reference_date: datetime = datetime(2000, 1, 1)
94
100
  use_dask: bool = False
95
101
  bypass_validation: bool = False
@@ -99,10 +105,23 @@ class SurfaceForcing:
99
105
  def __post_init__(self):
100
106
 
101
107
  self._input_checks()
108
+ data = self._get_data()
109
+
110
+ if self.coarse_grid_mode == "always":
111
+ use_coarse_grid = True
112
+ elif self.coarse_grid_mode == "never":
113
+ use_coarse_grid = False
114
+ elif self.coarse_grid_mode == "auto":
115
+ use_coarse_grid = self._determine_coarse_grid_usage(data)
116
+ if use_coarse_grid:
117
+ logging.info("Data will be interpolated onto grid coarsened by factor 2.")
118
+ else:
119
+ logging.info("Data will be interpolated onto fine grid.")
120
+ object.__setattr__(self, "use_coarse_grid", use_coarse_grid)
121
+
102
122
  target_coords = get_target_coords(self.grid, self.use_coarse_grid)
103
123
  object.__setattr__(self, "target_coords", target_coords)
104
124
 
105
- data = self._get_data()
106
125
  data.choose_subdomain(
107
126
  target_coords,
108
127
  buffer_points=20, # lateral fill needs some buffer from data margin
@@ -166,6 +185,47 @@ class SurfaceForcing:
166
185
  {**self.source, "climatology": self.source.get("climatology", False)},
167
186
  )
168
187
 
188
+ # Validate 'coarse_grid_mode'
189
+ valid_modes = ["auto", "always", "never"]
190
+ if self.coarse_grid_mode not in valid_modes:
191
+ raise ValueError(
192
+ f"`coarse_grid_mode` must be one of {valid_modes}, but got '{self.coarse_grid_mode}'."
193
+ )
194
+
195
+ def _determine_coarse_grid_usage(self, data):
196
+ """Determine if coarse grid interpolation should be used based on the resolution
197
+ of the dataset and the target grid.
198
+
199
+ Parameters
200
+ ----------
201
+ data : object
202
+ The dataset object containing the data to be analyzed for grid spacing.
203
+
204
+ Returns
205
+ -------
206
+ use_coarse_grid : bool
207
+ Whether to use the coarse grid or not.
208
+ """
209
+ # Get the target coordinates and select the subdomain of the data
210
+ target_coords = get_target_coords(self.grid, use_coarse_grid=False)
211
+ data_coords = data.choose_subdomain(
212
+ target_coords, buffer_points=1, return_coords_only=True
213
+ )
214
+
215
+ # Compute minimal grid spacing in the data subdomain
216
+ min_grid_spacing_data = data.compute_minimal_grid_spacing(data_coords)
217
+
218
+ # Compute the maximum grid spacing in the ROMS grid
219
+ max_grid_spacing = max((1 / self.grid.ds.pm).max(), (1 / self.grid.ds.pn).max())
220
+
221
+ # Determine whether to use coarse grid based on grid spacing comparison
222
+ if 2 * max_grid_spacing < min_grid_spacing_data:
223
+ use_coarse_grid = True
224
+ else:
225
+ use_coarse_grid = False
226
+
227
+ return use_coarse_grid
228
+
169
229
  def _get_data(self):
170
230
 
171
231
  data_dict = {
@@ -238,14 +298,14 @@ class SurfaceForcing:
238
298
  "qair": {**default_info, "validate": True},
239
299
  "rain": {**default_info, "validate": False},
240
300
  "uwnd": {
241
- "location": "u",
301
+ "location": "rho",
242
302
  "is_vector": True,
243
303
  "vector_pair": "vwnd",
244
304
  "is_3d": False,
245
305
  "validate": True,
246
306
  },
247
307
  "vwnd": {
248
- "location": "v",
308
+ "location": "rho",
249
309
  "is_vector": True,
250
310
  "vector_pair": "uwnd",
251
311
  "is_3d": False,
@@ -266,7 +326,7 @@ class SurfaceForcing:
266
326
  def _apply_correction(self, processed_fields, data):
267
327
 
268
328
  correction_data = self._get_correction_data()
269
- # choose same subdomain as forcing data so that we can use same mask
329
+ # Match subdomain to forcing data to reuse the mask
270
330
  coords_correction = {
271
331
  "lat": data.ds[data.dim_names["latitude"]],
272
332
  "lon": data.ds[data.dim_names["longitude"]],
@@ -275,23 +335,39 @@ class SurfaceForcing:
275
335
  coords_correction, straddle=self.target_coords["straddle"]
276
336
  )
277
337
  correction_data.ds["mask"] = data.ds["mask"] # use mask from ERA5 data
338
+ correction_data.ds["time"] = correction_data.ds["time"].dt.days
339
+
278
340
  correction_data.apply_lateral_fill()
279
- # regrid
280
- lateral_regrid = LateralRegrid(self.target_coords, correction_data.dim_names)
281
- corr_factor = lateral_regrid.apply(
282
- correction_data.ds[correction_data.var_names["swr_corr"]]
283
- )
284
341
 
285
- # temporal interpolation
286
- corr_factor = interpolate_from_climatology(
287
- corr_factor,
288
- correction_data.dim_names["time"],
289
- time=processed_fields["swrad"].time,
290
- )
342
+ # Temporal interpolation: Perform before spatial regridding for better performance
343
+ if self.use_dask:
344
+ # Perform temporal interpolation for each time slice to enforce chunking in time.
345
+ # This reduces memory usage by processing one time step at a time.
346
+ # The interpolated slices are then concatenated along the "time" dimension.
347
+ corr_factor = xr.concat(
348
+ [
349
+ interpolate_from_climatology(
350
+ correction_data.ds[correction_data.var_names["swr_corr"]],
351
+ correction_data.dim_names["time"],
352
+ time=time,
353
+ )
354
+ for time in processed_fields["swrad"].time
355
+ ],
356
+ dim="time",
357
+ )
358
+ else:
359
+ # Interpolate across all time steps at once
360
+ corr_factor = interpolate_from_climatology(
361
+ correction_data.ds[correction_data.var_names["swr_corr"]],
362
+ correction_data.dim_names["time"],
363
+ time=processed_fields["swrad"].time,
364
+ )
291
365
 
292
- processed_fields["swrad"] = processed_fields["swrad"] * corr_factor
366
+ # Spatial regridding
367
+ lateral_regrid = LateralRegrid(self.target_coords, correction_data.dim_names)
368
+ corr_factor = lateral_regrid.apply(corr_factor)
293
369
 
294
- del corr_factor
370
+ processed_fields["swrad"] = processed_fields["swrad"] * corr_factor
295
371
 
296
372
  return processed_fields
297
373
 
@@ -357,12 +433,8 @@ class SurfaceForcing:
357
433
 
358
434
  for var_name in ds.data_vars:
359
435
  if self.variable_info[var_name]["validate"]:
360
- if self.variable_info[var_name]["location"] == "rho":
361
- mask = self.target_coords["mask"]
362
- elif self.variable_info[var_name]["location"] == "u":
363
- mask = self.target_coords["mask_u"]
364
- elif self.variable_info[var_name]["location"] == "v":
365
- mask = self.target_coords["mask_v"]
436
+ # all variables are at rho-points
437
+ mask = self.target_coords["mask"]
366
438
  nan_check(ds[var_name].isel(time=0), mask)
367
439
 
368
440
  def _add_global_metadata(self, ds=None):
@@ -476,42 +548,26 @@ class SurfaceForcing:
476
548
  def save(
477
549
  self,
478
550
  filepath: Union[str, Path],
479
- np_eta: int = None,
480
- np_xi: int = None,
481
- group: bool = False,
551
+ group: bool = True,
482
552
  ) -> None:
483
553
  """Save the surface forcing fields to one or more netCDF4 files.
484
554
 
485
- This method saves the dataset either as a single file or as multiple files depending on the partitioning and grouping options.
486
- The dataset can be saved in two modes:
487
-
488
- 1. **Single File Mode (default)**:
489
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
490
- - The file is named based on the `filepath`, with `.nc` automatically appended.
491
-
492
- 2. **Partitioned Mode**:
493
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned into spatial tiles along the `eta` and `xi` axes.
494
- - Each tile is saved as a separate netCDF4 file, and filenames are modified with an index (e.g., `"filepath_YYYYMM.0.nc"`, `"filepath_YYYYMM.1.nc"`).
495
-
496
- Additionally, if `group` is set to `True`, the dataset is first grouped into temporal subsets, resulting in multiple grouped files before partitioning and saving.
555
+ This method saves the dataset to disk as either a single netCDF4 file or multiple files, depending on the `group` parameter.
556
+ If `group` is `True`, the dataset is divided into subsets (e.g., monthly or yearly) based on the temporal frequency
557
+ of the data, and each subset is saved to a separate file.
497
558
 
498
559
  Parameters
499
560
  ----------
500
561
  filepath : Union[str, Path]
501
- The base path and filename for the output files. The format of the filenames depends on whether partitioning is used
502
- and the temporal range of the data. For partitioned datasets, files will be named with an additional index, e.g.,
503
- `"filepath_YYYYMM.0.nc"`, `"filepath_YYYYMM.1.nc"`, etc.
504
- np_eta : int, optional
505
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed.
506
- np_xi : int, optional
507
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed.
508
- group: bool, optional
509
- If `True`, groups the dataset into multiple files based on temporal data frequency. Defaults to `False`.
562
+ The base path and filename for the output file(s). If `group` is `True`, the filenames will include additional
563
+ time-based information (e.g., year or month) to distinguish the subsets.
564
+ group : bool, optional
565
+ Whether to divide the dataset into multiple files based on temporal frequency. Defaults to `True`.
510
566
 
511
567
  Returns
512
568
  -------
513
569
  List[Path]
514
- A list of Path objects for the filenames that were saved.
570
+ A list of `Path` objects representing the filenames of the saved file(s).
515
571
  """
516
572
 
517
573
  # Ensure filepath is a Path object
@@ -521,12 +577,6 @@ class SurfaceForcing:
521
577
  if filepath.suffix == ".nc":
522
578
  filepath = filepath.with_suffix("")
523
579
 
524
- if self.use_dask:
525
- from dask.diagnostics import ProgressBar
526
-
527
- with ProgressBar():
528
- self.ds.load()
529
-
530
580
  if group:
531
581
  dataset_list, output_filenames = group_dataset(self.ds, str(filepath))
532
582
  else:
@@ -534,7 +584,7 @@ class SurfaceForcing:
534
584
  output_filenames = [str(filepath)]
535
585
 
536
586
  saved_filenames = save_datasets(
537
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
587
+ dataset_list, output_filenames, use_dask=self.use_dask
538
588
  )
539
589
 
540
590
  return saved_filenames
roms_tools/setup/tides.py CHANGED
@@ -9,6 +9,7 @@ from dataclasses import dataclass, field
9
9
  from roms_tools import Grid
10
10
  from roms_tools.plot import _plot
11
11
  from roms_tools.regrid import LateralRegrid
12
+ from roms_tools.utils import save_datasets
12
13
  from roms_tools.setup.datasets import TPXODataset
13
14
  from roms_tools.setup.utils import (
14
15
  nan_check,
@@ -16,7 +17,6 @@ from roms_tools.setup.utils import (
16
17
  interpolate_from_rho_to_u,
17
18
  interpolate_from_rho_to_v,
18
19
  get_variable_metadata,
19
- save_datasets,
20
20
  get_target_coords,
21
21
  rotate_velocities,
22
22
  get_vector_pairs,
@@ -368,36 +368,19 @@ class TidalForcing:
368
368
  kwargs=kwargs,
369
369
  )
370
370
 
371
- def save(
372
- self, filepath: Union[str, Path], np_eta: int = None, np_xi: int = None
373
- ) -> None:
371
+ def save(self, filepath: Union[str, Path]) -> None:
374
372
  """Save the tidal forcing information to a netCDF4 file.
375
373
 
376
- This method supports saving the dataset in two modes:
377
-
378
- 1. **Single File Mode (default)**:
379
-
380
- If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file
381
- with the base filename specified by `filepath.nc`.
382
-
383
- 2. **Partitioned Mode**:
384
-
385
- - If either `np_eta` or `np_xi` is specified, the dataset is divided into spatial tiles along the eta-axis and xi-axis.
386
- - Each spatial tile is saved as a separate netCDF4 file.
387
-
388
374
  Parameters
389
375
  ----------
390
376
  filepath : Union[str, Path]
391
- The base path or filename where the dataset should be saved.
392
- np_eta : int, optional
393
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed.
394
- np_xi : int, optional
395
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed.
377
+ The path or filename where the dataset will be saved. If a directory is specified,
378
+ the file will be saved with a default name within that directory.
396
379
 
397
380
  Returns
398
381
  -------
399
- List[Path]
400
- A list of Path objects for the filenames that were saved.
382
+ Path
383
+ A `Path` object representing the location of the saved file.
401
384
  """
402
385
 
403
386
  # Ensure filepath is a Path object
@@ -407,17 +390,11 @@ class TidalForcing:
407
390
  if filepath.suffix == ".nc":
408
391
  filepath = filepath.with_suffix("")
409
392
 
410
- if self.use_dask:
411
- from dask.diagnostics import ProgressBar
412
-
413
- with ProgressBar():
414
- self.ds.load()
415
-
416
393
  dataset_list = [self.ds]
417
394
  output_filenames = [str(filepath)]
418
395
 
419
396
  saved_filenames = save_datasets(
420
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
397
+ dataset_list, output_filenames, use_dask=self.use_dask
421
398
  )
422
399
 
423
400
  return saved_filenames