roms-tools 2.6.2__py3-none-any.whl → 2.7.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 (51) hide show
  1. roms_tools/__init__.py +1 -0
  2. roms_tools/analysis/roms_output.py +11 -77
  3. roms_tools/analysis/utils.py +0 -66
  4. roms_tools/constants.py +2 -0
  5. roms_tools/download.py +46 -3
  6. roms_tools/plot.py +22 -5
  7. roms_tools/setup/cdr_forcing.py +1126 -0
  8. roms_tools/setup/datasets.py +742 -87
  9. roms_tools/setup/grid.py +42 -4
  10. roms_tools/setup/river_forcing.py +11 -84
  11. roms_tools/setup/tides.py +81 -411
  12. roms_tools/setup/utils.py +241 -37
  13. roms_tools/tests/test_setup/test_cdr_forcing.py +772 -0
  14. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +53 -1
  15. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_tracer/.zattrs +1 -1
  16. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zarray +20 -0
  17. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zattrs +6 -0
  18. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/0 +0 -0
  19. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zarray +20 -0
  20. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zattrs +6 -0
  21. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +53 -1
  23. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_tracer/.zattrs +1 -1
  24. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zarray +20 -0
  25. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zattrs +6 -0
  26. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/0 +0 -0
  27. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zarray +20 -0
  28. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zattrs +6 -0
  29. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zattrs +1 -2
  31. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zmetadata +27 -5
  32. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zarray +20 -0
  33. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zattrs +5 -0
  34. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/0 +0 -0
  35. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/omega/.zattrs +1 -3
  36. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Im/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Re/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Im/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Re/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Im/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Re/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Im/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_datasets.py +103 -1
  45. roms_tools/tests/test_setup/test_tides.py +112 -47
  46. roms_tools/utils.py +115 -1
  47. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/METADATA +1 -1
  48. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/RECORD +51 -33
  49. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/WHEEL +1 -1
  50. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/licenses/LICENSE +0 -0
  51. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/top_level.txt +0 -0
roms_tools/__init__.py CHANGED
@@ -14,6 +14,7 @@ from roms_tools.setup.surface_forcing import SurfaceForcing # noqa: F401
14
14
  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
+ from roms_tools.setup.cdr_forcing import CDRVolumePointSource # noqa: F401
17
18
  from roms_tools.setup.nesting import ChildGrid # noqa: F401
18
19
  from roms_tools.tiling.partition import partition_netcdf # noqa: F401
19
20
  from roms_tools.analysis.roms_output import ROMSOutput # noqa: F401
@@ -2,7 +2,6 @@ import xarray as xr
2
2
  import numpy as np
3
3
  import matplotlib.pyplot as plt
4
4
  from roms_tools.plot import _plot, _section_plot, _profile_plot, _line_plot
5
- from roms_tools.utils import _load_data
6
5
  from roms_tools.regrid import LateralRegridFromROMS, VerticalRegridFromROMS
7
6
  from dataclasses import dataclass, field
8
7
  from typing import Union, Optional
@@ -15,8 +14,14 @@ from roms_tools import Grid
15
14
  from roms_tools.vertical_coordinate import (
16
15
  compute_depth_coordinates,
17
16
  )
18
- from roms_tools.utils import interpolate_from_rho_to_u, interpolate_from_rho_to_v
19
- from roms_tools.analysis.utils import _validate_plot_inputs, _generate_coordinate_range
17
+ from roms_tools.utils import (
18
+ _load_data,
19
+ interpolate_from_rho_to_u,
20
+ interpolate_from_rho_to_v,
21
+ _generate_coordinate_range,
22
+ _remove_edge_nans,
23
+ )
24
+ from roms_tools.analysis.utils import _validate_plot_inputs
20
25
 
21
26
 
22
27
  @dataclass(kw_only=True)
@@ -301,7 +306,7 @@ class ROMSOutput:
301
306
  lats = [lat]
302
307
  title = title + f", lat = {lat}°N"
303
308
  else:
304
- resolution = self._infer_nominal_horizontal_resolution()
309
+ resolution = self.grid._infer_nominal_horizontal_resolution()
305
310
  lats = _generate_coordinate_range(
306
311
  field.lat.min().values, field.lat.max().values, resolution
307
312
  )
