roms-tools 2.2.1__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 (152) hide show
  1. ci/environment.yml +1 -0
  2. roms_tools/__init__.py +2 -0
  3. roms_tools/analysis/roms_output.py +590 -0
  4. roms_tools/{setup/download.py → download.py} +3 -0
  5. roms_tools/{setup/plot.py → plot.py} +34 -28
  6. roms_tools/setup/boundary_forcing.py +199 -203
  7. roms_tools/setup/datasets.py +60 -136
  8. roms_tools/setup/grid.py +40 -67
  9. roms_tools/setup/initial_conditions.py +249 -247
  10. roms_tools/setup/nesting.py +6 -27
  11. roms_tools/setup/river_forcing.py +41 -76
  12. roms_tools/setup/surface_forcing.py +125 -75
  13. roms_tools/setup/tides.py +31 -51
  14. roms_tools/setup/topography.py +1 -1
  15. roms_tools/setup/utils.py +44 -224
  16. roms_tools/tests/test_analysis/test_roms_output.py +269 -0
  17. roms_tools/tests/{test_setup/test_regrid.py → test_regrid.py} +1 -1
  18. roms_tools/tests/test_setup/test_boundary_forcing.py +221 -58
  19. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zattrs +5 -3
  20. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zmetadata +156 -121
  21. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zarray +2 -2
  22. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zattrs +2 -1
  23. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/0 +0 -0
  24. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zarray +2 -2
  25. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zattrs +1 -1
  26. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/0 +0 -0
  27. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/.zarray +4 -4
  28. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/0.0.0 +0 -0
  29. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/.zarray +4 -4
  30. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/0.0.0 +0 -0
  31. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/.zarray +4 -4
  32. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/0.0.0 +0 -0
  33. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/.zarray +4 -4
  34. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/0.0.0 +0 -0
  35. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/.zarray +4 -4
  36. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/.zarray +4 -4
  38. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/.zarray +4 -4
  40. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/.zarray +4 -4
  42. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/.zarray +4 -4
  44. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/0.0.0 +0 -0
  45. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/.zarray +4 -4
  46. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/0.0.0 +0 -0
  47. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/.zarray +4 -4
  48. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/0.0.0 +0 -0
  49. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/.zarray +4 -4
  50. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/0.0.0 +0 -0
  51. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/.zarray +4 -4
  52. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/0.0 +0 -0
  53. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/.zarray +4 -4
  54. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/0.0 +0 -0
  55. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/.zarray +4 -4
  56. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/0.0 +0 -0
  57. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/.zarray +4 -4
  58. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/0.0 +0 -0
  59. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/.zarray +4 -4
  60. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/0.0.0 +0 -0
  61. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/.zarray +4 -4
  62. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/0.0.0 +0 -0
  63. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/.zarray +4 -4
  64. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/0.0.0 +0 -0
  65. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/.zarray +4 -4
  66. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/0.0.0 +0 -0
  67. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/.zarray +4 -4
  68. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/0.0 +0 -0
  69. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/.zarray +4 -4
  70. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/0.0 +0 -0
  71. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/.zarray +4 -4
  72. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/0.0 +0 -0
  73. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/.zarray +4 -4
  74. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/0.0 +0 -0
  75. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zarray +4 -4
  76. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zattrs +8 -0
  77. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/0.0 +0 -0
  78. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zarray +4 -4
  79. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zattrs +8 -0
  80. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/0.0 +0 -0
  81. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zarray +4 -4
  82. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zattrs +8 -0
  83. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/0.0 +0 -0
  84. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zarray +4 -4
  85. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zattrs +8 -0
  86. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/0.0 +0 -0
  87. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +4 -4
  88. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +4 -4
  89. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle/0.0 +0 -0
  90. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle_coarse/0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/f/0.0 +0 -0
  92. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  93. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_coarse/0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_rho/0.0 +0 -0
  95. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_u/0.0 +0 -0
  96. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_v/0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_coarse/0.0 +0 -0
  98. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_rho/0.0 +0 -0
  99. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_u/0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_v/0.0 +0 -0
  101. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_coarse/0.0 +0 -0
  102. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_rho/0.0 +0 -0
  103. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_u/0.0 +0 -0
  104. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_v/0.0 +0 -0
  105. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pm/0.0 +0 -0
  106. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pn/0.0 +0 -0
  107. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zattrs +2 -1
  108. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zmetadata +6 -4
  109. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_r/.zattrs +1 -1
  110. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_w/.zattrs +1 -1
  111. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NH4/0.0.0.0 +0 -0
  112. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NO3/0.0.0.0 +0 -0
  113. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/PO4/0.0.0.0 +0 -0
  114. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/abs_time/.zattrs +1 -0
  115. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatSi/0.0.0.0 +0 -0
  116. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ocean_time/.zattrs +1 -1
  117. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spC/0.0.0.0 +0 -0
  118. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spCaCO3/0.0.0.0 +0 -0
  119. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spFe/0.0.0.0 +0 -0
  120. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/0.0.0.0 +0 -0
  121. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/0.0.0.0 +0 -0
  122. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/0.0.0 +0 -0
  123. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/0.0.0.0 +0 -0
  124. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/0.0.0 +0 -0
  125. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +30 -0
  126. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zarray +22 -0
  127. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs +8 -0
  128. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/0.0 +0 -0
  129. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +30 -0
  130. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zarray +22 -0
  131. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs +8 -0
  132. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/0.0 +0 -0
  133. roms_tools/tests/test_setup/test_datasets.py +1 -1
  134. roms_tools/tests/test_setup/test_grid.py +1 -14
  135. roms_tools/tests/test_setup/test_initial_conditions.py +205 -67
  136. roms_tools/tests/test_setup/test_nesting.py +0 -16
  137. roms_tools/tests/test_setup/test_river_forcing.py +9 -37
  138. roms_tools/tests/test_setup/test_surface_forcing.py +103 -74
  139. roms_tools/tests/test_setup/test_tides.py +5 -17
  140. roms_tools/tests/test_setup/test_topography.py +1 -1
  141. roms_tools/tests/test_setup/test_utils.py +57 -1
  142. roms_tools/tests/{test_utils.py → test_tiling/test_partition.py} +1 -1
  143. roms_tools/tiling/partition.py +338 -0
  144. roms_tools/utils.py +310 -276
  145. roms_tools/vertical_coordinate.py +227 -0
  146. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/METADATA +1 -1
  147. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/RECORD +151 -142
  148. roms_tools/setup/vertical_coordinate.py +0 -109
  149. /roms_tools/{setup/regrid.py → regrid.py} +0 -0
  150. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/LICENSE +0 -0
  151. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/WHEEL +0 -0
  152. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/top_level.txt +0 -0
