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
ci/environment.yml CHANGED
@@ -8,6 +8,7 @@ dependencies:
8
8
  - zarr
9
9
  - pytest
10
10
  - pytest-xdist
11
+ - h5py
11
12
  - flake8
12
13
  - black
13
14
  - pre-commit==3.8.0
roms_tools/__init__.py CHANGED
@@ -15,6 +15,7 @@ from roms_tools.setup.initial_conditions import InitialConditions # noqa: F401
15
15
  from roms_tools.setup.boundary_forcing import BoundaryForcing # noqa: F401
16
16
  from roms_tools.setup.river_forcing import RiverForcing # noqa: F401
17
17
  from roms_tools.setup.nesting import Nesting # noqa: F401
18
+ from roms_tools.tiling.partition import partition_netcdf # noqa: F401
18
19
  from roms_tools.analysis.roms_output import ROMSOutput # noqa: F401
19
20
 
20
21
  # Configure logging when the package is imported
@@ -12,7 +12,6 @@ from datetime import datetime, timedelta
12
12
  from roms_tools import Grid
13
13
  from roms_tools.plot import _plot, _section_plot, _profile_plot, _line_plot
14
14
  from roms_tools.vertical_coordinate import (
15
- add_depth_coordinates_to_dataset,
16
15
  compute_depth_coordinates,
17
16
  )
18
17
 
@@ -186,24 +185,26 @@ class ROMSOutput:
186
185
 
187
186
  if compute_layer_depth:
188
187
  layer_depth = compute_depth_coordinates(
189
- self.ds.isel(time=time),
190
188
  self.grid.ds,
189
+ self.ds.zeta.isel(time=time),
191
190
  depth_type="layer",
192
191
  location=loc,
193
- s=s,
194
192
  eta=eta,
195
193
  xi=xi,
196
194
  )
195
+ if s is not None:
196
+ layer_depth = layer_depth.isel(s_rho=s)
197
197
  if compute_interface_depth:
198
198
  interface_depth = compute_depth_coordinates(
199
- self.ds.isel(time=time),
200
199
  self.grid.ds,
200
+ self.ds.zeta.isel(time=time),
201
201
  depth_type="interface",
202
202
  location=loc,
203
- s=s,
204
203
  eta=eta,
205
204
  xi=xi,
206
205
  )
206
+ if s is not None:
207
+ interface_depth = interface_depth.isel(s_w=s)
207
208
 
208
209
  # Slice the field as desired
209
210
  title = field.long_name
@@ -331,7 +332,10 @@ class ROMSOutput:
331
332
  This method uses the `compute_and_update_depth_coordinates` function to perform calculations and updates.
