roms-tools 2.5.0__py3-none-any.whl → 2.6.1__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 (33) hide show
  1. ci/environment-with-xesmf.yml +16 -0
  2. roms_tools/analysis/roms_output.py +521 -187
  3. roms_tools/analysis/utils.py +169 -0
  4. roms_tools/plot.py +351 -214
  5. roms_tools/regrid.py +161 -9
  6. roms_tools/setup/boundary_forcing.py +22 -22
  7. roms_tools/setup/datasets.py +40 -44
  8. roms_tools/setup/grid.py +28 -28
  9. roms_tools/setup/initial_conditions.py +23 -31
  10. roms_tools/setup/nesting.py +3 -3
  11. roms_tools/setup/river_forcing.py +22 -23
  12. roms_tools/setup/surface_forcing.py +14 -13
  13. roms_tools/setup/tides.py +7 -7
  14. roms_tools/setup/topography.py +2 -2
  15. roms_tools/tests/test_analysis/test_roms_output.py +299 -188
  16. roms_tools/tests/test_regrid.py +85 -2
  17. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +2 -2
  18. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +2 -2
  19. roms_tools/tests/test_setup/test_river_forcing.py +47 -51
  20. roms_tools/tests/test_vertical_coordinate.py +73 -0
  21. roms_tools/utils.py +11 -7
  22. roms_tools/vertical_coordinate.py +7 -0
  23. {roms_tools-2.5.0.dist-info → roms_tools-2.6.1.dist-info}/METADATA +22 -11
  24. {roms_tools-2.5.0.dist-info → roms_tools-2.6.1.dist-info}/RECORD +33 -30
  25. {roms_tools-2.5.0.dist-info → roms_tools-2.6.1.dist-info}/WHEEL +1 -1
  26. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/.zarray +0 -0
  27. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/.zattrs +0 -0
  28. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/0.0 +0 -0
  29. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/.zarray +0 -0
  30. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/.zattrs +0 -0
  31. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/0.0 +0 -0
  32. {roms_tools-2.5.0.dist-info → roms_tools-2.6.1.dist-info/licenses}/LICENSE +0 -0
  33. {roms_tools-2.5.0.dist-info → roms_tools-2.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,169 @@
1
+ import numpy as np
2
+
3
+
4
+ def _validate_plot_inputs(field, s, eta, xi, depth, lat, lon, include_boundary):
5
+ """Validate input parameters for the plot method.
6
+
7
+ Parameters
8
+ ----------
9
+ field : xr.DataArray
10
+ Input data to be plotted.
11
+ s : int, float, or None
12
+ Depth level index or value for the s-coordinate. Use None for surface plotting.
13
+ eta : int or None
14
+ Eta index for ROMS grid selection. Must be within bounds.
15
+ xi : int or None
16
+ Xi index for ROMS grid selection. Must be within bounds.
17
+ depth : int, float, or None
18
+ Depth value for slicing. Not yet implemented.
19
+ lat : float or None
20
+ Latitude value for slicing. Must be specified with `lon` if provided.
21
+ lon : float or None
22
+ Longitude value for slicing. Must be specified with `lat` if provided.
23
+ include_boundary : bool
24
+ Whether to include boundary points when selecting grid indices.
25
+
26
+ Raises
27
+ ------
28
+ ValueError
29
+ If conflicting dimensions are specified.
30
+ If eta or xi indices are out of bounds.
31
+ If eta or xi lie on the boundary when `include_boundary=False`.
32
+ """
33
+
34
+ # Check conflicting dimension choices
35
+ if s is not None and depth is not None:
36
+ raise ValueError(
37
+ "Conflicting input: You cannot specify both 's' and 'depth' at the same time."
38
+ )
39
+ if any([eta is not None, xi is not None]) and any(
40
+ [lat is not None, lon is not None]
41
+ ):
42
+ raise ValueError(
43
+ "Conflicting input: You cannot specify 'lat' or 'lon' simultaneously with 'eta' or 'xi'."
44
+ )
45
+
46
+ # 3D fields: Check for valid dimension specification
47
+ if len(field.dims) == 3:
48
+ if not any(
49
+ [
50
+ s is not None,
51
+ eta is not None,
52
+ xi is not None,
53
+ depth is not None,
54
+ lat is not None,
55
+ lon is not None,
56
+ ]
57
+ ):
58
+ raise ValueError(
59
+ "Invalid input: For 3D fields, you must specify at least one of the dimensions 's', 'eta', 'xi', 'depth', 'lat', or 'lon'."
60
+ )
61
+ if sum([dim is not None for dim in [s, eta, xi, depth, lat, lon]]) > 2:
62
+ raise ValueError(
63
+ "Ambiguous input: For 3D fields, specify at most two of 's', 'eta', 'xi', 'depth', 'lat', or 'lon'. Specifying more than two is not allowed."
64
+ )
65
+
66
+ # 2D fields: Check for conflicts in dimension choices
67
+ if len(field.dims) == 2:
68
+ if s is not None:
69
+ raise ValueError("Vertical dimension 's' should be None for 2D fields.")
70
+ if depth is not None:
71
+ raise ValueError("Vertical dimension 'depth' should be None for 2D fields.")
72
+ if all([eta is not None, xi is not None]):
73
+ raise ValueError(
74
+ "Conflicting input: For 2D fields, specify only one dimension, either 'eta' or 'xi', not both."
75
+ )
76
+ if all([lat is not None, lon is not None]):
77
+ raise ValueError(
78
+ "Conflicting input: For 2D fields, specify only one dimension, either 'lat' or 'lon', not both."
79
+ )
80
+
81
+ # Check that indices are within bounds
82
+ if eta is not None:
83
+ dim = "eta_rho" if "eta_rho" in field.dims else "eta_v"
84
+ if not eta < len(field[dim]):
85
+ raise ValueError(
86
+ f"Invalid eta index: {eta} is out of bounds. Must be between 0 and {len(field[dim]) - 1}."
87
+ )
88
+ if not include_boundary:
89
+ if eta == 0 or eta == len(field[dim]) - 1:
90
+ raise ValueError(
91
+ f"Invalid eta index: {eta} lies on the boundary, which is excluded when `include_boundary = False`. "
92
+ "Either set `include_boundary = True`, or adjust eta to avoid boundary values."
93
+ )
94
+
95
+ if xi is not None:
96
+ dim = "xi_rho" if "xi_rho" in field.dims else "xi_u"
97
+ if not xi < len(field[dim]):
98
+ raise ValueError(
99
+ f"Invalid eta index: {xi} is out of bounds. Must be between 0 and {len(field[dim]) - 1}."
100
+ )
101
+ if not include_boundary:
102
+ if xi == 0 or xi == len(field[dim]) - 1:
103
+ raise ValueError(
104
+ f"Invalid xi index: {xi} lies on the boundary, which is excluded when `include_boundary = False`. "
105
+ "Either set `include_boundary = True`, or adjust eta to avoid boundary values."
106
+ )
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)