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
@@ -5,32 +5,34 @@ import logging
5
5
  import importlib.metadata
6
6
  from typing import Dict, Union, List
7
7
  from dataclasses import dataclass, field
8
- from roms_tools.setup.grid import Grid
9
- from roms_tools.setup.regrid import LateralRegrid, VerticalRegrid
10
8
  from datetime import datetime
9
+ import matplotlib.pyplot as plt
10
+ from pathlib import Path
11
+ from roms_tools import Grid
12
+ from roms_tools.regrid import LateralRegrid, VerticalRegrid
13
+ from roms_tools.utils import save_datasets
14
+ from roms_tools.vertical_coordinate import compute_depth
15
+ from roms_tools.plot import _section_plot, _line_plot
16
+ from roms_tools.utils import (
17
+ interpolate_from_rho_to_u,
18
+ interpolate_from_rho_to_v,
19
+ transpose_dimensions,
20
+ )
11
21
  from roms_tools.setup.datasets import GLORYSDataset, CESMBGCDataset
12
- from roms_tools.setup.vertical_coordinate import compute_depth
13
22
  from roms_tools.setup.utils import (
14
23
  get_variable_metadata,
15
24
  group_dataset,
16
- save_datasets,
17
25
  get_target_coords,
18
26
  rotate_velocities,
19
27
  compute_barotropic_velocity,
20
- transpose_dimensions,
21
28
  one_dim_fill,
22
29
  nan_check,
23
30
  substitute_nans_by_fillvalue,
24
- interpolate_from_rho_to_u,
25
- interpolate_from_rho_to_v,
26
31
  convert_to_roms_time,
27
32
  get_boundary_coords,
28
33
  _to_yaml,
29
34
  _from_yaml,
30
35
  )
31
- from roms_tools.setup.plot import _section_plot, _line_plot
32
- import matplotlib.pyplot as plt
33
- from pathlib import Path
34
36
 
35
37
 
36
38
  @dataclass(frozen=True, kw_only=True)
@@ -64,10 +66,14 @@ class BoundaryForcing:
64
66
  - "physics": for physical atmospheric forcing.
65
67
  - "bgc": for biogeochemical forcing.
66
68
 
67
- apply_2d_horizontal_fill: bool, optional
69
+ apply_2d_horizontal_fill : bool, optional
68
70
  Indicates whether to perform a two-dimensional horizontal fill on the source data prior to regridding to boundaries.
69
71
  If `False`, a one-dimensional horizontal fill is performed separately on each of the four regridded boundaries.
70
72
  Defaults to `False`.
73
+ adjust_depth_for_sea_surface_height : bool, optional
74
+ Whether to account for sea surface height (`zeta`) variations when computing depth coordinates.
75
+ This adjustment is only applicable for `type="physics"`, as for biogeochemical fields usually `zeta` is not available.
76
+ Defaults to `False`.
71
77
  model_reference_date : datetime, optional
72
78
  Reference date for the model. Default is January 1, 2000.
73
79
  use_dask: bool, optional
@@ -103,6 +109,7 @@ class BoundaryForcing:
103
109
  source: Dict[str, Union[str, Path, List[Union[str, Path]]]]
104
110
  type: str = "physics"
105
111
  apply_2d_horizontal_fill: bool = False
112
+ adjust_depth_for_sea_surface_height: bool = False
106
113
  model_reference_date: datetime = datetime(2000, 1, 1)
107
114
  use_dask: bool = False
108
115
  bypass_validation: bool = False
@@ -112,6 +119,9 @@ class BoundaryForcing:
112
119
  def __post_init__(self):
113
120
 
114
121
  self._input_checks()
122
+ # Dataset for depth coordinates
123
+ object.__setattr__(self, "ds_depth_coords", xr.Dataset())
124
+
115
125
  target_coords = get_target_coords(self.grid)
116
126
 
117
127
  data = self._get_data()
@@ -152,13 +162,14 @@ class BoundaryForcing:
152
162
 
153
163
  processed_fields = {}
154
164
 