@@ -4,19 +4,19 @@ from scipy.interpolate import griddata
4
4
  from dataclasses import dataclass, field
5
5
  from typing import Dict, Union
6
6
  from pathlib import Path
7
- from roms_tools.setup.grid import Grid
7
+ import logging
8
+ from scipy.interpolate import interp1d
9
+ from roms_tools import Grid
10
+ from roms_tools.plot import _plot_nesting
11
+ from roms_tools.utils import save_datasets
8
12
  from roms_tools.setup.utils import (
9
13
  interpolate_from_rho_to_u,
10
14
  interpolate_from_rho_to_v,
11
15
  get_boundary_coords,
12
16
  wrap_longitudes,
13
- save_datasets,
14
17
  _to_yaml,
15
18
  _from_yaml,
16
19
  )
17
- from roms_tools.setup.plot import _plot_nesting
18
- import logging
19
- from scipy.interpolate import interp1d
20
20
 
21
21
 
22
22
  @dataclass(frozen=True, kw_only=True)
@@ -114,37 +114,18 @@ class Nesting:
114
114
  self,
115
115
  filepath: Union[str, Path],
116
116
  filepath_child_grid: Union[str, Path],
117
- np_eta: int = None,
118
- np_xi: int = None,
119
117
  ) -> None:
