@@ -10,7 +10,9 @@ def discharge(ds, water_depth, rho, mu=None, surface_offset=0, utm_zone=10):
1010 natively handle ADCP datasets read in using the `dolfyn` module.
1111
1212 Dataset velocity should already be corrected using ADCP-measured
13- bottom track or GPS-measured velocity.
13+ bottom track or GPS-measured velocity. The first velocity direction
14+ is assumed to be the primary flow axis.
15+
1416 This function linearly interpolates the lowest ADCP depth bin to
1517 the seafloor, and applies a constant extrapolation from the first
1618 ADCP bin to the surface.
@@ -30,14 +32,15 @@ def discharge(ds, water_depth, rho, mu=None, surface_offset=0, utm_zone=10):
3032 rho: float
3133 Water density in kg/m^3
3234 mu: float
33- Kinematic visocity based on water temperature and salinity, in Ns/m^2.
34- Required for Reynolds Number.
35+ Dynamic visocity based on water temperature and salinity, in Ns/m^2.
36+ If not provided, Reynolds Number will not be calculated.
37+ Default: None.
3538 surface_offset: float
3639 Surface level offset due to changes in tidal level, in meters.
37- Default: 0 m.
40+ Positive is down. Default: 0 m.
3841 utm_zone: int
39- UTM zone that measurements were acquired in. Map of UTM zones for the
40- contiguous US:
42+ UTM zone for coordinate transformations (e.g., to compute cross-sectional
43+ distances from GPS lat/lon data). Map of UTM zones for the contiguous US:
4144 https://www.usgs.gov/media/images/mapping-utm-grid-conterminous-48-united-states.
4245 Default: 10 (the US west coast).
4346
@@ -51,7 +54,38 @@ def discharge(ds, water_depth, rho, mu=None, surface_offset=0, utm_zone=10):
5154 - `reynolds_number`: (1) Reynolds number, unitless
5255 """
5356
54- def extrap2bottom (vel , bottom , rng ):
57+ def _extrapolate_to_bottom (vel , bottom , rng ):
58+ """
59+ Linearly extrapolate velocity values from the deepest valid bin down to zero at the seafloor.
60+
61+ This function sets velocity to zero at the seafloor and linearly interpolates
62+ between the last valid velocity bin and this zero-velocity boundary. If no valid
63+ velocity is found in a particular profile, no update is performed for that profile.
64+ This function assumes `rng` extends at least to (or below) the deepest seafloor depth
65+ specified in `bottom`.
66+
67+ Parameters
68+ ----------
69+ vel : numpy.ndarray
70+ A velocity array of shape (dir, range, time), typically containing:
71+ - `dir` : velocity component dimension (e.g., 2 or 3 for 2D or 3D flow).
72+ - `range` : vertical/bin dimension (positive downward).
73+ - `time` : time dimension corresponding to each profile.
74+ The array is modified in-place (the updated values are also returned).
75+ bottom : array-like
76+ Array of length equal to the time dimension in `vel`, specifying the seafloor
77+ depth (in the same coordinate system as `rng`) at each time step.
78+ rng : array-like
79+ The vertical/bin positions corresponding to `vel` along the `range` dimension,
80+ sorted in ascending order (e.g., depth from the water surface downward).
81+
82+ Returns
83+ -------
84+ vel : numpy.ndarray
85+ The same array passed in, with updated values below the last valid velocity bin
86+ for each time step (linear extrapolation to zero at the seafloor).
87+ """
88+
5589 for idx in range (vel .shape [- 1 ]):
5690 z_bot = bottom [idx ]
5791 # Fetch lowest range index
@@ -71,7 +105,47 @@ def extrap2bottom(vel, bottom, rng):
71105
72106 return vel
73107
74- def latlon2utm (ds , proj ):
108+ def _convert_latlon_to_utm (ds , proj ):
109+ """
110+ Convert latitude/longitude coordinates to UTM coordinates.
111+
112+ This function uses the Cartopy `transform_point` and `transform_points` methods to
113+ project GPS latitude/longitude data into the specified UTM coordinate reference
114+ system. The resulting (x, y) coordinates are stored in an xarray DataArray that is
115+ interpolated onto the main time axis of `ds`.
116+
117+ The function sets `proj.x0` and `proj.y0` to the UTM coordinates of the mean
118+ longitude and latitude from `ds`. This can be used as a reference origin.
119+ Missing or NaN lat/lon values are handled via interpolation and extrapolation
120+ onto the `ds["time"]` axis.
121+ This function modifies the `proj` object by adding `x0` and `y0` attributes,
122+ which may be used for subsequent coordinate transformations or offsets.
123+
124+ Parameters
125+ ----------
126+ ds : xarray.Dataset
127+ A dataset that must contain at least the following variables:
128+ - "latitude_gps" : (time_gps) latitude values in degrees North.
129+ - "longitude_gps" : (time_gps) longitude values in degrees East.
130+ - "time" : time axis onto which the projected coordinates will be
131+ interpolated.
132+ proj : cartopy.crs.Projection
133+ A Cartopy UTM projection or similar projection object. This is used both to
134+ store the reference origin (`x0`, `y0`) and to transform lat/lon coordinates
135+ into UTM.
136+
137+ Returns
138+ -------
139+ xy : xarray.DataArray
140+ A DataArray of shape (gps=2, time), where:
141+ - The first dimension (indexed by "gps") corresponds to ["x", "y"] UTM
142+ coordinates.
143+ - The second dimension ("time") matches `ds["time"]`.
144+ The returned coordinates are interpolated in time using `ds["longitude_gps"]`
145+ and `ds["latitude_gps"]`, with values extrapolated if necessary.
146+
147+ """
148+
75149 PlateC = ccrs .PlateCarree ()
76150 proj .x0 , proj .y0 = proj .transform_point (
77151 ds ["longitude_gps" ].mean (), ds ["latitude_gps" ].mean (), PlateC
@@ -90,11 +164,59 @@ def latlon2utm(ds, proj):
90164 return xy
91165
92166 def _distance (proj , x , y ):
93- # GPS distance traveled in meters
167+ """
168+ Compute the planar distance from the projection's reference origin.
169+
170+ Parameters
171+ ----------
172+ proj : cartopy.crs.Projection
173+ A projection object with attributes `x0` and `y0`, which define the
174+ reference origin in the projected coordinate system.
175+ x : float or array-like
176+ One or more x-coordinates in the same units (m) as `proj.x0`.
177+ y : float or array-like
178+ One or more y-coordinates in the same units (m) as `proj.y0`.
179+
180+ Returns
181+ -------
182+ dist : float or numpy.ndarray
183+ The distance(s) in m from the point(s) `(x, y)` to `(proj.x0, proj.y0)`.
184+ If `x` and `y` are arrays, the output is an array of the same shape.
185+ """
186+
94187 return np .sqrt ((proj .x0 - x ) ** 2 + (proj .y0 - y ) ** 2 )
95188
96- def calc_Q (vel , x , depth , surface_zoff = None ):
97- # depth and surface_zoff should be positive in down direction
189+ def _calc_discharge (vel , x , depth , surface_zoff = None ):
190+ """
191+ Calculate the integrated flux (e.g., discharge) by double integration of velocity
192+ over the cross-sectional area: depth and lateral distance.
193+
194+ Missing (NaN) velocities are treated as zero.
195+ Ensure `depth` and `surface_zoff` are both positive downward.
196+
197+ Parameters
198+ ----------
199+ vel : numpy.ndarray or xarray.DataArray
200+ A 2D array of shape (nz, nx) corresponding to velocity values (m/s).
201+ - `nz` is the number of vertical bins (downward).
202+ - `nx` is the number of horizontal points.
203+ x : array-like
204+ Horizontal positions (m) of length `nx`. If `x` is in descending order
205+ (i.e., `x[0] > x[-1]`), the resulting flux is assigned a negative sign to
206+ indicate reverse orientation.
207+ depth : array-like
208+ Vertical positions (m) of length `nz`, positive downward. This is used
209+ for integration along the vertical dimension.
210+ surface_zoff : float, optional
211+ Surface level offset due to changes in tidal level, in meters.
212+ Positive is down.
213+
214+ Returns
215+ -------
216+ Q : float
217+ The integrated flux (e.g., discharge) in units of m^3/s
218+
219+ """
98220 vel = vel .copy ()
99221 vel = vel .fillna (0 )
100222 if surface_zoff is not None :
@@ -109,37 +231,44 @@ def calc_Q(vel, x, depth, surface_zoff=None):
109231
110232 # Extrapolate to bed
111233 vel = ds ["vel" ].copy ()
112- vel .values = extrap2bottom (ds ["vel" ].values , water_depth , ds ["range" ].values )
234+ vel .values = _extrapolate_to_bottom (
235+ ds ["vel" ].values , water_depth , ds ["range" ].values
236+ )
113237 vel_x = vel [0 ]
114238 # Get position at each timestep in UTM grid
115239 proj = ccrs .UTM (utm_zone )
116- xy = latlon2utm (ds , proj )
240+ xy = _convert_latlon_to_utm (ds , proj )
117241 # Distance from UTM grid origin (mean of GPS points)
118242 _x = _distance (proj , xy [0 ], xy [1 ])
119243 # Set distance range for entire transect
120- Q_x_range = [_x .min (), _x .max ()] # meters
244+ q_x_range = [_x .min (), _x .max ()] # meters
121245
122246 # Calculate discharge, power, kinetic energy, and reynolds number
123- _xinds = (Q_x_range [0 ] < _x ) & (_x < Q_x_range [1 ])
247+ _xinds = (q_x_range [0 ] < _x ) & (_x < q_x_range [1 ])
124248 out = {}
125249 if _xinds .any ():
126- U = vel_x [:, _xinds ] # m/s
250+ speed = vel_x [:, _xinds ] # m/s
127251 # Volume Flux, aka Discharge
128- out ["Q" ] = calc_Q (
129- U , xy [0 ][_xinds ], ds ["range" ], surface_offset
252+ out ["Q" ] = _calc_discharge (
253+ speed , xy [0 ][_xinds ], ds ["range" ], surface_offset
130254 ) # m/s * m * m = m^3/s
131255 # Kinetic Energy Flux, aka Power
132256 out ["P" ] = (
133- 0.5 * rho * calc_Q (U ** 3 , xy [0 ][_xinds ], ds ["range" ], surface_offset )
257+ 0.5
258+ * rho
259+ * _calc_discharge (speed ** 3 , xy [0 ][_xinds ], ds ["range" ], surface_offset )
134260 ) # kg/m^3 * m^3/s^3 * m * m = kg*m^2/s = W
135261 # Power Density
136- out ["J" ] = (0.5 * rho * U ** 3 ).mean ().item () # kg/m^3 * m^3/s^3 = kg/s^3 = W/m^2
137- # Hydraulic Depth
138- L = abs (np .trapz ((water_depth - surface_offset )[_xinds ], xy [0 ][_xinds ])) / (
262+ out ["J" ] = (
263+ (0.5 * rho * speed ** 3 ).mean ().item ()
264+ ) # kg/m^3 * m^3/s^3 = kg/s^3 = W/m^2
265+ hydraulic_depth = abs (
266+ np .trapz ((water_depth - surface_offset )[_xinds ], xy [0 ][_xinds ])
267+ ) / (
139268 xy [0 ][_xinds ].max () - xy [0 ][_xinds ].min ()
140269 ) # area / surface-width
141270 # Reynolds Number
142- out ["Re" ] = ((rho * ds .velds .U_mag .mean () * L ) / mu ).item ()
271+ out ["Re" ] = ((rho * ds .velds .U_mag .mean () * hydraulic_depth ) / mu ).item ()
143272 else :
144273 out ["Q" ] = np .nan
145274 out ["P" ] = np .nan
0 commit comments