Skip to content

Commit 0ad774e

Browse files
authored
Merge pull request #173 from simmsa/feat_dolfyn_turbulence
Feature: Add Dolfyn Turbulence Functionality
2 parents b9e07bf + 9d7011c commit 0ad774e

31 files changed

Lines changed: 12290 additions & 298 deletions

examples/adcp_example.html

Lines changed: 872 additions & 164 deletions
Large diffs are not rendered by default.

examples/adcp_example.m

Lines changed: 796 additions & 81 deletions
Large diffs are not rendered by default.

examples/adcp_example.mlx

286 KB
Binary file not shown.

mhkit/dolfyn/adp/water_depth_from_pressure.m

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
function out = water_depth_from_pressure(ds, options)
22

3-
%%%%%%%%%%%%%%%%%%%%
4-
% Calculates the distance to the water surface. Temperature and salinity
5-
% are used to calculate seawater density, which is in turn used with the
6-
% pressure data to calculate depth.
3+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4+
%
5+
% Calculate water depth from pressure using seawater density
6+
%
7+
% This function calculates the distance to the water surface using pressure
8+
% ADCP sensor data. Temperature and salinity are used to calculate seawater density,
9+
% which is then used with pressure data to calculate depth%
710
%
811
% Parameters
9-
% ----------
10-
% ds: Dataset
11-
% The full adcp dataset
12-
% salinity: numeric
13-
% Water salinity in psu
12+
% ------------
13+
% ds : structure
14+
% ADCP dataset structure containing pressure and temperature data
15+
% ds.pressure.data : Pressure measurements [dbar]
16+
% ds.temp.data : Temperature measurements [°C]
17+
% ds.attrs.h_deploy : Optional deployment height above seafloor [m]
18+
% salinity : double, optional (name-value)
19+
% Water salinity [psu] (default: 35)
1420
%
1521
% Returns
16-
% -------
17-
% out: Dataset
18-
% adds the variables "water_density" and "depth" to the input dataset.
19-
%
20-
% Notes
21-
% -----
22-
% Requires that the instrument's pressure sensor was calibrated/zeroed
23-
% before deployment to remove atmospheric pressure.
22+
% ---------
23+
% out : structure
24+
% Input dataset with added depth and density fields:
25+
% out.depth.data : Water depth measurements [m]
26+
% out.water_density.data : Seawater density [kg/m³]
27+
% out.depth.long_name : Descriptive name
28+
% out.depth.units : "m"
2429
%
25-
%%%%%%%%%%%%%%%%%%%%
30+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2631

2732
arguments
2833
ds

mhkit/dolfyn/io/dolfyn_read.m

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,48 @@
11
function ds=dolfyn_read(filename,options)
22