155
- # lateral regridding of vector fields
156
- vector_var_names = [
157
- name
158
- for name, info in self.variable_info.items()
159
- if info["is_vector"]
160
- ]
161
- if len(vector_var_names) > 0:
165
+ # vector fields (velocities) are only present in physics datasets
166
+ if self.type == "physics":
167
+ # lateral regridding of vector fields
168
+ vector_var_names = [
169
+ name
170
+ for name, info in self.variable_info.items()
171
+ if info["is_vector"]
172
+ ]
162
173
  lon = target_coords["lon"].isel(
163
174
  **self.bdry_coords["vector"][direction]
164
175
  )
@@ -174,6 +185,13 @@ class BoundaryForcing:
174
185
  bdry_data.ds[bdry_data.var_names[var_name]]
175
186
  )
176
187
 
188
+ if self.adjust_depth_for_sea_surface_height:
189
+ # Regrid sea surface height ('zeta') onto a 2-cell-wide margin.
190
+ # This is needed to correctly infer depth coordinates at u- and v-points along the boundary.
191
+ zeta_vector = lateral_regrid.apply(
192
+ bdry_data.ds[bdry_data.var_names["zeta"]]
193
+ )
194
+
177
195
  # lateral regridding of tracer fields
178
196
  tracer_var_names = [
179
197
  name
@@ -207,6 +225,9 @@ class BoundaryForcing:
207
225
  angle,
208
226
  interpolate=True,
209
227
  )
228
+ if self.adjust_depth_for_sea_surface_height:
229
+ zeta_u = interpolate_from_rho_to_u(zeta_vector)
230
+ zeta_v = interpolate_from_rho_to_v(zeta_vector)
210
231
 
211
232
  # selection of outermost margin for u/v variables
212
233
  for var_name in self.variable_info.keys():
@@ -216,6 +237,9 @@ class BoundaryForcing:
216
237
  processed_fields[var_name] = processed_fields[
217
238
  var_name
218
239
  ].isel(**self.bdry_coords[location][direction])
240
+ if self.adjust_depth_for_sea_surface_height:
241
+ zeta_u = zeta_u.isel(**self.bdry_coords["u"][direction])
242
+ zeta_v = zeta_v.isel(**self.bdry_coords["v"][direction])
219
243
 
220
244
  if not self.apply_2d_horizontal_fill:
221
245
  self._validate_1d_fill(
@@ -223,7 +247,20 @@ class BoundaryForcing:
223
247
  direction,
224
248
  bdry_data.dim_names["depth"],
225
249
  )
226
- processed_fields = apply_1d_horizontal_fill(processed_fields)
250
+ for var_name in processed_fields.keys():
251
+ processed_fields[var_name] = apply_1d_horizontal_fill(
252
+ processed_fields[var_name]
253
+ )
254
+ if self.adjust_depth_for_sea_surface_height:
255
+ zeta_u = apply_1d_horizontal_fill(zeta_u)
256
+ zeta_v = apply_1d_horizontal_fill(zeta_v)
257
+
258
+ if self.adjust_depth_for_sea_surface_height:
259
+ zeta = processed_fields["zeta"]
260
+ else:
261
+ zeta = 0
262
+ zeta_u = 0
263
+ zeta_v = 0
227
264
 
228
265
  var_names_dict = {}
229
266
  for location in ["rho", "u", "v"]:
@@ -232,24 +269,22 @@ class BoundaryForcing:
232
269
  for name, info in self.variable_info.items()
233
270
  if info["location"] == location and info["is_3d"]
234
271
  ]
272
+
235
273
  # compute layer depth coordinates
274
+ if len(var_names_dict["rho"]) > 0:
275
+ self._get_depth_coordinates(zeta, direction, "rho", "layer")
276
+ self._get_depth_coordinates(
277
+ zeta, direction, "rho", "interface"
278
+ ) # only necessary for plotting
236
279
  if len(var_names_dict["u"]) > 0 or len(var_names_dict["v"]) > 0:
237
- self._get_vertical_coordinates(
238
- type="layer",
239
- direction=direction,
240
- additional_locations=["u", "v"],
241
- )
242
- else:
243
- if len(var_names_dict["rho"]) > 0:
244
- self._get_vertical_coordinates(
245
- type="layer", direction=direction, additional_locations=[]
246
- )
280
+ self._get_depth_coordinates(zeta_u, direction, "u", "layer")
281
+ self._get_depth_coordinates(zeta_v, direction, "v", "layer")
247
282
 