120
118
  """Save the nesting and child grid file to netCDF4 files. The child grid file is
121
119
  required because the topography and mask of the child grid has been modified.
122
120
 
123
- This method allows saving the nesting and child 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:
124
-
125
- 1. **Single File Mode (default)**:
126
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
127
- - The file is named based on the provided `filepath`, with `.nc` automatically appended to the filename.
128
-
129
- 2. **Partitioned Mode**:
130
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned spatially along the `eta` and `xi` axes into tiles.
131
- - 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.
132
-
133
121
  Parameters
134
122
  ----------
135
123
  filepath : Union[str, Path]
136
124
  The base path and filename for the output files. The filenames will include the specified path and the `.nc` extension.
137
- If partitioning is used, additional indices will be appended to the filenames, e.g., `"filepath.0.nc"`, `"filepath.1.nc"`, etc.
138
125
 
139
126
  filepath_child_grid : Union[str, Path]
140
127
  The base path and filename for saving the childe grid file.
141
128
 
142
- np_eta : int, optional
143
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed along the `eta` axis.
144
-
145
- np_xi : int, optional
146
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed along the `xi` axis.
147
-
148
129
  Returns
149
130
  -------
150
131
  List[Path]
@@ -164,9 +145,7 @@ class Nesting:
164
145
  dataset_list = [self.ds, self.child_grid.ds]
165
146
  output_filenames = [str(filepath), str(filepath_child_grid)]
166
147
 
167
- saved_filenames = save_datasets(
168
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
169
- )
148
+ saved_filenames = save_datasets(dataset_list, output_filenames)
170
149
 
171
150
  return saved_filenames
172
151
 
@@ -2,24 +2,24 @@ import xarray as xr
2
2
  import numpy as np
3
3
  import logging
4
4
  from dataclasses import dataclass, field
5
- from roms_tools.setup.grid import Grid
5
+ import cartopy.crs as ccrs
6
6
  from datetime import datetime
7
7
  from typing import Dict, Union, List
8
- from roms_tools.setup.datasets import DaiRiverDataset
9
8
  from pathlib import Path
10
9
  import matplotlib.pyplot as plt
10
+ from roms_tools import Grid
11
+ from roms_tools.plot import _get_projection, _add_field_to_ax
12
+ from roms_tools.utils import save_datasets
13
+ from roms_tools.setup.datasets import DaiRiverDataset
11
14
  from roms_tools.setup.utils import (
12
15
  get_target_coords,
13
16
  gc_dist,
14
17
  substitute_nans_by_fillvalue,
15
18
  convert_to_roms_time,
16
- save_datasets,
17
19
  _to_yaml,
18
20
  _from_yaml,
19
21
  get_variable_metadata,
20
22
  )
21
- from roms_tools.setup.plot import _get_projection, _add_field_to_ax
22
- import cartopy.crs as ccrs
23
23
 
24
24
 
25
25
  @dataclass(frozen=True, kw_only=True)
@@ -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.
@@ -416,16 +412,13 @@ class RiverForcing:
416
412
  """Plots the original and updated river locations on a map projection."""
417
413
 
418
414
  field = self.grid.ds.mask_rho
419
- field = field.assign_coords(
420
- {"lon": self.grid.ds.lon_rho, "lat": self.grid.ds.lat_rho}
421
- )
422
415
  vmax = 3
423
416
  vmin = 0
424
417
  cmap = plt.colormaps.get_cmap("Blues")
425
418
  kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
426
419
 
427
- lon_deg = field.lon
428
- lat_deg = field.lat
420
+ lon_deg = self.grid.ds.lon_rho
421
+ lat_deg = self.grid.ds.lat_rho
429
422
 
430
423
  # check if North or South pole are in domain
431
424
  if lat_deg.max().values > 89 or lat_deg.min().values < -89:
@@ -435,6 +428,7 @@ class RiverForcing:
435
428
 
436
429
  if self.grid.straddle:
437
430
  lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
431
+ field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
438
432
 
439
433
  trans = _get_projection(lon_deg, lat_deg)
440
434
 
@@ -594,37 +588,13 @@ class RiverForcing:
594
588
  def save(
595
589
  self,
596
590
  filepath: Union[str, Path],
597
- filepath_grid: Union[str, Path],
598
- np_eta: int = None,
599
- np_xi: int = None,
600
591
  ) -> None:
601
- """Save the river forcing and grid file to netCDF4 files. The grid file is
602
- required because a new field `river_flux` has been added.
603
-
604
- 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:
605
-
606
- 1. **Single File Mode (default)**:
607
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
608
- - The file is named based on the provided `filepath`, with `.nc` automatically appended to the filename.
609
-
610
- 2. **Partitioned Mode**:
611
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned spatially along the `eta` and `xi` axes into tiles.
612
- - 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.
613
593
 
614
594
  Parameters
615
595
  ----------
616
596
  filepath : Union[str, Path]