3-
%%%%%%%%%%%%%%%%%%%%
4-
% Read a binary Nortek (e.g., .VEC, .wpr, .ad2cp, etc.) or RDI
5-
% (.000, .PD0, .ENX, etc.) data file.
3+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4+
%
5+
% Read binary ADCP data files from Nortek or RDI instruments
6+
%
7+
% This function reads binary ADCP data files in .VEC, .wpr, .ad2cp (Nortek)-
8+
% and .000, .PD0, .ENX, (RDI) formats into MATLAB structures that can be saved
9+
% as .nc files and used with MHKiT-MATLAB dolfyn ADCP analysis functions.
610
%
711
% Parameters
812
% ------------
9-
% filename: string
10-
% Filename of instrument file to read.
11-
% userdata: bool or string (optional)
12-
% true, false, or string of userdata.json filename (default true)
13-
% Whether to read the '<base-filename>.userdata.json' file.
14-
% nens: nan, int, or 2-element array (optional)
15-
% nan (default: read entire file), int, or 2-element tuple
16-
% (start, stop) Number of pings to read from the file.
17-
%
18-
% call with options -> dolfyn_read(filename,'userdata',false,'nens',12)
13+
% filename : string
14+
% Path to instrument file to read
15+
% userdata : logical or string, optional (name-value)
16+
% Whether to read '<base-filename>.userdata.json' file
17+
% - true: read userdata.json file (default)
18+
% - false: skip userdata.json file
19+
% - string: path to specific userdata.json file
20+
% nens : double or array, optional (name-value)
21+
% Number of pings/ensembles to read
22+
% - nan: read entire file (default)
23+
% - scalar: read first N pings
24+
% - [start, stop]: read pings from start to stop
1925
%
2026
% Returns
2127
% ---------
22-
% ds: structure
23-
% Structure from the binary instrument data
28+
% ds : structure
29+
% ADCP dataset structure containing:
30+
% ds.vel.data : Velocity data [range x time x beam] [m/s]
31+
% ds.coords : Coordinate information (time, range, beam)
32+
% ds.attrs : Instrument metadata and deployment information
33+
%
34+
% Examples
35+
% --------
36+
% Read entire ADCP file, may be slow for large (>1GB) files
37+
% ds = dolfyn_read('deployment.ad2cp');
38+
%
39+
% Read first 1000 ensembles without userdata
40+
% ds = dolfyn_read('deployment.ad2cp', 'userdata', false, 'nens', 1000);
41+
%
42+
% Read specific ensemble range
43+
% ds = dolfyn_read('deployment.ad2cp', 'nens', [500, 1500]);
2444
%
25-
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
45+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2646
arguments
2747
filename
2848
options.userdata = true;

mhkit/dolfyn/rotate/calc_tilt.m

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
function tilt = calc_tilt(pitch, roll, options)
2+
3+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4+
%
5+
% Calculate instrument tilt from pitch and roll angles.
6+
%
7+
% This function calculates the total tilt angle of an ADCP instrument from
8+
% pitch and roll measurements. This function warns the user if the tilt is above 5°
9+
% as tilts above this threshold are likely to have a negative affect on the accuracy
10+
% of flow and turbulence calculations.
11+
%
12+
% Parameters
13+
% ------------
14+
% pitch: array
15+
% time series of pitch angle (forward/backward tilt)
16+
% roll: array
17+
% time series of roll angle (side-to-side tilt)
18+
% units: string
19+
% Units of input angles: 'degrees', 'deg', 'radians' or 'rad'
20+
% Default: 'degrees'
21+
% output_units: string
22+
% Units for output tilt: 'degrees', 'deg', 'radians' or 'rad'
23+
% Default: same as input units
24+
%
25+
% Returns
26+
% ---------
27+
% tilt: array
28+
% tilt angle in specified output units
29+
%
30+
% Algorithm
31+
% ---------
32+
% tilt_rad = atan( √( tan(roll_rad)² + tan(pitch_rad)² ) )
33+
%
34+
% Example
35+
% -------
36+
% % Calculate a time series of tilt from pitch and roll time series in degrees
37+
% tilt = calc_tilt(pitch_data, roll_data, 'units', 'degrees');
38+
%
39+
% Notes
40+
% -----
41+
% - Large tilts (> 5°) can affect turbulence measurements
42+
% - This function issues warnings for tilts > 5°
43+
%
44+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
45+
46+
arguments
47+
pitch
48+
roll
49+
options.units = 'degrees'
50+
options.output_units = ''
51+
end
52+
53+
% Validate inputs
54+
if ~isnumeric(pitch) || ~isnumeric(roll)
55+
error('mhkit:dolfyn:calc_tilt: Pitch and roll must be numeric arrays');
56+
end
57+
58+
if ~isequal(size(pitch), size(roll))
59+
error('mhkit:dolfyn:calc_tilt: Pitch and roll arrays must have the same size');
60+
end
61+
62+
% Validate units
63+
valid_units = {'degrees', 'radians', 'deg', 'rad'};
64+
if ~ismember(lower(options.units), valid_units)
65+
error('mhkit:dolfyn:calc_tilt: Units must be ''degrees'' or ''radians''');
66+
end
67+
68+
% Set default output units
69+
if isempty(options.output_units)
70+
options.output_units = options.units;
71+
end
72+
73+
if ~ismember(lower(options.output_units), valid_units)
74+
error('mhkit:dolfyn:calc_tilt: Output units must be ''degrees'' or ''radians''');
75+
end
76+
77+
% Normalize unit names
78+
input_is_degrees = ismember(lower(options.units), {'degrees', 'deg'});
79+
output_is_degrees = ismember(lower(options.output_units), {'degrees', 'deg'});
80+
81+
% Convert to radians if needed for calculation
82+
if input_is_degrees
83+
pitch_rad = deg2rad(pitch);
84+
roll_rad = deg2rad(roll);
85+
else
86+
pitch_rad = pitch;
87+
roll_rad = roll;
88+
end
89+
90+
% Calculate tilt using trigonometric relationship
91+
% Source: mhkit_python:dolfyn.rotate.base.calc_tilt, author @jmcvey3
92+
tilt_rad = atan(sqrt(tan(roll_rad).^2 + tan(pitch_rad).^2));
93+
94+
% Quality assessment and warnings
95+
% Convert tilt to degrees for quality assessment regardless of output units
96+
tilt_deg = rad2deg(tilt_rad);
97+
max_tilt_deg = max(tilt_deg(:));
98+
mean_tilt_deg = mean(tilt_deg(:));
99+
100+
% Issue warnings for large tilts
101+
if max_tilt_deg > 5
102+
warning('mhkit:dolfyn: Maximum tilt %.1f° exceeds recommended 5° limit for accurate turbulence measurements', max_tilt_deg);
103+
fprintf('Tilt Analysis Summary:\n');
104+
fprintf(' Mean tilt: %.2f°\n', mean_tilt_deg);
105+
fprintf(' Max tilt: %.2f°\n', max_tilt_deg);
106+
fprintf(' Std tilt: %.2f°\n', std(tilt_deg(:)));
107+
end
108+
109+
% Convert to desired output units
110+
if output_is_degrees
111+
tilt = rad2deg(tilt_rad);
112+
else
113+
tilt = tilt_rad;
114+
end
115+
end