248
283
  # vertical regridding
249
284
  for location in ["rho", "u", "v"]:
250
285
  if len(var_names_dict[location]) > 0:
251
286
  vertical_regrid = VerticalRegrid(
252
- self.grid.ds[f"layer_depth_{location}_{direction}"],
287
+ self.ds_depth_coords[f"layer_depth_{location}_{direction}"],
253
288
  bdry_data.ds[bdry_data.dim_names["depth"]],
254
289
  )
255
290
  for var_name in var_names_dict[location]:
@@ -260,17 +295,16 @@ class BoundaryForcing:
260
295
 
261
296
  # compute barotropic velocities
262
297
  if "u" in self.variable_info and "v" in self.variable_info:
263
- self._get_vertical_coordinates(
264
- type="interface",
265
- direction=direction,
266
- additional_locations=["u", "v"],
267
- )
298
+ self._get_depth_coordinates(zeta_u, direction, "u", "interface")
299
+ self._get_depth_coordinates(zeta_v, direction, "v", "interface")
268
300
  for location in ["u", "v"]:
269
301
  processed_fields[
270
302
  f"{location}bar"
271
303
  ] = compute_barotropic_velocity(
272
304
  processed_fields[location],
273
- self.grid.ds[f"interface_depth_{location}_{direction}"],
305
+ self.ds_depth_coords[
306
+ f"interface_depth_{location}_{direction}"
307
+ ],
274
308
  )
275
309
 
276
310
  # Reorder dimensions
@@ -312,6 +346,29 @@ class BoundaryForcing:
312
346
  {**self.source, "climatology": self.source.get("climatology", False)},
313
347
  )
314
348
 
349
+ # Ensure adjust_depth_for_sea_surface_height is only used with type="physics"
350
+ if self.type == "bgc" and self.adjust_depth_for_sea_surface_height:
351
+ logging.warning(
352
+ "adjust_depth_for_sea_surface_height is not applicable for BGC fields. "
353
+ "Setting it to False."
354
+ )
355
+ object.__setattr__(self, "adjust_depth_for_sea_surface_height", False)
356
+ elif self.adjust_depth_for_sea_surface_height:
357
+ logging.info("Sea surface height will be used to adjust depth coordinates.")
358
+ else:
359
+ logging.info(
360
+ "Sea surface height will NOT be used to adjust depth coordinates."
361
+ )
362
+
363
+ if self.apply_2d_horizontal_fill:
364
+ logging.info(
365
+ "Applying 2D horizontal fill to the source data before regridding."
366
+ )
367
+ else:
368
+ logging.info(
369
+ "Applying 1D horizontal fill separately to each regridded boundary."
370
+ )
371
+
315
372
  def _get_data(self):
316
373
 