@@ -311,7 +316,7 @@ class ROMSOutput:
311
316
  lons = [lon]
312
317
  title = title + f", lon = {lon}°E"
313
318
  else:
314
- resolution = self._infer_nominal_horizontal_resolution(lat)
319
+ resolution = self.grid._infer_nominal_horizontal_resolution(lat)
315
320
  lons = _generate_coordinate_range(
316
321
  field.lon.min().values, field.lon.max().values, resolution
317
322
  )
@@ -327,33 +332,6 @@ class ROMSOutput:
327
332
  if compute_layer_depth:
328
333
  field = field.assign_coords({"layer_depth": layer_depth})
329
334
 
330
- def _remove_edge_nans(field, xdim, layer_depth=None):
331
- """Removes NaNs from the edges along the specified dimension."""
332
- if xdim in field.dims:
333
- if layer_depth is not None:
334
- nan_mask = layer_depth.isnull().sum(
335
- dim=[dim for dim in layer_depth.dims if dim != xdim]
336
- )
337
- else:
338
- nan_mask = field.isnull().sum(
339
- dim=[dim for dim in field.dims if dim != xdim]
340
- )
341
-
342
- # Find the valid indices where the sum of the nans is 0
343
- valid_indices = np.where(nan_mask.values == 0)[0]
344
-
345
- if len(valid_indices) > 0:
346
- first_valid = valid_indices[0]
347
- last_valid = valid_indices[-1]
348
-
349
- field = field.isel({xdim: slice(first_valid, last_valid + 1)})
350
- if layer_depth is not None:
351
- layer_depth = layer_depth.isel(
352
- {xdim: slice(first_valid, last_valid + 1)}
353
- )
354
-
355
- return field, layer_depth
356
-
357
335
  if lat is not None:
358
336
  field, layer_depth = _remove_edge_nans(
359
337
  field, "lon", layer_depth if "layer_depth" in locals() else None
@@ -459,7 +437,7 @@ class ROMSOutput:
459
437
  lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
460
438
 
461
439
  if horizontal_resolution is None:
462
- horizontal_resolution = self._infer_nominal_horizontal_resolution()
440
+ horizontal_resolution = self.grid._infer_nominal_horizontal_resolution()
463
441
  lons = _generate_coordinate_range(
464
442
  lon_deg.min().values, lon_deg.max().values, horizontal_resolution
465
443
  )
@@ -809,50 +787,6 @@ class ROMSOutput:
809
787
  ds = ds.assign_coords(coords_to_add)
810
788
  return ds
811
789
 
812
- def _infer_nominal_horizontal_resolution(self, lat=None):
813
- """Estimate the nominal horizontal resolution of the grid in degrees at a
814
- specified latitude.
815
-
816
- This method calculates the nominal horizontal resolution of the grid by first
817
- determining the average grid spacing in meters. The spacing is then converted
818
- to degrees, accounting for the Earth's curvature, and the latitude where the
819
- resolution is being computed.
820
-
821
- Parameters
822
- ----------
823
- lat : float, optional
824
- Latitude (in degrees) at which to estimate the horizontal resolution.
825
- If not provided, the resolution is calculated at the average latitude of
826
- the grid (`lat_rho`).
827
-
828
- Returns
829
- -------
830
- float
831
- The estimated horizontal resolution in degrees, adjusted for the Earth's curvature.
832
- """
833
- # Earth radius in meters
834
- r_earth = 6371315.0
835
-
836
- if lat is None:
837
- # Center latitude in degrees
838
- lat = (self.grid.ds.lat_rho.max() + self.grid.ds.lat_rho.min()) / 2
839
-
840
- # Convert latitude to radians
841
- lat_rad = np.deg2rad(lat)
842
-
843
- # Mean resolution in meters
844
- resolution_in_m = (
845
- (1 / self.grid.ds.pm).mean() + (1 / self.grid.ds.pn).mean()
846
- ) / 2
847
-
848
- # Meters per degree at the equator
849
- meters_per_degree = 2 * np.pi * r_earth / 360
850
-
851
- # Correct for latitude by multiplying by cos(latitude) for longitude
852
- resolution_in_degrees = resolution_in_m / (meters_per_degree * np.cos(lat_rad))
853
-
854
- return resolution_in_degrees
855
-
856
790
  def _compute_exponential_depth_levels(self, Nz=None, depth=None, h=None):
857
791
  """Compute vertical grid center and face depths using an exponential profile.
858
792
 
@@ -1,6 +1,3 @@
1
- import numpy as np
2
-
3
-
4
1
  def _validate_plot_inputs(field, s, eta, xi, depth, lat, lon, include_boundary):
5
2
  """Validate input parameters for the plot method.
6
3
 
@@ -104,66 +101,3 @@ def _validate_plot_inputs(field, s, eta, xi, depth, lat, lon, include_boundary):
104
101
  f"Invalid xi index: {xi} lies on the boundary, which is excluded when `include_boundary = False`. "
105
102
  "Either set `include_boundary = True`, or adjust eta to avoid boundary values."
106
103
  )