mhkit/dolfyn/tools/average_by_dimension.m

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,19 @@
4545
%
4646
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4747

48-
if nargin < 3
49-
dim_to_find = 'time';
50-
end
51-
if nargin < 2
52-
error('n_samples is required');
53-
end
48+
arguments
49+
ds (1,1) struct
50+
n_samples (1,1) double {mustBePositive, mustBeInteger}
51+
dim_to_find {mustBeTextScalar} = 'time'
52+
end
5453

5554
ds_out = ds; % Start with a copy of the input
55+
56+
% Store n_bin in attrs for compatibility with turbulence functions
57+
if ~isfield(ds_out, 'attrs')
58+
ds_out.attrs = struct();
59+
end
60+
ds_out.attrs.n_bin = n_samples;
5661

5762
% Validate dimension name exists in at least one field
5863
dim_exists = false;
@@ -124,14 +129,30 @@
124129
inv_perm_order(perm_order) = 1:length(perm_order);
125130
ds_out.(current_field).data = permute(reshape(mean_data, [n_bins sz(perm_order(2:end))]), inv_perm_order);
126131

132+
% Calculate standard deviation for velocity fields (following Python DOLfYN)
133+
if strcmp(current_field, 'vel') || contains(current_field, 'vel')
134+
% Calculate standard deviation along first dimension
135+
std_data = squeeze(std(reshaped, 0, 1, 'omitnan'));
136+
137+
% Create vel_std field
138+
std_field_name = [current_field '_std'];
139+
ds_out.(std_field_name) = ds_out.(current_field); % Copy structure
140+
ds_out.(std_field_name).data = permute(reshape(std_data, [n_bins sz(perm_order(2:end))]), inv_perm_order);
141+
ds_out.(std_field_name).long_name = 'Velocity Standard Deviation';
142+
ds_out.(std_field_name).description = 'Standard deviation calculated during ensemble averaging';
143+
end
144+
127145
% Update coordinates for this dimension if they exist
128146
if isfield(ds.(current_field), 'coords')
129147
coord_fields = fieldnames(ds.(current_field).coords);
130148
for k = 1:length(coord_fields)
131149
coord_field = coord_fields{k};
132150
if contains(lower(coord_field), lower(dim_to_find))
133151
coord_data = ds.(current_field).coords.(coord_field);
134-
ds_out.(current_field).coords.(coord_field) = coord_data(1:n_samples:usable_samples);
152+
% For numeric data (like Unix timestamps), take arithmetic mean of each bin
153+
% This matches Python's sequential chunking with mean per bin
154+
coord_reshaped = reshape(coord_data(1:usable_samples), n_samples, n_bins);
155+
ds_out.(current_field).coords.(coord_field) = mean(coord_reshaped, 1, 'omitnan')';
135156
end
136157
end
137158
end
@@ -147,8 +168,41 @@
147168
if contains(lower(coord_fields{i}), lower(dim_to_find))
148169
coord_data = ds.coords.(coord_fields{i});
149170
usable_samples = floor(length(coord_data)/n_samples) * n_samples;
150-
ds_out.coords.(coord_fields{i}) = coord_data(1:n_samples:usable_samples);
171+
n_bins = usable_samples / n_samples;
172+
% For numeric data (like Unix timestamps), take arithmetic mean of each bin
173+
% This matches Python's sequential chunking with mean per bin
174+
coord_reshaped = reshape(coord_data(1:usable_samples), n_samples, n_bins);
175+
ds_out.coords.(coord_fields{i}) = mean(coord_reshaped, 1, 'omitnan')';
151176
end
152177
end
153178
end
179+
180+
% Calculate U_std from horizontal velocity components (following Python DOLfYN)
181+
if isfield(ds_out, 'vel') && isfield(ds_out, 'vel_std')
182+
% Calculate horizontal velocity magnitude standard deviation
183+
if size(ds_out.vel.data, 3) >= 2 % Check we have at least u and v components
184+
u_mean = ds_out.vel.data(:, :, 1);
185+
v_mean = ds_out.vel.data(:, :, 2);
186+
u_std = ds_out.vel_std.data(:, :, 1);
187+
v_std = ds_out.vel_std.data(:, :, 2);
188+
189+
% Calculate U_mag standard deviation using error propagation formula
190+
% U_std = sqrt((u*u_std)^2 + (v*v_std)^2) / U_mag
191+
u_mag = sqrt(u_mean.^2 + v_mean.^2);
192+
u_std_mag = sqrt((u_mean .* u_std).^2 + (v_mean .* v_std).^2) ./ u_mag;
193+
194+
% Handle division by zero
195+
u_std_mag(u_mag == 0) = 0;
196+
197+
% Create U_std field
198+
ds_out.U_std = struct();
199+
ds_out.U_std.data = single(u_std_mag);
200+
ds_out.U_std.dims = ds_out.vel.dims(1:2); % Remove direction dimension
201+
ds_out.U_std.coords = ds_out.vel.coords;
202+
ds_out.U_std.coords = rmfield(ds_out.U_std.coords, 'dir'); % Remove dir coordinate
203+
ds_out.U_std.units = "m s-1";
204+
ds_out.U_std.long_name = "Water Velocity Standard Deviation";
205+
ds_out.U_std.description = 'Horizontal velocity magnitude standard deviation from ensemble averaging';
206+
end
207+
end
154208
end

mhkit/dolfyn/tools/dolfyn_plot.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ function plot_3d_data(current_dim)
288288
% Set colormap based on field type
289289
switch field
290290
case 'vel'
291-
colormap(bluewhitered_colormap(256))
291+
colormap(cmocean('balance', 256))
292292
case 'corr'
293-
colormap(viridis_colormap(256))
293+
colormap(cmocean('haline', 256))
294294
otherwise
295-
colormap('default') % Use MATLAB's default colormap
295+
% Default colormap
296+
colormap(cmocean('thermal', 256))
296297
end
297298

298299
% Set colorbar limits if provided

0 commit comments

Comments
 (0)