317
374
  data_dict = {
@@ -487,118 +544,80 @@ class BoundaryForcing:
487
544
 
488
545
  object.__setattr__(self, "bdry_coords", bdry_coords)
489
546
 
490
- def _get_vertical_coordinates(
491
- self, type, direction, additional_locations=["u", "v"]
492
- ):
493
- """Retrieve layer and interface depth coordinates for a specified grid boundary.
547
+ def _get_depth_coordinates(
548
+ self,
549
+ zeta: xr.DataArray | float,
550
+ direction: str,
551
+ location: str,
552
+ depth_type: str = "layer",
553
+ ) -> None:
554
+ """Compute and store depth coordinates for a specified boundary direction, grid
555
+ location, and depth type.
494
556
 
495
- This method computes and updates the layer and interface depth coordinates along a specified
496
- boundary (north, south, east, or west). It handles depth calculations for rho points and
497
- additional specified locations (u and v).
557
+ This method efficiently computes depth coordinates along the specified boundary without
558
+ interpolating the entire domain topography. The computed depth values are stored in
559
+ `self.ds_depth_coords`.
498
560
 
499
561
  Parameters
500
562
  ----------
501
- type : str
502
- The type of depth coordinate to retrieve. Valid options are:
503
- - "layer": Retrieves layer depth coordinates.
504
- - "interface": Retrieves interface depth coordinates.
505
-
563
+ zeta : xr.DataArray or float
564
+ Free-surface elevation (`zeta`). Can be:
565
+ - A scalar float value (constant sea surface height).
566
+ - An `xarray.DataArray` with spatial variations. If provided as an array, it may have a
567
+ time dimension, but must be **1D** (varying only in time).
506
568
  direction : str
507
- The direction of the boundary to retrieve coordinates for. Valid options are:
569
+ The boundary direction for which depth coordinates are computed. Must be one of:
508
570
  - "north"
509
571
  - "south"
510
572
  - "east"
511
573
  - "west"
574
+ location : str
575
+ Grid location at which depth is computed. Must be one of:
576
+ - `"rho"`: Depth at scalar grid points.
577
+ - `"u"`: Depth at U-velocity grid points.
578
+ - `"v"`: Depth at V-velocity grid points.
579
+ depth_type : str, optional
580
+ Type of depth coordinate to compute, either:
581
+ - `"layer"` (default): Depth at vertical layer midpoints.
582
+ - `"interface"`: Depth at vertical layer interfaces.
512
583
 
513
- additional_locations : list of str, optional
514
- Specifies additional locations to compute depth coordinates for. Default is ["u", "v"].
515
- Valid options include:
516
- - "u": Computes depth coordinates for u points.
517
- - "v": Computes depth coordinates for v points.
518
-
519
- Updates
520
- -------
521
- self.grid.ds : xarray.Dataset
522
- The dataset is updated with the following vertical depth coordinates:
523
- - f"{type}_depth_rho_{direction}": Depth coordinates at rho points.
524
- - f"{type}_depth_u_{direction}": Depth coordinates at u points (if applicable).
525
- - f"{type}_depth_v_{direction}": Depth coordinates at v points (if applicable).
584
+ Notes
585
+ -----
586
+ - This method is optimized for boundary computations by selecting only the relevant margin
587
+ (2 grid cells) instead of interpolating the entire domain.
526
588
  """
527
-
528
- layer_vars = []
529
- for location in ["rho"] + additional_locations:
530
- layer_vars.append(f"{type}_depth_{location}_{direction}")
531
-
532
- if all(layer_var in self.grid.ds for layer_var in layer_vars):
533
- # Vertical coordinate data already exists
534
- pass
535
-
536
- elif f"{type}_depth_rho" in self.grid.ds:
537
- depth = self.grid.ds[f"{type}_depth_rho"]
538
- depth.attrs["long_name"] = f"{type} depth at rho-points"
539
- depth.attrs["units"] = "m"
540
- self.grid.ds[f"{type}_depth_rho_{direction}"] = depth.isel(
541
- **self.bdry_coords["rho"][direction]
542
- )
543
-
544
- if "u" in additional_locations or "v" in additional_locations:
589
+ key = f"{depth_type}_depth_{location}_{direction}"
590
+ if key not in self.ds_depth_coords:
591
+ if location in ["u", "v"]:
545
592
  # selection of margin consisting of 2 grid cells
546
- depth = depth.isel(**self.bdry_coords["vector"][direction])
547
- # interpolation
548
- if "u" in additional_locations:
549
- depth_u = interpolate_from_rho_to_u(depth)
550
- depth_u.attrs["long_name"] = f"{type} depth at u-points"
551
- depth_u.attrs["units"] = "m"
552
- self.grid.ds[f"{type}_depth_u_{direction}"] = depth_u.isel(
553
- **self.bdry_coords["u"][direction]
554
- )
555
- if "v" in additional_locations:
556
- depth_v = interpolate_from_rho_to_v(depth)
557
- depth_v.attrs["long_name"] = f"{type} depth at v-points"
558
- depth_v.attrs["units"] = "m"
559
- self.grid.ds[f"{type}_depth_v_{direction}"] = depth_v.isel(
560
- **self.bdry_coords["v"][direction]
561
- )
562
- else:
563
- if "u" in additional_locations or "v" in additional_locations:
564
593
  h = self.grid.ds["h"].isel(**self.bdry_coords["vector"][direction])
594
+ if location == "u":
595
+ h = interpolate_from_rho_to_u(h)
596
+ h = h.isel(**self.bdry_coords["u"][direction])
597
+ elif location == "v":
598
+ h = interpolate_from_rho_to_v(h)
599
+ h = h.isel(**self.bdry_coords["v"][direction])
565
600
  else:
566
601
  h = self.grid.ds["h"].isel(**self.bdry_coords["rho"][direction])
567
- if type == "layer":
602
+
603
+ if depth_type == "layer":
568
604
  depth = compute_depth(
569
- 0, h, self.grid.hc, self.grid.ds.Cs_r, self.grid.ds.sigma_r
605
+ zeta, h, self.grid.hc, self.grid.ds.Cs_r, self.grid.ds.sigma_r
570
606
  )
571
607
  else:
572
608
  depth = compute_depth(
573
- 0, h, self.grid.hc, self.grid.ds.Cs_w, self.grid.ds.sigma_w
609
+ zeta, h, self.grid.hc, self.grid.ds.Cs_w, self.grid.ds.sigma_w
574
610
  )
575
611
 
576
- if "u" in additional_locations or "v" in additional_locations:
577
- depth.attrs["long_name"] = f"{type} depth at rho-points"
578
- depth.attrs["units"] = "m"
579
- self.grid.ds[f"{type}_depth_rho_{direction}"] = depth.isel(
580
- **self.bdry_coords["rho"][direction]
581
- )
582
- # selection of margin consisting of 2 grid cells
583
- depth = depth.isel(**self.bdry_coords["vector"][direction])
584
- # interpolation
585
- depth_u = interpolate_from_rho_to_u(depth)
586
- depth_v = interpolate_from_rho_to_v(depth)
587
- # selection of outermost margin
588
- depth_u.attrs["long_name"] = f"{type} depth at u-points"
589
- depth_u.attrs["units"] = "m"
590
- self.grid.ds[f"{type}_depth_u_{direction}"] = depth_u.isel(
591
- **self.bdry_coords["u"][direction]
592
- )
593
- depth_v.attrs["long_name"] = f"{type} depth at v-points"
594
- depth_v.attrs["units"] = "m"
595
- self.grid.ds[f"{type}_depth_v_{direction}"] = depth_v.isel(
596
- **self.bdry_coords["v"][direction]
597
- )
598
- else:
599
- depth.attrs["long_name"] = f"{type} depth at rho-points"
600
- depth.attrs["units"] = "m"
601
- self.grid.ds[f"{type}_depth_rho_{direction}"] = depth
612
+ # Add metadata
613
+ depth.attrs.update(
614
+ {
615
+ "long_name": f"{depth_type} depth at {location}-points along {direction}ern boundary",
616
+ "units": "m",
617
+ }
618
+ )
619
+
620
+ self.ds_depth_coords[key] = depth
602
621
 
603
622
  def _add_global_metadata(self, data, ds=None):
604
623
 
@@ -615,6 +634,10 @@ class BoundaryForcing:
615
634
  ds.attrs["end_time"] = str(self.end_time)
616
635
  ds.attrs["source"] = self.source["name"]
617
636
  ds.attrs["model_reference_date"] = str(self.model_reference_date)
637
+ ds.attrs["apply_2d_horizontal_fill"] = str(self.apply_2d_horizontal_fill)
638
+ ds.attrs["adjust_depth_for_sea_surface_height"] = str(
639
+ self.adjust_depth_for_sea_surface_height
640
+ )
618
641
 
619
642
  ds.attrs["theta_s"] = self.grid.ds.attrs["theta_s"]
620
643
  ds.attrs["theta_b"] = self.grid.ds.attrs["theta_b"]
@@ -827,11 +850,21 @@ class BoundaryForcing:
827
850
  var_name_wo_direction, direction = var_name.split("_")
828
851
  location = self.variable_info[var_name_wo_direction]["location"]
829
852
 
853
+ # Find correct mask
854
+ if location == "rho":
855
+ mask = self.grid.ds.mask_rho
856
+ elif location == "u":
857
+ mask = self.grid.ds.mask_u
858
+ elif location == "v":
859
+ mask = self.grid.ds.mask_v
860
+
861
+ mask = mask.isel(**self.bdry_coords[location][direction])
862
+
830
863
  if "s_rho" in field.dims:
831
- field = field.assign_coords(
832
- {"layer_depth": self.grid.ds[f"layer_depth_{location}_{direction}"]}
833
- )
834
- # chose colorbar
864
+ layer_depth = self.ds_depth_coords[f"layer_depth_{location}_{direction}"]
865
+ if self.adjust_depth_for_sea_surface_height:
866
+ layer_depth = layer_depth.isel(time=time)
867
+ field = field.assign_coords({"layer_depth": layer_depth})
835
868
  if var_name.startswith(("u", "v", "ubar", "vbar", "zeta")):
836
869
  vmax = max(field.max().values, -field.min().values)
837
870
  vmin = -vmax
@@ -848,19 +881,11 @@ class BoundaryForcing:
848
881
 
849
882
  if len(field.dims) == 2:
850
883
  if layer_contours:
851
- if location in ["u", "v"]:
852
- additional_locations = ["u", "v"]
853
- else:
854
- additional_locations = []
855
- self._get_vertical_coordinates(
856
- type="interface",
857
- direction=direction,
858
- additional_locations=additional_locations,
859
- )
860
-
861
- interface_depth = self.grid.ds[
884
+ interface_depth = self.ds_depth_coords[
862
885
  f"interface_depth_{location}_{direction}"
863
886
  ]
887
+ if self.adjust_depth_for_sea_surface_height:
888
+ interface_depth = interface_depth.isel(time=time)
864
889
  # restrict number of layer_contours to 10 for the sake of plot clearity
865
890
  nr_layers = len(interface_depth["s_w"])
866
891
  selected_layers = np.linspace(
@@ -879,42 +904,26 @@ class BoundaryForcing:
879
904
  ax=ax,
880
905
  )
881
906
  else:
882
- _line_plot(field, title=title, ax=ax)
907
+ _line_plot(field.where(mask), title=title, ax=ax)
883
908
 
884
909
  def save(
885
910
  self,
886
911
  filepath: Union[str, Path],
887
- np_eta: int = None,
888
- np_xi: int = None,
889
- group: bool = False,
912
+ group: bool = True,
890
913
  ) -> None:
891
914
  """Save the boundary forcing fields to one or more netCDF4 files.
892
915
 
893
- This method saves the dataset either as a single file or as multiple files depending on the partitioning and grouping options.
894
- The dataset can be saved in two modes:
895
-
896
- 1. **Single File Mode (default)**:
897
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
898
- - The file is named based on the `filepath`, with `.nc` automatically appended.
899
-
900
- 2. **Partitioned Mode**:
901
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned into spatial tiles along the `eta` and `xi` axes.
902
- - 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"`).
903
-
904
- Additionally, if `group` is set to `True`, the dataset is first grouped into temporal subsets, resulting in multiple grouped files before partitioning and saving.
916
+ This method saves the dataset to disk as either a single netCDF4 file or multiple files, depending on the `group` parameter.
917
+ If `group` is `True`, the dataset is divided into subsets (e.g., monthly or yearly) based on the temporal frequency
918
+ of the data, and each subset is saved to a separate file.
905
919
 
906
920
  Parameters
907
921
  ----------
908
922
  filepath : Union[str, Path]
909
- The base path and filename for the output files. The format of the filenames depends on whether partitioning is used
910
- and the temporal range of the data. For partitioned datasets, files will be named with an additional index, e.g.,
911
- `"filepath_YYYYMM.0.nc"`, `"filepath_YYYYMM.1.nc"`, etc.
912
- np_eta : int, optional
913
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed.
914
- np_xi : int, optional
915
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed.
916
- group: bool, optional
917
- If `True`, groups the dataset into multiple files based on temporal data frequency. Defaults to `False`.
923
+ The base path and filename for the output file(s). If `group` is `True`, the filenames will include additional
924
+ time-based information (e.g., year or month) to distinguish the subsets.
925
+ group : bool, optional
926
+ Whether to divide the dataset into multiple files based on temporal frequency. Defaults to `True`.
918
927
 
919
928
  Returns
920
929
  -------
@@ -929,12 +938,6 @@ class BoundaryForcing:
929
938
  if filepath.suffix == ".nc":
930
939
  filepath = filepath.with_suffix("")
931
940
 
932
- if self.use_dask:
933
- from dask.diagnostics import ProgressBar
934
-
935
- with ProgressBar():
936
- self.ds.load()
937
-
938
941
  if group:
939
942
  dataset_list, output_filenames = group_dataset(self.ds, str(filepath))
940
943
  else:
@@ -942,7 +945,7 @@ class BoundaryForcing:
942
945
  output_filenames = [str(filepath)]
943
946
 
944
947
  saved_filenames = save_datasets(
945
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
948
+ dataset_list, output_filenames, use_dask=self.use_dask
946
949
  )
947
950
 
948
951
  return saved_filenames
@@ -995,19 +998,19 @@ class BoundaryForcing:
995
998
  )
