roms-tools 1.7.0__py3-none-any.whl → 2.1.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 (124) hide show
  1. roms_tools/__init__.py +2 -1
  2. roms_tools/setup/boundary_forcing.py +246 -146
  3. roms_tools/setup/datasets.py +229 -69
  4. roms_tools/setup/download.py +13 -17
  5. roms_tools/setup/grid.py +777 -614
  6. roms_tools/setup/initial_conditions.py +168 -32
  7. roms_tools/setup/mask.py +115 -0
  8. roms_tools/setup/nesting.py +575 -0
  9. roms_tools/setup/plot.py +218 -63
  10. roms_tools/setup/regrid.py +4 -2
  11. roms_tools/setup/river_forcing.py +125 -29
  12. roms_tools/setup/surface_forcing.py +31 -25
  13. roms_tools/setup/tides.py +29 -14
  14. roms_tools/setup/topography.py +250 -153
  15. roms_tools/setup/utils.py +174 -44
  16. roms_tools/setup/vertical_coordinate.py +5 -16
  17. roms_tools/tests/test_setup/test_boundary_forcing.py +10 -5
  18. roms_tools/tests/test_setup/test_data/grid.zarr/.zattrs +0 -1
  19. roms_tools/tests/test_setup/test_data/grid.zarr/.zmetadata +56 -201
  20. roms_tools/tests/test_setup/test_data/grid.zarr/Cs_r/.zattrs +1 -1
  21. roms_tools/tests/test_setup/test_data/grid.zarr/Cs_w/.zattrs +1 -1
  22. roms_tools/tests/test_setup/test_data/grid.zarr/{layer_depth_rho → sigma_r}/.zarray +2 -6
  23. roms_tools/tests/test_setup/test_data/grid.zarr/sigma_r/.zattrs +7 -0
  24. roms_tools/tests/test_setup/test_data/grid.zarr/sigma_r/0 +0 -0
  25. roms_tools/tests/test_setup/test_data/grid.zarr/sigma_w/.zarray +20 -0
  26. roms_tools/tests/test_setup/test_data/grid.zarr/sigma_w/.zattrs +7 -0
  27. roms_tools/tests/test_setup/test_data/grid.zarr/sigma_w/0 +0 -0
  28. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +1 -2
  29. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +58 -203
  30. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/Cs_r/.zattrs +1 -1
  31. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/Cs_w/.zattrs +1 -1
  32. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/.zattrs +1 -1
  33. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  34. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_coarse/0.0 +0 -0
  35. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_rho/0.0 +0 -0
  36. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_u/0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_v/0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_r/.zarray +20 -0
  39. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_r/.zattrs +7 -0
  40. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_r/0 +0 -0
  41. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_w/.zarray +20 -0
  42. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_w/.zattrs +7 -0
  43. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/sigma_w/0 +0 -0
  44. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +2 -3
  45. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_tracer/.zattrs +1 -2
  46. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_name/.zarray +1 -1
  47. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_name/0 +0 -0
  48. roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/.zmetadata +5 -6
  49. roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_tracer/.zarray +2 -2
  50. roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_tracer/.zattrs +1 -2
  51. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_tracer/0.0.0 +0 -0
  52. roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/tracer_name/.zarray +2 -2
  53. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_name/0 +0 -0
  54. roms_tools/tests/test_setup/test_datasets.py +2 -2
  55. roms_tools/tests/test_setup/test_grid.py +110 -12
  56. roms_tools/tests/test_setup/test_initial_conditions.py +2 -1
  57. roms_tools/tests/test_setup/test_nesting.py +489 -0
  58. roms_tools/tests/test_setup/test_river_forcing.py +53 -15
  59. roms_tools/tests/test_setup/test_surface_forcing.py +3 -22
  60. roms_tools/tests/test_setup/test_tides.py +2 -1
  61. roms_tools/tests/test_setup/test_topography.py +106 -1
  62. roms_tools/tests/test_setup/test_validation.py +2 -2
  63. {roms_tools-1.7.0.dist-info → roms_tools-2.1.0.dist-info}/LICENSE +1 -1
  64. {roms_tools-1.7.0.dist-info → roms_tools-2.1.0.dist-info}/METADATA +9 -4
  65. {roms_tools-1.7.0.dist-info → roms_tools-2.1.0.dist-info}/RECORD +85 -108
  66. {roms_tools-1.7.0.dist-info → roms_tools-2.1.0.dist-info}/WHEEL +1 -1
  67. roms_tools/_version.py +0 -2
  68. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_rho/.zarray +0 -24
  69. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_rho/.zattrs +0 -9
  70. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_rho/0.0.0 +0 -0
  71. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_u/.zarray +0 -24
  72. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_u/.zattrs +0 -9
  73. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_u/0.0.0 +0 -0
  74. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_v/.zarray +0 -24
  75. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_v/.zattrs +0 -9
  76. roms_tools/tests/test_setup/test_data/grid.zarr/interface_depth_v/0.0.0 +0 -0
  77. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_rho/.zattrs +0 -9
  78. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_rho/0.0.0 +0 -0
  79. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_u/.zarray +0 -24
  80. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_u/.zattrs +0 -9
  81. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_u/0.0.0 +0 -0
  82. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_v/.zarray +0 -24
  83. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_v/.zattrs +0 -9
  84. roms_tools/tests/test_setup/test_data/grid.zarr/layer_depth_v/0.0.0 +0 -0
  85. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_rho/.zarray +0 -24
  86. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_rho/.zattrs +0 -9
  87. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_rho/0.0.0 +0 -0
  88. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_u/.zarray +0 -24
  89. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_u/.zattrs +0 -9
  90. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_u/0.0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_v/.zarray +0 -24
  92. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_v/.zattrs +0 -9
  93. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/interface_depth_v/0.0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_rho/.zarray +0 -24
  95. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_rho/.zattrs +0 -9
  96. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_rho/0.0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_u/.zarray +0 -24
  98. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_u/.zattrs +0 -9
  99. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_u/0.0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_v/.zarray +0 -24
  101. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_v/.zattrs +0 -9
  102. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/layer_depth_v/0.0.0 +0 -0
  103. roms_tools/tests/test_setup/test_data/river_forcing.zarr/river_tracer/0.0.0 +0 -0
  104. roms_tools/tests/test_setup/test_data/river_forcing.zarr/tracer_name/0 +0 -0
  105. roms_tools/tests/test_setup/test_vertical_coordinate.py +0 -91
  106. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/.zattrs +0 -0
  107. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/.zgroup +0 -0
  108. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/abs_time/.zarray +0 -0
  109. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/abs_time/.zattrs +0 -0
  110. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/abs_time/0 +0 -0
  111. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/month/.zarray +0 -0
  112. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/month/.zattrs +0 -0
  113. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/month/0 +0 -0
  114. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_name/.zarray +0 -0
  115. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_name/.zattrs +0 -0
  116. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_name/0 +0 -0
  117. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_time/.zarray +0 -0
  118. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_time/.zattrs +0 -0
  119. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_time/0 +0 -0
  120. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_volume/.zarray +0 -0
  121. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_volume/.zattrs +0 -0
  122. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/river_volume/0.0 +0 -0
  123. /roms_tools/tests/test_setup/test_data/{river_forcing.zarr → river_forcing_with_bgc.zarr}/tracer_name/.zattrs +0 -0
  124. {roms_tools-1.7.0.dist-info → roms_tools-2.1.0.dist-info}/top_level.txt +0 -0
