Skip to content

Commit 1b5fbcf

Browse files
committed
Merge branch 'discharge' of https://github.com/jmcvey3/MHKiT-Python into discharge
2 parents f6f7301 + a11759b commit 1b5fbcf

6 files changed

Lines changed: 1960 additions & 1 deletion

File tree

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ dependencies:
2525
- matplotlib>=3.9.1
2626
- fatpack
2727
- nrel-rex
28+
- cartopy

examples/adcp_disharge_example.ipynb

Lines changed: 1773 additions & 0 deletions
Large diffs are not rendered by default.

mhkit/dolfyn/adp/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from . import clean
44
from ..velocity import VelBinner
55
from .turbulence import ADPBinner
6+
from .discharge import discharge

mhkit/dolfyn/adp/discharge.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import numpy as np
2+
import xarray as xr
3+
import cartopy.crs as ccrs
4+
5+
6+
def discharge(ds, water_depth, rho, mu=None, surface_offset=0, utm_zone=10):
7+
"""Calculate discharge (volume flux), power (kinetic energy flux),
8+
power density, and Reynolds number from a dataset containing a
9+
boat survey with a down-looking ADCP. This function is built to
10+
natively handle ADCP datasets read in using the `dolfyn` module.
11+
12+
Dataset velocity should already be corrected using ADCP-measured
13+
bottom track or GPS-measured velocity.
14+
This function linearly interpolates the lowest ADCP depth bin to
15+
the seafloor, and applies a constant extrapolation from the first
16+
ADCP bin to the surface.
17+
18+
Parameters
19+
----------
20+
ds: xarray.Dataset
21+
Dataset containing the following variables:
22+
- `vel`: (dir, range, time) motion-corrected velocity, in m/s
23+
- `latitude_gps`: (time_gps) latitude measured by GPS, in deg N
24+
- `longitude_gps`: (time_gps) longitude measured by GPS, in deg E
25+
water_depth: xarray.DataArray
26+
Total water depth measured by the ADCP or other input, in
27+
meters. If measured by the ADCP, add the ADCP's depth below
28+
the surface to this array.
29+
The "down" direction should be positive.
30+
rho: float
31+
Water density in kg/m^3
32+
mu: float
33+
Kinematic visocity based on water temperature and salinity, in Ns/m^2.
34+
Required for Reynolds Number.
35+
surface_offset: float
36+
Surface level offset due to changes in tidal level, in meters.
37+
Default: 0 m.
38+
utm_zone: int
39+
UTM zone that measurements were acquired in. Map of UTM zones for the
40+
contiguous US:
41+
https://www.usgs.gov/media/images/mapping-utm-grid-conterminous-48-united-states.
42+
Default: 10 (the US west coast).
43+
44+
Returns
45+
-------
46+
out: xarray.Dataset
47+
Dataset containing the following variables:
48+
- `discharge`: (1) volume flux, in m^3/s
49+
- `power`: (1) power, in W
50+
- `power_density`: (1) power density, in W/m^2
51+
- `reynolds_number`: (1) Reynolds number, unitless
52+
"""
53+
54+
def extrap2bottom(vel, bottom, rng):
55+
for idx in range(vel.shape[-1]):
56+
z_bot = bottom[idx]
57+
# Fetch lowest range index
58+
ind_bot = np.nonzero(rng > z_bot)[0][0]
59+
for idim in range(vel.shape[0]):
60+
vnow = vel[idim, :, idx]
61+
# Check that data exists in slice
62+
gd = np.isfinite(vnow) & (vnow != 0)
63+
if not gd.sum():
64+
continue
65+
else:
66+
ind = np.nonzero(gd)[0][-1]
67+
z_top = rng[ind]
68+
# linearly interpolate next lowest range bin based on 0 m/s at bottom
69+
vals = np.interp(rng[ind:ind_bot], [z_top, z_bot], [vnow[ind], 0])
70+
vel[idim, ind:ind_bot, idx] = vals
71+
72+
return vel
73+
74+
def latlon2utm(ds, proj):
75+
PlateC = ccrs.PlateCarree()
76+
proj.x0, proj.y0 = proj.transform_point(
77+
ds["longitude_gps"].mean(), ds["latitude_gps"].mean(), PlateC
78+
)
79+
xy = xr.DataArray(
80+
proj.transform_points(PlateC, ds["longitude_gps"], ds["latitude_gps"])[
81+
:, :2
82+
].T,
83+
coords={"gps": ["x", "y"], "time_gps": ds["longitude_gps"]["time_gps"]},
84+
)
85+
86+
# this seems to work for missing latlon
87+
xy = xy.interp(
88+
time_gps=ds["time"], kwargs={"fill_value": "extrapolate"}
89+
).drop_vars("time_gps")
90+
return xy
91+
92+
def _distance(proj, x, y):
93+
# GPS distance traveled in meters
94+
return np.sqrt((proj.x0 - x) ** 2 + (proj.y0 - y) ** 2)
95+
96+
def calc_Q(vel, x, depth, surface_zoff=None):
97+
# depth and surface_zoff should be positive in down direction
98+
vel = vel.copy()
99+
vel = vel.fillna(0)
100+
if surface_zoff is not None:
101+
# Add a copy of the top row of data
102+
vel = np.vstack((vel[0], vel))
103+
depth = np.hstack((surface_zoff, depth))
104+
if x[0] > x[-1]:
105+
sign = -1
106+
else:
107+
sign = 1
108+
return sign * np.trapz(np.trapz(vel, depth, axis=0), x)
109+
110+
# Extrapolate to bed
111+
vel = ds["vel"].copy()
112+
vel.values = extrap2bottom(ds["vel"].values, water_depth, ds["range"].values)
113+
vel_x = vel[0]
114+
# Get position at each timestep in UTM grid
115+
proj = ccrs.UTM(utm_zone)
116+
xy = latlon2utm(ds, proj)
117+
# Distance from UTM grid origin (mean of GPS points)
118+
_x = _distance(proj, xy[0], xy[1])
119+
# Set distance range for entire transect
120+
Q_x_range = [_x.min(), _x.max()] # meters
121+
122+
# Calculate discharge, power, kinetic energy, and reynolds number
123+
_xinds = (Q_x_range[0] < _x) & (_x < Q_x_range[1])
124+
out = {}
125+
if _xinds.any():
126+
U = vel_x[:, _xinds] # m/s
127+
# Volume Flux, aka Discharge
128+
out["Q"] = calc_Q(
129+
U, xy[0][_xinds], ds["range"], surface_offset
130+
) # m/s * m * m = m^3/s
131+
# Kinetic Energy Flux, aka Power
132+
out["P"] = (
133+
0.5 * rho * calc_Q(U**3, xy[0][_xinds], ds["range"], surface_offset)
134+
) # kg/m^3 * m^3/s^3 * m * m = kg*m^2/s = W
135+
# 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])) / (
139+
xy[0][_xinds].max() - xy[0][_xinds].min()
140+
) # area / surface-width
141+
# Reynolds Number
142+
out["Re"] = ((rho * ds.velds.U_mag.mean() * L) / mu).item()
143+
else:
144+
out["Q"] = np.nan
145+
out["P"] = np.nan
146+
out["J"] = np.nan
147+
out["Re"] = np.nan
148+
149+
ds["discharge"] = xr.DataArray(
150+
np.float32(out["Q"]),
151+
dims=[],
152+
attrs={
153+
"units": "m3 s-1",
154+
"long_name": "Discharge",
155+
},
156+
)
157+
ds["power"] = xr.DataArray(
158+
np.float32(out["P"]),
159+
dims=[],
160+
attrs={
161+
"units": "W",
162+
"long_name": "Power",
163+
},
164+
)
165+
ds["power_density"] = xr.DataArray(
166+
np.float32(out["J"]),
167+
dims=[],
168+
attrs={
169+
"units": "W m-2",
170+
"long_name": "Power Density",
171+
},
172+
)
173+
ds["reynolds_number"] = xr.DataArray(
174+
np.float32(out["Re"]),
175+
dims=[],
176+
attrs={
177+
"units": "1",
178+
"long_name": "Reynolds Number",
179+
},
180+
)
181+
182+
return ds

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ beautifulsoup4
1616
notebook
1717
numexpr>=2.10.0
1818
lxml
19-
bottleneck
19+
bottleneck
20+
cartopy

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"numexpr>=2.10.0",
3838
"lxml",
3939
"bottleneck",
40+
"cartopy",
4041
]
4142

4243
LONG_DESCRIPTION = """

0 commit comments

Comments
 (0)