996
999
 
997
1000
 
998
- def apply_1d_horizontal_fill(processed_fields: dict) -> dict:
999
- """Forward and backward fill NaN values in horizontal direction for open boundaries.
1001
+ def apply_1d_horizontal_fill(data_array: xr.DataArray) -> xr.DataArray:
1002
+ """Forward and backward fill NaN values in a single horizontal dimension for open
1003
+ boundaries.
1000
1004
 
1001
1005
  Parameters
1002
1006
  ----------
1003
- processed_fields : dict
1004
- A dictionary of variables to be updated, where each value is an
1005
- `xarray.DataArray`.
1007
+ data_array : xarray.DataArray
1008
+ The data array to be updated.
1006
1009
 
1007
1010
  Returns
1008
1011
  -------
1009
- dict of str : xarray.DataArray
1010
- The updated dictionary of variables, with NaN values filled.
1012
+ xarray.DataArray
1013
+ The updated data array with NaN values filled.
1011
1014
 
1012
1015
  Raises
1013
1016
  ------
@@ -1016,29 +1019,22 @@ def apply_1d_horizontal_fill(processed_fields: dict) -> dict:
1016
1019
  """
1017
1020
 
1018
1021
  horizontal_dims = ["eta_rho", "eta_v", "xi_rho", "xi_u"]
1022
+ selected_horizontal_dim = None
1019
1023
 
1020
- for var_name in processed_fields.keys():
1021
- selected_horizontal_dim = None
1022
- # Determine the horizontal dimension to fill
1023
- for dim in horizontal_dims:
1024
- if dim in processed_fields[var_name].dims:
1025
- if selected_horizontal_dim is not None:
1026
- raise ValueError(
1027
- f"More than one horizontal dimension found in variable '{var_name}'."
1028
- )
1029
- selected_horizontal_dim = dim
1030
-
1031
- if selected_horizontal_dim is None:
1032
- raise ValueError(
1033
- f"No valid horizontal dimension found for variable '{var_name}'."
1034
- )
1024
+ # Determine the horizontal dimension to fill
1025
+ for dim in horizontal_dims:
1026
+ if dim in data_array.dims:
1027
+ if selected_horizontal_dim is not None:
1028
+ raise ValueError(
1029
+ f"More than one horizontal dimension found in variable '{data_array.name}'."
1030
+ )
1031
+ selected_horizontal_dim = dim
1035
1032
 
1036
- # Forward and backward fill in the horizontal direction
1037
- filled = one_dim_fill(
1038
- processed_fields[var_name], selected_horizontal_dim, direction="forward"
1039
- )
1040
- processed_fields[var_name] = one_dim_fill(
1041
- filled, selected_horizontal_dim, direction="backward"
1033
+ if selected_horizontal_dim is None:
1034
+ raise ValueError(
1035
+ f"No valid horizontal dimension found for variable '{data_array.name}'."
1042
1036
  )
1043
1037
 
1044
- return processed_fields
1038
+ # Forward and backward fill in the horizontal direction
1039
+ filled = one_dim_fill(data_array, selected_horizontal_dim, direction="forward")
1040
+ return one_dim_fill(filled, selected_horizontal_dim, direction="backward")