332
333
  """
333
334
 
334
- add_depth_coordinates_to_dataset(self.ds, self.grid.ds, depth_type, locations)
335
+ for location in locations:
336
+ self.ds[f"{depth_type}_depth_{location}"] = compute_depth_coordinates(
337
+ self.grid.ds, self.ds.zeta, depth_type, location
338
+ )
335
339
 
336
340
  def _load_model_output(self) -> xr.Dataset:
337
341
  """Load the model output based on the type."""
@@ -10,6 +10,7 @@ import matplotlib.pyplot as plt
10
10
  from pathlib import Path
11
11
  from roms_tools import Grid
12
12
  from roms_tools.regrid import LateralRegrid, VerticalRegrid
13
+ from roms_tools.utils import save_datasets
13
14
  from roms_tools.vertical_coordinate import compute_depth
14
15
  from roms_tools.plot import _section_plot, _line_plot
15
16
  from roms_tools.utils import (
@@ -21,7 +22,6 @@ from roms_tools.setup.datasets import GLORYSDataset, CESMBGCDataset
21
22
  from roms_tools.setup.utils import (
22
23
  get_variable_metadata,
23
24
  group_dataset,
24
- save_datasets,
25
25
  get_target_coords,
26
26
  rotate_velocities,
27
27
  compute_barotropic_velocity,
@@ -66,10 +66,14 @@ class BoundaryForcing:
66
66
  - "physics": for physical atmospheric forcing.
67
67
  - "bgc": for biogeochemical forcing.
68
68
 
69
- apply_2d_horizontal_fill: bool, optional
69
+ apply_2d_horizontal_fill : bool, optional
70
70
  Indicates whether to perform a two-dimensional horizontal fill on the source data prior to regridding to boundaries.
71
71
  If `False`, a one-dimensional horizontal fill is performed separately on each of the four regridded boundaries.
72
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`.
73
77
  model_reference_date : datetime, optional
74
78
  Reference date for the model. Default is January 1, 2000.
75
79
  use_dask: bool, optional
@@ -105,6 +109,7 @@ class BoundaryForcing:
105
109
  source: Dict[str, Union[str, Path, List[Union[str, Path]]]]
106
110
  type: str = "physics"
107
111
  apply_2d_horizontal_fill: bool = False
112
+ adjust_depth_for_sea_surface_height: bool = False
108
113
  model_reference_date: datetime = datetime(2000, 1, 1)
109
114
  use_dask: bool = False
110
115
  bypass_validation: bool = False
@@ -114,6 +119,9 @@ class BoundaryForcing:
114
119
  def __post_init__(self):
115
120
 
116
121
  self._input_checks()
122
+ # Dataset for depth coordinates
123
+ object.__setattr__(self, "ds_depth_coords", xr.Dataset())
124
+
117
125
  target_coords = get_target_coords(self.grid)
118
126
 
119
127
  data = self._get_data()
@@ -154,13 +162,14 @@ class BoundaryForcing:
154
162
 
155
163
  processed_fields = {}
156
164
 
157
- # lateral regridding of vector fields
158
- vector_var_names = [
159
- name
160
- for name, info in self.variable_info.items()
161
- if info["is_vector"]
162
- ]
163
- 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
+ ]
164
173
  lon = target_coords["lon"].isel(
165
174
  **self.bdry_coords["vector"][direction]
166
175
  )
@@ -176,6 +185,13 @@ class BoundaryForcing:
176
185
  bdry_data.ds[bdry_data.var_names[var_name]]
177
186
  )
178
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
+
179
195
  # lateral regridding of tracer fields
180
196
  tracer_var_names = [
181
197
  name
@@ -209,6 +225,9 @@ class BoundaryForcing:
209
225
  angle,
210
226
  interpolate=True,
211
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)
212
231
 
213
232
  # selection of outermost margin for u/v variables
214
233
  for var_name in self.variable_info.keys():
@@ -218,6 +237,9 @@ class BoundaryForcing:
218
237
  processed_fields[var_name] = processed_fields[
219
238
  var_name
220
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])
221
243
 
222
244
  if not self.apply_2d_horizontal_fill:
223
245
  self._validate_1d_fill(
@@ -225,7 +247,20 @@ class BoundaryForcing:
225
247
  direction,
226
248
  bdry_data.dim_names["depth"],
227
249
  )
228
- 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
229
264
 
230
265
  var_names_dict = {}
231
266
  for location in ["rho", "u", "v"]:
@@ -234,24 +269,22 @@ class BoundaryForcing:
234
269
  for name, info in self.variable_info.items()
235
270
  if info["location"] == location and info["is_3d"]
236
271
  ]
272
+
237
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
238
279
  if len(var_names_dict["u"]) > 0 or len(var_names_dict["v"]) > 0:
239
- self._get_vertical_coordinates(
240
- type="layer",
241
- direction=direction,
242
- additional_locations=["u", "v"],
243
- )
244
- else:
245
- if len(var_names_dict["rho"]) > 0:
246
- self._get_vertical_coordinates(
247
- type="layer", direction=direction, additional_locations=[]
248
- )
280
+ self._get_depth_coordinates(zeta_u, direction, "u", "layer")
281
+ self._get_depth_coordinates(zeta_v, direction, "v", "layer")
249
282
 
250
283
  # vertical regridding
251
284
  for location in ["rho", "u", "v"]:
252
285
  if len(var_names_dict[location]) > 0:
253
286
  vertical_regrid = VerticalRegrid(
254
- self.grid.ds[f"layer_depth_{location}_{direction}"],
287
+ self.ds_depth_coords[f"layer_depth_{location}_{direction}"],
255
288
  bdry_data.ds[bdry_data.dim_names["depth"]],
256
289
  )
257
290
  for var_name in var_names_dict[location]:
@@ -262,17 +295,16 @@ class BoundaryForcing:
262
295
 
263
296
  # compute barotropic velocities
264
297
  if "u" in self.variable_info and "v" in self.variable_info:
265
- self._get_vertical_coordinates(
266
- type="interface",
267
- direction=direction,
268
- additional_locations=["u", "v"],
269
- )
298
+ self._get_depth_coordinates(zeta_u, direction, "u", "interface")
299
+ self._get_depth_coordinates(zeta_v, direction, "v", "interface")
270
300
  for location in ["u", "v"]:
271
301
  processed_fields[
272
302
  f"{location}bar"
273
303
  ] = compute_barotropic_velocity(
274
304
  processed_fields[location],
275
- self.grid.ds[f"interface_depth_{location}_{direction}"],
305
+ self.ds_depth_coords[
306
+ f"interface_depth_{location}_{direction}"
307
+ ],
276
308
  )
277
309
 
278
310
  # Reorder dimensions
@@ -314,6 +346,29 @@ class BoundaryForcing:
314
346
  {**self.source, "climatology": self.source.get("climatology", False)},
315
347
  )
316
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
+
317
372
  def _get_data(self):
318
373
 
319
374
  data_dict = {
@@ -489,118 +544,80 @@ class BoundaryForcing:
489
544
 
490
545
  object.__setattr__(self, "bdry_coords", bdry_coords)
491
546
 
492
- def _get_vertical_coordinates(
493
- self, type, direction, additional_locations=["u", "v"]
494
- ):
495
- """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.
496
556
 