@@ -1,34 +1,37 @@
1
+ import time
2
+ import logging
1
3
  import xarray as xr
2
4
  import numpy as np
3
5
  import gcm_filters
4
- from scipy.interpolate import RegularGridInterpolator
5
- from scipy.ndimage import label
6
- from roms_tools.setup.download import fetch_topo
7
- from roms_tools.setup.utils import interpolate_from_rho_to_u, interpolate_from_rho_to_v
6
+ from roms_tools.setup.utils import handle_boundaries
8
7
  import warnings
9
8
  from itertools import count
10
-
11
-
12
- def _add_topography_and_mask(
13
- ds, topography_source, hmin, smooth_factor=8.0, rmax=0.2
9
+ from roms_tools.setup.datasets import ETOPO5Dataset, SRTM15Dataset
10
+ from roms_tools.setup.regrid import LateralRegrid
11
+
12
+
13
+ def _add_topography(
14
+ ds,
15
+ target_coords,
16
+ topography_source,
17
+ hmin,
18
+ smooth_factor=8.0,
19
+ rmax=0.2,
20
+ verbose=False,
14
21
  ) -> xr.Dataset:
15
- """Adds topography and a land/water mask to the dataset based on the provided
16
- topography source.
17
-
18
- This function performs the following operations:
19
- 1. Interpolates topography data onto the desired grid.
20
- 2. Applies a mask based on ocean depth.
21
- 3. Smooths the topography globally to reduce grid-scale instabilities.
22
- 4. Fills enclosed basins with land.
23
- 5. Smooths the topography locally to ensure the steepness ratio satisfies the rmax criterion.
24
- 6. Adds topography metadata.
22
+ """Adds topography to the dataset based on the provided topography source.
25
23
 
26
24
  Parameters
27
25
  ----------
28
26
  ds : xr.Dataset
29
- The dataset to which topography and the land/water mask will be added.
30
- topography_source : str
31
- The source of the topography data.
27
+ The dataset to which topography will be added.
28
+ topography_source : Dict[str, Union[str, Path]], optional
29
+ Dictionary specifying the source of the topography data:
30
+
31
+ - "name" (str): The name of the topography data source (e.g., "SRTM15").
32
+ - "path" (Union[str, Path, List[Union[str, Path]]]): The path to the raw data file. Can be a string or a Path object.
33
+
34
+ The default is "ETOPO5", which does not require a path.
32
35
  hmin : float
33
36
  The minimum allowable depth for the topography.
34
37
  smooth_factor : float, optional
@@ -39,89 +42,142 @@ def _add_topography_and_mask(
39
42
  The maximum allowable steepness ratio for the topography smoothing.
40
43
  This parameter controls the local smoothing of the topography. Smaller values result in
41
44
  smoother topography, while larger values preserve more detail. The default is 0.2.
45
+ verbose: bool, optional
46
+ Indicates whether to print topography generation steps with timing. Defaults to False.
42
47
 
43
48
  Returns
44
49
  -------
45
50
  xr.Dataset
46
- The dataset with added topography, mask, and metadata.
51
+ Updated dataset with added topography and metadata.
47
52
  """
48
53
 
49
- lon = ds.lon_rho.values
50
- lat = ds.lat_rho.values
54
+ if verbose:
55
+ start_time = time.time()
56
+ data = _get_topography_data(topography_source)
57
+ if verbose:
58
+ logging.info(
59
+ f"Reading the topography data: {time.time() - start_time:.3f} seconds"
60
+ )
51
61
 
52
62
  # interpolate topography onto desired grid
53
- hraw = _make_raw_topography(lon, lat, topography_source)
54
- hraw = xr.DataArray(data=hraw, dims=["eta_rho", "xi_rho"])
55
-
56
- # Mask is obtained by finding locations where ocean depth is positive
57
- mask = xr.where(hraw > 0, 1.0, 0.0)
63
+ hraw = _make_raw_topography(data, target_coords, verbose=verbose)
64
+ nan_check(hraw)
58
65
 
59
66
  # smooth topography domain-wide with Gaussian kernel to avoid grid scale instabilities
67
+ if verbose:
68
+ start_time = time.time()
60
69
  hraw = _smooth_topography_globally(hraw, smooth_factor)
61
-
62
- # fill enclosed basins with land
63
- mask = _fill_enclosed_basins(mask.values)
64
-
65
- # adjust mask boundaries by copying values from adjacent cells
66
- mask = _handle_boundaries(mask)
67
-
68
- ds["mask_rho"] = xr.DataArray(mask.astype(np.int32), dims=("eta_rho", "xi_rho"))
69
- ds["mask_rho"].attrs = {
70
- "long_name": "Mask at rho-points",
71
- "units": "land/water (0/1)",
72
- }
73
-
74
- ds = _add_velocity_masks(ds)
70
+ if verbose:
71
+ logging.info(
72
+ f"Smoothing the topography globally: {time.time() - start_time:.3f} seconds"
73
+ )
75
74
 
76
75
  # smooth topography locally to satisfy r < rmax
76
+ if verbose:
77
+ start_time = time.time()
78
+ # inserting hraw * mask_rho into this function eliminates any inconsistencies between
79
+ # the land according to the topography and the land according to the mask; land points
80
+ # will always be set to hmin
77
81
  ds["h"] = _smooth_topography_locally(hraw * ds["mask_rho"], hmin, rmax)
78
82
  ds["h"].attrs = {
79
- "long_name": "Final bathymetry at rho-points",
83
+ "long_name": "Bathymetry at rho-points",
80
84
  "units": "meter",
81
85
  }
86
+ if verbose:
87
+ logging.info(
88
+ f"Smoothing the topography locally: {time.time() - start_time:.3f} seconds"
89
+ )
82
90
 
83
91
  ds = _add_topography_metadata(ds, topography_source, smooth_factor, hmin, rmax)
84
92
 
85
93
  return ds
86
94
 
87
95
 
88
- def _make_raw_topography(lon, lat, topography_source) -> np.ndarray:
89
- """Given a grid of (lon, lat) points, fetch the topography file and interpolate
90
- height values onto the desired grid."""
96
+ def _get_topography_data(source):
97
+ """Load topography data based on the specified source.
91
98
 
92
- topo_ds = fetch_topo(topography_source)
99
+ Parameters
100
+ ----------
101
+ source : dict
102
+ A dictionary containing the source details (e.g., "name" and "path").
93
103
 
94
- # the following will depend on the topography source
95
- if topography_source == "ETOPO5":
96
- topo_lon = topo_ds["topo_lon"].copy()
97
- # Modify longitude values where necessary
98
- topo_lon = xr.where(topo_lon < 0, topo_lon + 360, topo_lon)
99
- topo_lon_minus360 = topo_lon - 360
100
- topo_lon_plus360 = topo_lon + 360
101
- # Concatenate along the longitude axis
102
- topo_lon_concatenated = xr.concat(
103
- [topo_lon_minus360, topo_lon, topo_lon_plus360], dim="lon"
104
- )
105
- topo_concatenated = xr.concat(
106
- [-topo_ds["topo"], -topo_ds["topo"], -topo_ds["topo"]], dim="lon"
104
+ Returns
105
+ -------
106
+ data : object
107
+ The loaded topography dataset (ETOPO5 or SRTM15).
108
+ """
109
+ kwargs = {"use_dask": False}
110
+
111
+ if source["name"] == "ETOPO5":
112
+ if "path" in source.keys():
113
+ kwargs["filename"] = source["path"]
114
+ data = ETOPO5Dataset(**kwargs)
115
+ elif source["name"] == "SRTM15":
116
+ kwargs["filename"] = source["path"]
117
+ data = SRTM15Dataset(**kwargs)
118
+ else:
119
+ raise ValueError(
120
+ 'Only "ETOPO5" and "SRTM15" are valid options for topography_source["name"].'
107
121
  )
108
122
 
109
- interp = RegularGridInterpolator(
110
- (topo_ds["topo_lat"].values, topo_lon_concatenated.values),
111
- topo_concatenated.values,
112
- method="linear",
123
+ return data
124
+
125
+
126
+ def _make_raw_topography(
127
+ data, target_coords, method="linear", verbose=False
128
+ ) -> xr.DataArray:
129
+ """Regrid topography data to match target coordinates.
130
+
131
+ Parameters
132
+ ----------
133
+ data : object
134
+ The dataset object containing the topography data.
135
+ target_coords : object
136
+ The target coordinates to which the data will be regridded.
137
+ method : str, optional
138
+ The regridding method to use, by default "linear".
139
+ verbose : bool, optional
140
+ If True, logs the time taken for regridding, by default False.
141
+
142
+ Returns
143
+ -------
144
+ xr.DataArray
145
+ The regridded topography data with the sign flipped (bathymetry positive).
146
+ """
147
+ data.choose_subdomain(target_coords, buffer_points=3, verbose=verbose)
148
+
149
+ if verbose:
150
+ start_time = time.time()
151
+ lateral_regrid = LateralRegrid(target_coords, data.dim_names)
152
+ hraw = lateral_regrid.apply(data.ds[data.var_names["topo"]], method=method)
153
+ if verbose:
154
+ logging.info(
155
+ f"Regridding the topography: {time.time() - start_time:.3f} seconds"
113
156
  )
114
157
 
115
- # Interpolate onto desired domain grid points
116
- hraw = interp((lat, lon))
158
+ # flip sign so that bathmetry is positive
159
+ hraw = -hraw
117
160
 
118
161
  return hraw
119
162
 
120
163
 
121
164
  def _smooth_topography_globally(hraw, factor) -> xr.DataArray:
165
+ """Apply global smoothing to the topography using a Gaussian filter.
166
+
167
+ Parameters
168
+ ----------
169
+ hraw : xr.DataArray
170
+ The raw topography data to be smoothed.
171
+ factor : float
172
+ The smoothing factor (controls the width of the Gaussian filter).
173
+
174
+ Returns
175
+ -------
176
+ xr.DataArray
177
+ The smoothed topography data.
178
+ """
122
179
  # since GCM-Filters assumes periodic domain, we extend the domain by one grid cell in each dimension
123
180
  # and set that margin to land
124
-
125
181
  mask = xr.ones_like(hraw)
126
182
  margin_mask = xr.concat([mask, 0 * mask.isel(eta_rho=-1)], dim="eta_rho")
127
183
  margin_mask = xr.concat(
@@ -148,30 +204,28 @@ def _smooth_topography_globally(hraw, factor) -> xr.DataArray:
148
204
  return hsmooth
149
205
 
150
206
 
151
- def _fill_enclosed_basins(mask) -> np.ndarray:
152
- """Fills in enclosed basins with land."""
153
-
154
- # Label connected regions in the mask
155
- reg, nreg = label(mask)
156
- # Find the largest region
157
- lint = 0
158
- lreg = 0
159
- for ireg in range(nreg):
160
- int_ = np.sum(reg == ireg)
161
- if int_ > lint and mask[reg == ireg].sum() > 0:
162
- lreg = ireg
163
- lint = int_
164
-
165
- # Remove regions other than the largest one
166
- for ireg in range(nreg):
167
- if ireg != lreg:
168
- mask[reg == ireg] = 0
207
+ def _smooth_topography_locally(h, hmin=5, rmax=0.2):
208
+ """Smooths topography locally to ensure the slope (r-factor) is below the specified
209
+ threshold.
169
210
 
170
- return mask
211
+ This function applies a logarithmic transformation to the topography and iteratively smooths
212
+ it in four directions (eta, xi, and two diagonals) until the maximum slope parameter (r) is
213
+ below `rmax`. A threshold `hmin` is applied to prevent values from going below a minimum height.
171
214
 
215
+ Parameters
216
+ ----------
217
+ h : xarray.DataArray
218
+ The topography data to be smoothed.
219
+ hmin : float, optional
220
+ The minimum height threshold. Default is 5.
221
+ rmax : float, optional
222
+ The maximum allowable slope parameter (r-factor). Default is 0.2.
172
223
 
173
- def _smooth_topography_locally(h, hmin=5, rmax=0.2):
174
- """Smoothes topography locally to satisfy r < rmax."""
224
+ Returns
225
+ -------
226
+ xarray.DataArray
227
+ The smoothed topography data.
228
+ """
175
229
  # Compute rmax_log
176
230
  if rmax > 0.0:
177
231
  rmax_log = np.log((1.0 + rmax * 0.9) / (1.0 - rmax * 0.9))
@@ -181,65 +235,90 @@ def _smooth_topography_locally(h, hmin=5, rmax=0.2):
181
235
  # Apply hmin threshold
182
236
  h = xr.where(h < hmin, hmin, h)
183
237
 
184
- # We will smooth logarithmically
238
+ # Perform logarithmic transformation of the height field
185
239
  h_log = np.log(h / hmin)
186
240
 
187
- cf1 = 1.0 / 6
188
- cf2 = 0.25
241
+ # Constants for smoothing
242
+ smoothing_factor_1 = 1.0 / 6
243
+ smoothing_factor_2 = 0.25
189
244
 
245
+ # Iterate until convergence
190
246
  for iter in count():
191
- # Compute gradients in domain interior
247
+ # Compute gradients and smoothing for eta, xi, and diagonal directions
192
248
 
193
- # in eta-direction
194
- cff = h_log.diff("eta_rho").isel(xi_rho=slice(1, -1))
195
- cr = np.abs(cff)
249
+ # Gradient in eta-direction
250
+ delta_eta = h_log.diff("eta_rho").isel(xi_rho=slice(1, -1))
251
+ abs_eta_gradient = np.abs(delta_eta)
196
252
  with warnings.catch_warnings():
197
253
  warnings.simplefilter("ignore") # Ignore division by zero warning
198
- Op1 = xr.where(cr < rmax_log, 0, 1.0 * cff * (1 - rmax_log / cr))
199
-
200
- # in xi-direction
201
- cff = h_log.diff("xi_rho").isel(eta_rho=slice(1, -1))
202
- cr = np.abs(cff)
254
+ eta_correction = xr.where(
255
+ abs_eta_gradient < rmax_log,
256
+ 0,
257
+ delta_eta * (1 - rmax_log / abs_eta_gradient),
258
+ )
259
+
260
+ # Gradient in xi-direction
261
+ delta_xi = h_log.diff("xi_rho").isel(eta_rho=slice(1, -1))
262
+ abs_xi_gradient = np.abs(delta_xi)
203
263
  with warnings.catch_warnings():
204
264
  warnings.simplefilter("ignore") # Ignore division by zero warning
205
- Op2 = xr.where(cr < rmax_log, 0, 1.0 * cff * (1 - rmax_log / cr))
206
-
207
- # in diagonal direction
208
- cff = (h_log - h_log.shift(eta_rho=1, xi_rho=1)).isel(
265
+ xi_correction = xr.where(
266
+ abs_xi_gradient < rmax_log,
267
+ 0,
268
+ delta_xi * (1 - rmax_log / abs_xi_gradient),
269
+ )
270
+
271
+ # Gradient in first diagonal direction
272
+ delta_diag_1 = (h_log - h_log.shift(eta_rho=1, xi_rho=1)).isel(
209
273
  eta_rho=slice(1, None), xi_rho=slice(1, None)
210
274
  )
211
- cr = np.abs(cff)
275
+ abs_diag_1_gradient = np.abs(delta_diag_1)
212
276
  with warnings.catch_warnings():
213
277
  warnings.simplefilter("ignore") # Ignore division by zero warning
214
- Op3 = xr.where(cr < rmax_log, 0, 1.0 * cff * (1 - rmax_log / cr))
215
-
216
- # in the other diagonal direction
217
- cff = (h_log.shift(eta_rho=1) - h_log.shift(xi_rho=1)).isel(
278
+ diag_1_correction = xr.where(
279
+ abs_diag_1_gradient < rmax_log,
280
+ 0,
281
+ delta_diag_1 * (1 - rmax_log / abs_diag_1_gradient),
282
+ )
283
+
284
+ # Gradient in second diagonal direction
285
+ delta_diag_2 = (h_log.shift(eta_rho=1) - h_log.shift(xi_rho=1)).isel(
218
286
  eta_rho=slice(1, None), xi_rho=slice(1, None)
219
287
  )
220
- cr = np.abs(cff)
288
+ abs_diag_2_gradient = np.abs(delta_diag_2)
221
289
  with warnings.catch_warnings():
222
290
  warnings.simplefilter("ignore") # Ignore division by zero warning
223
- Op4 = xr.where(cr < rmax_log, 0, 1.0 * cff * (1 - rmax_log / cr))
291
+ diag_2_correction = xr.where(
292
+ abs_diag_2_gradient < rmax_log,
293
+ 0,
294
+ delta_diag_2 * (1 - rmax_log / abs_diag_2_gradient),
295
+ )
224
296
 
225
297
  # Update h_log in domain interior
226
- h_log[1:-1, 1:-1] += cf1 * (
227
- Op1[1:, :]
228
- - Op1[:-1, :]
229
- + Op2[:, 1:]
230
- - Op2[:, :-1]
231
- + cf2 * (Op3[1:, 1:] - Op3[:-1, :-1] + Op4[:-1, 1:] - Op4[1:, :-1])
298
+ h_log[1:-1, 1:-1] += smoothing_factor_1 * (
299
+ eta_correction[1:, :]
300
+ - eta_correction[:-1, :]
301
+ + xi_correction[:, 1:]
302
+ - xi_correction[:, :-1]
303
+ + smoothing_factor_2
304
+ * (
305
+ diag_1_correction[1:, 1:]
306
+ - diag_1_correction[:-1, :-1]
307
+ + diag_2_correction[:-1, 1:]
308
+ - diag_2_correction[1:, :-1]
309
+ )
232
310
  )
233
311
 
234
312
  # No gradient at the domain boundaries
235
- h_log = _handle_boundaries(h_log)
313
+ h_log = handle_boundaries(h_log)
236
314
 
237
- # Update h
315
+ # Recompute the topography after smoothing
238
316
  h = hmin * np.exp(h_log)
317
+
239
318
  # Apply hmin threshold again
240
319
  h = xr.where(h < hmin, hmin, h)
241
320
 
242
- # compute maximum slope parameter r
321
+ # Compute maximum slope parameter r
243
322
  r_eta, r_xi = _compute_rfactor(h)
244
323
  rmax0 = np.max([r_eta.max(), r_xi.max()])
245
324
  if rmax0 < rmax:
@@ -248,32 +327,24 @@ def _smooth_topography_locally(h, hmin=5, rmax=0.2):
248
327
  return h
249
328
 
250
329
 
251
- def _handle_boundaries(field):
252
- """Adjust the boundaries of a 2D field by copying values from adjacent cells.
330
+ def _compute_rfactor(h):
331
+ """Computes the slope parameter (r-factor) in both horizontal directions.
332
+
333
+ The r-factor is calculated as |Δh| / (2h) in the eta and xi directions:
334
+ - r_eta = |h_i - h_{i-1}| / (h_i + h_{i+1})
335
+ - r_xi = |h_i - h_{i-1}| / (h_i + h_{i+1})
253
336
 
254
337
  Parameters
255
338
  ----------
256
- field : numpy.ndarray or xarray.DataArray
257
- A 2D array representing a field (e.g., topography or mask) whose boundary values
258
- need to be adjusted.
339
+ h : xarray.DataArray
340
+ The topography data.
259
341
 
260
342
  Returns
261
343
  -------
262
- field : numpy.ndarray or xarray.DataArray
263
- The input field with adjusted boundary values.
344
+ tuple of xarray.DataArray
345
+ r_eta : r-factor in the eta direction.
346
+ r_xi : r-factor in the xi direction.
264
347
  """
265
-
266
- field[0, :] = field[1, :]
267
- field[-1, :] = field[-2, :]
268
- field[:, 0] = field[:, 1]
269
- field[:, -1] = field[:, -2]
270
-
271
- return field
272
-
273
-
274
- def _compute_rfactor(h):
275
- """Computes slope parameter (or r-factor) r = |Delta h| / 2h in both horizontal grid
276
- directions."""
277
348
  # compute r_{i-1/2} = |h_i - h_{i-1}| / (h_i + h_{i+1})
278
349
  r_eta = np.abs(h.diff("eta_rho")) / (h + h.shift(eta_rho=1)).isel(
279
350
  eta_rho=slice(1, None)
@@ -286,23 +357,49 @@ def _compute_rfactor(h):
286
357
 
287
358
 
288
359
  def _add_topography_metadata(ds, topography_source, smooth_factor, hmin, rmax):
289
- ds.attrs["topography_source"] = topography_source
360
+ """Adds topography metadata to the dataset.
361
+
362
+ Parameters
363
+ ----------
364
+ ds : xarray.Dataset
365
+ Dataset to update.
366
+ topography_source : dict
367
+ Dictionary with topography source information (requires 'name' key).
368
+ smooth_factor : float
369
+ Smoothing factor (unused in this function).
370
+ hmin : float
371
+ Minimum height threshold for smoothing.
372
+ rmax : float
373
+ Maximum slope parameter (unused in this function).
374
+
375
+ Returns
376
+ -------
377
+ xarray.Dataset
378
+ Updated dataset with added metadata.
379
+ """
380
+ ds.attrs["topography_source"] = topography_source["name"]
290
381
  ds.attrs["hmin"] = hmin
291
382
 
292
383
  return ds
293
384
 
294
385
 
295
- def _add_velocity_masks(ds):
386
+ def nan_check(hraw):
387
+ """Checks for NaN values in the topography data.
296
388
 
297
- # add u- and v-masks
298
- ds["mask_u"] = interpolate_from_rho_to_u(
299
- ds["mask_rho"], method="multiplicative"
300
- ).astype(np.int32)
301
- ds["mask_v"] = interpolate_from_rho_to_v(
302
- ds["mask_rho"], method="multiplicative"
303
- ).astype(np.int32)
304
-
305
- ds["mask_u"].attrs = {"long_name": "Mask at u-points", "units": "land/water (0/1)"}
306
- ds["mask_v"].attrs = {"long_name": "Mask at v-points", "units": "land/water (0/1)"}
389
+ Parameters
390
+ ----------
391
+ hraw : xarray.DataArray
392
+ Input topography data to check for NaN values.
307
393
 
308
- return ds
394
+ Raises
395
+ ------
396
+ ValueError
397
+ If NaN values are found in the data, raises an error with a descriptive message.
398
+ """
399
+ error_message = (
400
+ "NaN values found in regridded topography. This likely occurs because the ROMS grid, including "
401
+ "a small safety margin for interpolation, is not fully contained within the topography dataset's longitude/latitude range. Please ensure that the "
402
+ "dataset covers the entire area required by the ROMS grid."
403
+ )
404
+ if hraw.isnull().any().values:
405
+ raise ValueError(error_message)