107
-
108
-
109
- def _generate_coordinate_range(min, max, resolution):
110
- """Generate an array of target coordinates (e.g., latitude or longitude) within a
111
- specified range, with a resolution that is rounded to the nearest value of the form
112
- `1/n` (or integer).
113
-
114
- This method generates an array of target coordinates between the provided `min` and `max`
115
- values, ensuring that both `min` and `max` are included in the resulting range. The resolution
116
- is rounded to the nearest fraction of the form `1/n` or an integer, based on the input.
117
-
118
- Parameters
119
- ----------
120
- min : float
121
- The minimum value (in degrees) of the coordinate range (inclusive).
122
-
123
- max : float
124
- The maximum value (in degrees) of the coordinate range (inclusive).
125
-
126
- resolution : float
127
- The spacing (in degrees) between each coordinate in the array. The resolution will
128
- be rounded to the nearest value of the form `1/n` or an integer, depending on the size
129
- of the resolution value.
130
-
131
- Returns
132
- -------
133
- numpy.ndarray
134
- An array of target coordinates generated from the specified range, with the resolution
135
- rounded to a suitable fraction (e.g., `1/n`) or integer, depending on the input resolution.
136
- """
137
-
138
- # Find the closest fraction of the form 1/n or integer to match the resolution
139
- resolution_rounded = None
140
- min_diff = float("inf") # Initialize the minimum difference as infinity
141
-
142
- # Search for the best fraction or integer approximation to the resolution
143
- for n in range(1, 1000): # Try fractions 1/n, where n ranges from 1 to 999
144
- if resolution <= 1:
145
- fraction = (
146
- 1 / n
147
- ) # For small resolutions (<= 1), consider fractions of the form 1/n
148
- else:
149
- fraction = n # For larger resolutions (>1), consider integers (n)
150
-
151
- diff = abs(
152
- fraction - resolution
153
- ) # Calculate the difference between the fraction and the resolution
154
-
155
- if diff < min_diff: # If the current fraction is a better approximation
156
- min_diff = diff
157
- resolution_rounded = fraction # Update the best fraction (or integer) found
158
-
159
- # Adjust the start and end of the range to include integer values
160
- start_int = np.floor(min) # Round the minimum value down to the nearest integer
161
- end_int = np.ceil(max) # Round the maximum value up to the nearest integer
162
-
163
- # Generate the array of target coordinates, including both the min and max values
164
- target = np.arange(start_int, end_int + resolution_rounded, resolution_rounded)
165
-
166
- # Truncate any values that exceed max (including small floating point errors)
167
- target = target[target <= end_int + 1e-10]
168
-
169
- return target.astype(np.float32)
@@ -0,0 +1,2 @@
1
+ R_EARTH = 6371315.0 # Earth radius in meters
2
+ NUM_TRACERS = 34 # Number of tracers (temperature, salinity, BGC tracers)
roms_tools/download.py CHANGED
@@ -22,7 +22,7 @@ correction_data = pooch.create(
22
22
  },
23
23
  )
24
24
 