497
- This method computes and updates the layer and interface depth coordinates along a specified
498
- boundary (north, south, east, or west). It handles depth calculations for rho points and
499
- 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`.
500
560
 
501
561
  Parameters
502
562
  ----------
503
- type : str
504
- The type of depth coordinate to retrieve. Valid options are:
505
- - "layer": Retrieves layer depth coordinates.
506
- - "interface": Retrieves interface depth coordinates.
507
-
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).
508
568
  direction : str
509
- 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:
510
570
  - "north"
511
571
  - "south"
512
572
  - "east"
513
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.
514
583
 
515
- additional_locations : list of str, optional
516
- Specifies additional locations to compute depth coordinates for. Default is ["u", "v"].
517
- Valid options include:
518
- - "u": Computes depth coordinates for u points.
519
- - "v": Computes depth coordinates for v points.
520
-
521
- Updates
522
- -------
523
- self.grid.ds : xarray.Dataset
524
- The dataset is updated with the following vertical depth coordinates:
525
- - f"{type}_depth_rho_{direction}": Depth coordinates at rho points.
526
- - f"{type}_depth_u_{direction}": Depth coordinates at u points (if applicable).
527
- - 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.
528
588
  """
529
-
530
- layer_vars = []
531
- for location in ["rho"] + additional_locations:
532
- layer_vars.append(f"{type}_depth_{location}_{direction}")
533
-
534
- if all(layer_var in self.grid.ds for layer_var in layer_vars):
535
- # Vertical coordinate data already exists
536
- pass
537
-
538
- elif f"{type}_depth_rho" in self.grid.ds:
539
- depth = self.grid.ds[f"{type}_depth_rho"]
540
- depth.attrs["long_name"] = f"{type} depth at rho-points"
541
- depth.attrs["units"] = "m"
542
- self.grid.ds[f"{type}_depth_rho_{direction}"] = depth.isel(
543
- **self.bdry_coords["rho"][direction]
544
- )
545
-
546
- 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"]:
547
592
  # selection of margin consisting of 2 grid cells
548
- depth = depth.isel(**self.bdry_coords["vector"][direction])
549
- # interpolation
550
- if "u" in additional_locations:
551
- depth_u = interpolate_from_rho_to_u(depth)
552
- depth_u.attrs["long_name"] = f"{type} depth at u-points"
553
- depth_u.attrs["units"] = "m"
554
- self.grid.ds[f"{type}_depth_u_{direction}"] = depth_u.isel(
555
- **self.bdry_coords["u"][direction]
556
- )
557
- if "v" in additional_locations:
558
- depth_v = interpolate_from_rho_to_v(depth)
559
- depth_v.attrs["long_name"] = f"{type} depth at v-points"
560
- depth_v.attrs["units"] = "m"
561
- self.grid.ds[f"{type}_depth_v_{direction}"] = depth_v.isel(
562
- **self.bdry_coords["v"][direction]
563
- )
564
- else:
565
- if "u" in additional_locations or "v" in additional_locations:
566
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])
567
600
  else:
568
601
  h = self.grid.ds["h"].isel(**self.bdry_coords["rho"][direction])
569
- if type == "layer":
602
+
603
+ if depth_type == "layer":
570
604
  depth = compute_depth(
571
- 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
572
606
  )
573
607
  else:
574
608
  depth = compute_depth(
575
- 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
576
610
  )
577
611
 
578
- if "u" in additional_locations or "v" in additional_locations:
579
- depth.attrs["long_name"] = f"{type} depth at rho-points"
580
- depth.attrs["units"] = "m"
581
- self.grid.ds[f"{type}_depth_rho_{direction}"] = depth.isel(
582
- **self.bdry_coords["rho"][direction]
583
- )
584
- # selection of margin consisting of 2 grid cells
585
- depth = depth.isel(**self.bdry_coords["vector"][direction])
586
- # interpolation
587
- depth_u = interpolate_from_rho_to_u(depth)
588
- depth_v = interpolate_from_rho_to_v(depth)
589
- # selection of outermost margin
590
- depth_u.attrs["long_name"] = f"{type} depth at u-points"
591
- depth_u.attrs["units"] = "m"
592
- self.grid.ds[f"{type}_depth_u_{direction}"] = depth_u.isel(
593
- **self.bdry_coords["u"][direction]
594
- )
595
- depth_v.attrs["long_name"] = f"{type} depth at v-points"
596
- depth_v.attrs["units"] = "m"
597
- self.grid.ds[f"{type}_depth_v_{direction}"] = depth_v.isel(
598
- **self.bdry_coords["v"][direction]
599
- )
600
- else:
601
- depth.attrs["long_name"] = f"{type} depth at rho-points"
602
- depth.attrs["units"] = "m"
603
- 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
604
621
 
605
622
  def _add_global_metadata(self, data, ds=None):
606
623
 
@@ -617,6 +634,10 @@ class BoundaryForcing:
617
634
  ds.attrs["end_time"] = str(self.end_time)
618
635
  ds.attrs["source"] = self.source["name"]
619
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
+ )
620
641
 
621
642
  ds.attrs["theta_s"] = self.grid.ds.attrs["theta_s"]
622
643
  ds.attrs["theta_b"] = self.grid.ds.attrs["theta_b"]
@@ -840,9 +861,10 @@ class BoundaryForcing:
840
861
  mask = mask.isel(**self.bdry_coords[location][direction])
841
862
 
842
863
  if "s_rho" in field.dims:
843
- field = field.assign_coords(
844
- {"layer_depth": self.grid.ds[f"layer_depth_{location}_{direction}"]}
845
- )
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})
846
868
  if var_name.startswith(("u", "v", "ubar", "vbar", "zeta")):
847
869
  vmax = max(field.max().values, -field.min().values)
848
870
  vmin = -vmax
@@ -859,19 +881,11 @@ class BoundaryForcing:
859
881
 
860
882
  if len(field.dims) == 2:
861
883
  if layer_contours:
862
- if location in ["u", "v"]:
863
- additional_locations = ["u", "v"]
864
- else:
865
- additional_locations = []
866
- self._get_vertical_coordinates(
867
- type="interface",
868
- direction=direction,
869
- additional_locations=additional_locations,
870
- )
871
-
872
- interface_depth = self.grid.ds[
884
+ interface_depth = self.ds_depth_coords[
873
885
  f"interface_depth_{location}_{direction}"
874
886
  ]
887
+ if self.adjust_depth_for_sea_surface_height:
888
+ interface_depth = interface_depth.isel(time=time)
875
889
  # restrict number of layer_contours to 10 for the sake of plot clearity
876
890
  nr_layers = len(interface_depth["s_w"])
877
891
  selected_layers = np.linspace(
@@ -883,7 +897,7 @@ class BoundaryForcing:
883
897
  interface_depth = None
884
898
 
885
899
  _section_plot(
886
- field.where(mask),
900
+ field,
887
901
  interface_depth=interface_depth,
888
902
  title=title,
889
903
  kwargs=kwargs,
@@ -895,37 +909,21 @@ class BoundaryForcing:
895
909
  def save(
896
910
  self,
897
911
  filepath: Union[str, Path],
898
- np_eta: int = None,
899
- np_xi: int = None,
900
- group: bool = False,
912
+ group: bool = True,
901
913
  ) -> None:
902
914
  """Save the boundary forcing fields to one or more netCDF4 files.
903
915
 
904
- This method saves the dataset either as a single file or as multiple files depending on the partitioning and grouping options.
905
- The dataset can be saved in two modes:
906
-
907
- 1. **Single File Mode (default)**:
908
- - If both `np_eta` and `np_xi` are `None`, the entire dataset is saved as a single netCDF4 file.
909
- - The file is named based on the `filepath`, with `.nc` automatically appended.
910
-
911
- 2. **Partitioned Mode**:
912
- - If either `np_eta` or `np_xi` is specified, the dataset is partitioned into spatial tiles along the `eta` and `xi` axes.
913
- - 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"`).
914
-
915
- 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.
916
919
 
