Skip to content

Commit 8fcf7cf

Browse files
committed
Add comments
1 parent a11759b commit 8fcf7cf

1 file changed

Lines changed: 152 additions & 23 deletions

File tree

mhkit/dolfyn/adp/discharge.py

Lines changed: 152 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)