25
- # Create a Pooch object to manage the global topography data
25
+ # Create a Pooch object to manage the global river data
26
26
  river_data = pooch.create(
27
27
  # Use the default cache folder for the operating system
28
28
  path=pooch.os_cache("roms-tools"),
@@ -32,6 +32,18 @@ river_data = pooch.create(
32
32
  "dai_trenberth_may2019.nc": "sha256:793849e6aa60d1f6bdb480c345515fb2453d903c0a30599241b3d752f53715ab",
33
33
  },
34
34
  )
35
+
36
+ # Create a Pooch object to manage the global SAL TPXO data
37
+ sal_data = pooch.create(
38
+ # Use the default cache folder for the operating system
39
+ path=pooch.os_cache("roms-tools"),
40
+ base_url="https://github.com/CWorthy-ocean/roms-tools-data/raw/main/",
41
+ # The registry specifies the files that can be fetched
42
+ registry={
43
+ "sal_tpxo9.v2a.nc": "sha256:5343d745b4374170a069bfc2c67dcdd9f1dc4eb0df7c0e6de5c004432c903f40",
44
+ },
45
+ )
46
+
35
47
  # Create a Pooch object to manage the test data
36
48
  pup_test_data = pooch.create(
37
49
  # Use the default cache folder for the operating system
@@ -46,8 +58,19 @@ pup_test_data = pooch.create(
46
58
  "GLORYS_NA_20121231.nc": "03c1155087195deff76ad3f136d6a7f35bc01ccae3402f3d95557a2886d39e71",
47
59
  "ERA5_regional_test_data.nc": "bd12ce3b562fbea2a80a3b79ba74c724294043c28dc98ae092ad816d74eac794",
48
60
  "ERA5_global_test_data.nc": "8ed177ab64c02caf509b9fb121cf6713f286cc603b1f302f15f3f4eb0c21dc4f",
49
- "TPXO_global_test_data.nc": "457bfe87a7b247ec6e04e3c7d3e741ccf223020c41593f8ae33a14f2b5255e60",
50
- "TPXO_regional_test_data.nc": "11739245e2286d9c9d342dce5221e6435d2072b50028bef2e86a30287b3b4032",
61
+ "global_grid_tpxo10.v2.nc": "26eb97cd135cd6f2b4e894c5f11bf7f860ff19cec8dbaa9190e37d30ee6e744e",
62
+ "global_h_tpxo10.v2.nc": "ef60fae6d52fa514dcc59a737435d74aa798dc114b57f01b123aa39dbaffc592",
63
+ "global_u_tpxo10.v2.nc": "022e57e6287e51f52eb1e5296614b1086e0e22ecd0bd57c9fd8d0e155babf5c3",
64
+ "regional_grid_tpxo10v2a.nc": "c5022bfe93ead7cd46e836578645bd87cb5be63c736e660937c7f5703c968cbc",
65
+ "regional_h_tpxo10.v2.nc": "202fd0c197490ac460af12cd9fa1156aa40023c0023c705f145c596de5b5ad3d",
66
+ "regional_grid_tpxo10v2.nc": "0789b6a24ecb2ced522481dfcfb7282e32f999984747b9b9f46f044a8898d0ac",
67
+ "regional_grid_tpxo9v5a.nc": "497a2ae9e6adc7e4b06408dadb57734e2ad24afaa3f0e2e4fd90ebc6eafc2557",
68
+ "regional_h_tpxo10v2a.nc": "2df2f181f748a960e4072f975226f6f98f6a6c4d5b77da23057946585152d59c",
69
+ "regional_h_tpxo10v2.nc": "202fd0c197490ac460af12cd9fa1156aa40023c0023c705f145c596de5b5ad3d",
70
+ "regional_h_tpxo9v5a.nc": "c7e4d9ab73bc11dcb415f88c48131531488e1aed5113df5797e80d3d374607fc",
71
+ "regional_u_tpxo10v2a.nc": "2d1680ecd53242e858281a762221d91827999967f8e1f3cb7de3d23b47efe8c8",
72
+ "regional_u_tpxo10v2.nc": "3b0849473cbb7f9076ca907e4fc39eceda3c7d64659c121fa0692024d59dcdb3",
73
+ "regional_u_tpxo9v5a.nc": "b0cc5f6934d2e212549c7120d458d61a4963ba73d17055e67cc9e4312901b041",
51
74
  "CESM_BGC_coarse_global_clim.nc": "20806e4e99285d6de168d3236e2d9245f4e9106474b1464beaa266a73e6ef79f",
52
75
  "CESM_BGC_2012.nc": "e374d5df3c1be742d564fd26fd861c2d40af73be50a432c51d258171d5638eb6",
53
76
  "CESM_regional_test_data_one_time_slice.nc": "43b578ecc067c85f95d6b97ed7b9dc8da7846f07c95331c6ba7f4a3161036a17",
@@ -127,6 +150,26 @@ def download_correction_data(filename: str) -> str:
127
150
  return fname
128
151
 
129
152
 
153
+ def download_sal_data(filename: str) -> str:
154
+ """Download the SAL data file.
155
+
156
+ Parameters
157
+ ----------
158
+ filename : str
159
+ The name of the test data file to be downloaded. Available options:
160
+ - "sal_tpxo9.v2a.nc"
161
+
162
+ Returns
163
+ -------
164
+ str
165
+ The path to the downloaded test data file.
166
+ """
167
+ # Fetch the file using Pooch, downloading if necessary
168
+ fname = sal_data.fetch(filename)
169
+
170
+ return fname
171
+
172
+
130
173
  def download_test_data(filename: str) -> str:
131
174
  """Download the test data file.
132
175
 
roms_tools/plot.py CHANGED
@@ -11,7 +11,9 @@ def _plot(
11
11
  title="",
12
12
  with_dim_names=False,
13
13
  plot_data=True,
14
+ add_colorbar=True,
14
15
  kwargs={},
16
+ ax=None,
15
17
  ):
16
18
  """Plots a grid or field on a map with optional depth contours.
17
19
 
@@ -30,13 +32,17 @@ def _plot(
30
32
  plot_data : bool, optional
31
33
  If True, plots the provided field data on the map. If False, only the grid
32
34
  boundaries and optional depth contours are plotted. Default is True.
35
+ add_colorbar : bool, optional
36
+ If True, add colobar.
33
37
  kwargs : dict, optional
34
38
  Additional keyword arguments to pass to `pcolormesh` (e.g., colormap or color limits).
39
+ ax : matplotlib.axes.Axes, optional
40
+ Pre-existing axes to draw the plot on. If None, a new figure and axes are created.
35
41
 
36
42
  Returns
37
43
  -------
38
44
  matplotlib.figure.Figure
39
- The generated figure with the plotted data.
45
+ The generated figure with the plotted data, if a new figure was created.
40
46
 
41
47
  Raises
42
48
  ------
@@ -56,18 +62,27 @@ def _plot(
56
62
 
57
63
  trans = _get_projection(lon_deg, lat_deg)
58
64
 
65
+ if ax is None:
66
+ fig, ax = plt.subplots(1, 1, figsize=(13, 7), subplot_kw={"projection": trans})
67
+
59
68
  lon_deg = lon_deg.values
60
69
  lat_deg = lat_deg.values
61
70
 
62
- fig, ax = plt.subplots(1, 1, figsize=(13, 7), subplot_kw={"projection": trans})
63
-
64
71
  if c is not None:
65
72
  _add_boundary_to_ax(
66
73
  ax, lon_deg, lat_deg, trans, c, with_dim_names=with_dim_names
67
74
  )
68
75
 
69
76
  if plot_data:
70
- _add_field_to_ax(ax, lon_deg, lat_deg, field, depth_contours, kwargs=kwargs)
77
+ _add_field_to_ax(
78
+ ax,
79
+ lon_deg,
80
+ lat_deg,
81
+ field,
82
+ depth_contours,
83
+ add_colorbar=add_colorbar,
84
+ kwargs=kwargs,
85
+ )
71
86
 
72
87
  ax.coastlines(
73
88
  resolution="50m", linewidth=0.5, color="black"
@@ -90,7 +105,9 @@ def _plot(
90
105
 
91
106
  ax.set_title(title)
92
107
 
93
- return fig
108
+ # Only return fig if it was created inside the function (i.e., ax was not provided)
109
+ if ax is None:
110
+ return fig
94
111
 
95
112
 
96
113
  def _plot_nesting(parent_grid_ds, child_grid_ds, parent_straddle, with_dim_names=False):