917
920
  Parameters
918
921
  ----------
919
922
  filepath : Union[str, Path]
920
- The base path and filename for the output files. The format of the filenames depends on whether partitioning is used
921
- and the temporal range of the data. For partitioned datasets, files will be named with an additional index, e.g.,
922
- `"filepath_YYYYMM.0.nc"`, `"filepath_YYYYMM.1.nc"`, etc.
923
- np_eta : int, optional
924
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed.
925
- np_xi : int, optional
926
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed.
927
- group: bool, optional
928
- 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`.
929
927
 
930
928
  Returns
931
929
  -------
@@ -940,12 +938,6 @@ class BoundaryForcing:
940
938
  if filepath.suffix == ".nc":
941
939
  filepath = filepath.with_suffix("")
942
940
 
943
- if self.use_dask:
944
- from dask.diagnostics import ProgressBar
945
-
946
- with ProgressBar():
947
- self.ds.load()
948
-
949
941
  if group:
950
942
  dataset_list, output_filenames = group_dataset(self.ds, str(filepath))
951
943
  else:
@@ -953,7 +945,7 @@ class BoundaryForcing:
953
945
  output_filenames = [str(filepath)]
954
946
 
955
947
  saved_filenames = save_datasets(
956
- dataset_list, output_filenames, np_eta=np_eta, np_xi=np_xi
948
+ dataset_list, output_filenames, use_dask=self.use_dask
957
949
  )
958
950
 
959
951
  return saved_filenames
@@ -1006,19 +998,19 @@ class BoundaryForcing:
1006
998
  )
1007
999
 
1008
1000
 
1009
- def apply_1d_horizontal_fill(processed_fields: dict) -> dict:
1010
- """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.
1011
1004
 
1012
1005
  Parameters
1013
1006
  ----------
1014
- processed_fields : dict
1015
- A dictionary of variables to be updated, where each value is an
1016
- `xarray.DataArray`.
1007
+ data_array : xarray.DataArray
1008
+ The data array to be updated.
1017
1009
 
1018
1010
  Returns
1019
1011
  -------
1020
- dict of str : xarray.DataArray
1021
- The updated dictionary of variables, with NaN values filled.
1012
+ xarray.DataArray
1013
+ The updated data array with NaN values filled.
1022
1014
 
1023
1015
  Raises
1024
1016
  ------
@@ -1027,29 +1019,22 @@ def apply_1d_horizontal_fill(processed_fields: dict) -> dict:
1027
1019
  """
1028
1020
 
1029
1021
  horizontal_dims = ["eta_rho", "eta_v", "xi_rho", "xi_u"]
1022
+ selected_horizontal_dim = None
1030
1023
 
1031
- for var_name in processed_fields.keys():
1032
- selected_horizontal_dim = None
1033
- # Determine the horizontal dimension to fill
1034
- for dim in horizontal_dims:
1035
- if dim in processed_fields[var_name].dims:
1036
- if selected_horizontal_dim is not None:
1037
- raise ValueError(
1038
- f"More than one horizontal dimension found in variable '{var_name}'."
1039
- )
1040
- selected_horizontal_dim = dim
1041
-
1042
- if selected_horizontal_dim is None:
1043
- raise ValueError(
1044
- f"No valid horizontal dimension found for variable '{var_name}'."
1045
- )
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
1046
1032
 
1047
- # Forward and backward fill in the horizontal direction
1048
- filled = one_dim_fill(
1049
- processed_fields[var_name], selected_horizontal_dim, direction="forward"
1050
- )
1051
- processed_fields[var_name] = one_dim_fill(
1052
- 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}'."
1053
1036
  )
1054
1037
 
1055
- 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")