617
- The base path and filename for the output files. The filenames will include the specified path and the `.nc` extension.
618
- If partitioning is used, additional indices will be appended to the filenames, e.g., `"filepath.0.nc"`, `"filepath.1.nc"`, etc.
619
-
620
- filepath_grid : Union[str, Path]
621
- The base path and filename for saving the grid file.
622
-
623
- np_eta : int, optional
624
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed along the `eta` axis.
625
-
626
- np_xi : int, optional
627
- 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.
628
598
 
629
599
  Returns
630
600
  -------
@@ -634,20 +604,15 @@ class RiverForcing:
634
604
 
635
605
  # Ensure filepath is a Path object
636
606
  filepath = Path(filepath)
637
- filepath_grid = Path(filepath_grid)
638
607
 
639
608
  # Remove ".nc" suffix if present
640
609
  if filepath.suffix == ".nc":
641
610
  filepath = filepath.with_suffix("")
642
- if filepath_grid.suffix == ".nc":
643
- filepath_grid = filepath_grid.with_suffix("")
644
611
 
645
- dataset_list = [self.ds, self.grid.ds]
646
- output_filenames = [str(filepath), str(filepath_grid)]
612
+ dataset_list = [self.ds]
613
+ output_filenames = [str(filepath)]
647
614
 
648
- saved_filenames = save_datasets(
649
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
650
- )
615
+ saved_filenames = save_datasets(dataset_list, output_filenames)
651
616
 
652
617
  return saved_filenames
653
618
 
@@ -1,11 +1,16 @@
1
1
  import xarray as xr
2
2
  import importlib.metadata
3
3
  from dataclasses import dataclass, field
4
- from roms_tools.setup.grid import Grid
5
4
  from datetime import datetime
6
5
  import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from pathlib import Path
8
+ import logging
7
9
  from typing import Dict, Union, List
8
- from roms_tools.setup.regrid import LateralRegrid
10
+ from roms_tools import Grid
11
+ from roms_tools.utils import save_datasets
12
+ from roms_tools.regrid import LateralRegrid
13
+ from roms_tools.plot import _plot
9
14
  from roms_tools.setup.datasets import (
10
15
  ERA5Dataset,
11
16
  ERA5Correction,
@@ -18,15 +23,11 @@ from roms_tools.setup.utils import (
18
23
  interpolate_from_climatology,
19
24
  get_variable_metadata,
20
25
  group_dataset,
21
- save_datasets,
22
26
  rotate_velocities,
23
27
  convert_to_roms_time,
24
28
  _to_yaml,
25
29
  _from_yaml,
26
30
  )
27
- from roms_tools.setup.plot import _plot
28
- import matplotlib.pyplot as plt
29
- from pathlib import Path
30
31
 
31
32
 
32
33
  @dataclass(frozen=True, kw_only=True)
@@ -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):
@@ -434,21 +506,23 @@ class SurfaceForcing:
434
506
  raise ValueError(f"Variable '{var_name}' is not found in dataset.")
435
507
 
436
508
  field = self.ds[var_name].isel(time=time)
509
+
437
510
  if self.use_dask:
438
511
  from dask.diagnostics import ProgressBar
439
512
 
440
513
  with ProgressBar():
441
514
  field = field.load()
442
515
 
443
- title = field.long_name
444
-
445
516
  field = field.where(self.target_coords["mask"])
446
517
 
447
- field = field.assign_coords(
448
- {"lon": self.target_coords["lon"], "lat": self.target_coords["lat"]}
449
- )
518
+ lon_deg = self.target_coords["lon"]
519
+ lat_deg = self.target_coords["lat"]
520
+ if self.grid.straddle:
521
+ lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
522
+ field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
523
+
524
+ title = field.long_name
450
525
 
451
- # choose colorbar
452
526
  if var_name in ["uwnd", "vwnd"]:
453
527
  vmax = max(field.max().values, -field.min().values)
454
528
  vmin = -vmax
@@ -465,53 +539,35 @@ class SurfaceForcing:
465
539
  kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
466
540
 
467
541
  _plot(
468
- self.grid.ds,
469
542
  field=field,
470
- straddle=self.grid.straddle,
471
543
  title=title,
472
- kwargs=kwargs,
473
544
  c="g",
545
+ kwargs=kwargs,
474
546
  )
475
547
 
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