-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dolfyn: Add horizontal speed and velocity calculation
- Loading branch information
Showing
2 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
143 changes: 143 additions & 0 deletions
143
mhkit/dolfyn/tools/calculate_horizontal_speed_and_direction.m
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
function ds_out = calculate_horizontal_speed_and_direction(ds) | ||
|
||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | ||
% Calculate horizontal speed and direction from velocity components | ||
% | ||
% Parameters | ||
% ------------ | ||
% ds: structure | ||
% Dolfyn data structure containing velocity data. Must include: | ||
% - vel: structure with fields: | ||
% - data: array with velocity components | ||
% - dims: cell array of dimension names | ||
% - coords: structure with coordinate information including: | ||
% - dir: cell array of direction labels ('E', 'N', 'U1', 'U2') | ||
% - attrs: structure with metadata | ||
% - coord_sys: string | ||
% Coordinate system identifier (e.g., "earth", "beam", "inst", "principal") | ||
% | ||
% Returns | ||
% ------------ | ||
% ds_out: structure | ||
% Input structure with additional fields: | ||
% - U_mag: structure | ||
% - data: array of velocity magnitudes (m/s) | ||
% - dims: cell array of dimension names (excluding dir) | ||
% - coords: coordinate information | ||
% - units: "m s-1" | ||
% - standard_name: "sea_water_speed" | ||
% - long_name: "Water Speed" | ||
% - U_dir: structure | ||
% - data: array of flow directions | ||
% - dims: cell array of dimension names (excluding dir) | ||
% - coords: coordinate information | ||
% - units: "degrees_CW_from_[REF]" where [REF] depends on coord_sys: | ||
% - earth: "degrees_CW_from_N" | ||
% - others: "degrees_CW_from_E" (or relevant streamwise component) | ||
% - standard_name: "sea_water_to_direction" | ||
% - long_name: "Water Direction" | ||
% Note: Direction is NaN where velocity magnitude is zero | ||
% Note: For earth coordinates, angles are CW from North [0, 360] | ||
% For other coordinates, angles are CW from East/streamwise [0, 360] | ||
% | ||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | ||
|
||
% Input validation - check for required fields | ||
validateattributes(ds, {'struct'}, {'nonempty'}, mfilename, 'ds'); | ||
assert(isfield(ds, 'attrs'), 'MATLAB:assert:failed', 'Field attrs missing from ds'); | ||
assert(isfield(ds, 'vel'), 'MATLAB:assert:failed', 'Field vel missing from ds'); | ||
validateattributes(ds.vel, {'struct'}, {'nonempty'}, mfilename, 'ds.vel'); | ||
|
||
% Check for required vel subfields | ||
required_fields = {'data', 'dims', 'coords'}; | ||
for i = 1:length(required_fields) | ||
assert(isfield(ds.vel, required_fields{i}), 'MATLAB:assert:failed', ... | ||
['Field ' required_fields{i} ' missing from ds.vel']); | ||
end | ||
|
||
% Check for coords.dir | ||
assert(isfield(ds.vel.coords, 'dir'), 'MATLAB:assert:failed', ... | ||
'Field dir missing from ds.vel.coords'); | ||
validateattributes(ds.vel.coords.dir, {'cell'}, {'nonempty'}, mfilename, 'ds.vel.coords.dir'); | ||
|
||
% Find the index of the 'dir' dimension | ||
dir_idx = find(strcmp(ds.vel.dims, 'dir')); | ||
assert(~isempty(dir_idx), 'MATLAB:assert:failed', ... | ||
"'dir' dimension not found in vel.dims"); | ||
|
||
% Get the size of the data array | ||
data_size = size(ds.vel.data); | ||
|
||
% Ensure data array has enough dimensions | ||
assert(length(data_size) >= dir_idx, 'MATLAB:assert:failed', ... | ||
'Data array has fewer dimensions than the dir index'); | ||
|
||
% Find indices for East and North components in the direction coordinates | ||
dir_coords = ds.vel.coords.dir; | ||
|
||
E_idx = find(strcmp(dir_coords, 'E')); | ||
N_idx = find(strcmp(dir_coords, 'N')); | ||
|
||
% Verify both components exist | ||
assert(~isempty(E_idx), 'MATLAB:assert:failed', ... | ||
'East component not found in vel.coords.dir'); | ||
assert(~isempty(N_idx), 'MATLAB:assert:failed', ... | ||
'North component not found in vel.coords.dir'); | ||
|
||
% Create indexing cell array for the entire data array | ||
idx_cell = repmat({':'}, 1, length(data_size)); | ||
|
||
% Extract East and North velocities using dynamic indexing | ||
idx_cell{dir_idx} = E_idx; | ||
|
||
% Remove singleton dimensions | ||
vel = squeeze(ds.vel.data); | ||
u = squeeze(vel(idx_cell{:})); % East component | ||
|
||
idx_cell{dir_idx} = N_idx; | ||
v = squeeze(vel(idx_cell{:})); % North component | ||
|
||
% Calculate complex velocity | ||
U = u + v * 1j; | ||
U_mag = abs(U); | ||
|
||
% Prepare output structure for magnitude | ||
ds_out = ds; | ||
ds_out.U_mag = struct(); | ||
ds_out.U_mag.data = U_mag; | ||
ds_out.U_mag.dims = ds.vel.dims(1:end ~= dir_idx); | ||
ds_out.U_mag.coords = ds.vel.coords; | ||
ds_out.U_mag.units = "m s-1"; | ||
ds_out.U_mag.standard_name = "sea_water_speed"; | ||
ds_out.U_mag.long_name = "Water Speed"; | ||
|
||
% Calculate direction | ||
angle_rad = angle(U); | ||
angle_deg = angle_rad * (180/pi); | ||
|
||
% Convert angle based on coordinate system | ||
if strcmp(ds.coord_sys, "earth") | ||
% Convert "deg CCW from East" to "deg CW from North" [0, 360] | ||
angle_compass = -(angle_deg - 90); | ||
relative_to = ds.vel.coords.dir{N_idx}; % North component | ||
else | ||
% Switch to clockwise and from [-180, 180] to [0, 360] | ||
angle_compass = -angle_deg; | ||
angle_compass(angle_compass < 0) = angle_compass(angle_compass < 0) + 360; | ||
relative_to = ds.vel.coords.dir{E_idx}; % East/streamwise component | ||
end | ||
|
||
% Ensure angles are in [0, 360] range | ||
angle_compass = mod(angle_compass, 360); | ||
% Set direction to NaN where velocity is zero | ||
angle_compass(U_mag == 0) = NaN; | ||
|
||
ds_out.U_dir = struct(); | ||
% Create direction output | ||
ds_out.U_dir.data = angle_compass; | ||
ds_out.U_dir.dims = ds.vel.dims(1:end ~= dir_idx); | ||
ds_out.U_dir.coords = ds.vel.coords; | ||
ds_out.U_dir.units = "degrees clockwise from " + relative_to; | ||
ds_out.U_dir.standard_name = "sea_water_to_direction"; | ||
ds_out.U_dir.long_name = "Water To Direction"; | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
classdef Dolfyn_Test_VAP < matlab.unittest.TestCase | ||
methods(Test) | ||
function test_basic_velocity_magnitude(testCase) | ||
% Test basic velocity magnitude calculation with simple values | ||
ds.vel.data = zeros(2,2,1,4); % time, range, dir, components | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.data(:,:,:,1) = 3 * ones(2,2,1); % East component | ||
ds.vel.data(:,:,:,2) = 4 * ones(2,2,1); % North component | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "earth"; | ||
|
||
result = calculate_horizontal_speed_and_direction(ds); | ||
|
||
% Magnitude should be 5 (3-4-5 triangle) | ||
expected_magnitude = 5 * ones(2,2,1); | ||
testCase.verifyEqual(result.U_mag.data, expected_magnitude, 'AbsTol', 1e-10); | ||
testCase.verifyEqual(result.U_mag.units, "m s-1"); | ||
end | ||
|
||
function test_different_dir_index(testCase) | ||
% Test with dir in different dimension positions | ||
ds.vel.data = zeros(2,4,2); % time, dir, range | ||
ds.vel.dims = {'time', 'dir', 'range'}; | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "earth"; | ||
|
||
% Set velocity components | ||
idx_east = repmat({':'}, 1, ndims(ds.vel.data)); | ||
idx_north = repmat({':'}, 1, ndims(ds.vel.data)); | ||
idx_east{2} = 1; % E component in dir dimension | ||
idx_north{2} = 2; % N component in dir dimension | ||
|
||
ds.vel.data(idx_east{:}) = 3; % East component | ||
ds.vel.data(idx_north{:}) = 4; % North component | ||
|
||
result = calculate_horizontal_speed_and_direction(ds); | ||
|
||
% Verify dimensions are correct (dir dimension removed) | ||
testCase.verifyEqual(result.U_mag.dims, {'time', 'range'}); | ||
% Verify magnitude calculation | ||
expected_magnitude = 5 * ones(2,2); % 3-4-5 triangle | ||
testCase.verifyEqual(result.U_mag.data, expected_magnitude, 'AbsTol', 1e-10); | ||
end | ||
|
||
function test_squeezed_dimensions(testCase) | ||
% Test with singleton dimensions that need squeezing | ||
ds.vel.data = zeros(2,1,4,1,2); % time, singleton, dir, singleton, range | ||
ds.vel.dims = {'time', 'dir', 'range'}; | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "earth"; | ||
|
||
% Set velocity components | ||
idx_east = repmat({':'}, 1, ndims(ds.vel.data)); | ||
idx_north = repmat({':'}, 1, ndims(ds.vel.data)); | ||
idx_east{3} = 1; % E component in dir dimension | ||
idx_north{3} = 2; % N component in dir dimension | ||
|
||
ds.vel.data(idx_east{:}) = 3; % East component | ||
ds.vel.data(idx_north{:}) = 4; % North component | ||
|
||
result = calculate_horizontal_speed_and_direction(ds); | ||
|
||
% Verify dimensions are correct after squeezing | ||
testCase.verifyEqual(result.U_mag.dims, {'time', 'range'}); | ||
% Verify magnitude calculation | ||
expected_magnitude = 5 * ones(2,2); % 3-4-5 triangle | ||
testCase.verifyEqual(result.U_mag.data, expected_magnitude, 'AbsTol', 1e-10); | ||
end | ||
|
||
function test_zero_velocity(testCase) | ||
% Test behavior with zero velocity | ||
ds.vel.data = zeros(2,2,4); | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.coord_sys = "earth"; | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
|
||
result = calculate_horizontal_speed_and_direction(ds); | ||
|
||
% Verify zero magnitude | ||
testCase.verifyEqual(sum(result.U_mag.data, 'all'), 0); | ||
% Direction should be NaN for zero velocity | ||
testCase.verifyTrue(isnan(sum(result.U_dir.data, 'all'))); | ||
end | ||
|
||
function test_cardinal_directions(testCase) | ||
% Create base dataset | ||
ds.vel.data = zeros(2,2,1,4); | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "earth"; | ||
|
||
% Test East direction (0 degrees mathematical, 90 degrees compass) | ||
ds.vel.data(:,:,:,1) = 1; % East = 1 | ||
ds.vel.data(:,:,:,2) = 0; % North = 0 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 90 * ones(2,2,1), 'AbsTol', 1e-10); | ||
|
||
% Test North direction (90 degrees mathematical, 0 degrees compass) | ||
ds.vel.data(:,:,:,1) = 0; % East = 0 | ||
ds.vel.data(:,:,:,2) = 1; % North = 1 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 0 * ones(2,2,1), 'AbsTol', 1e-10); | ||
|
||
% Test West direction (180 degrees mathematical, 270 degrees compass) | ||
ds.vel.data(:,:,:,1) = -1; % East = -1 | ||
ds.vel.data(:,:,:,2) = 0; % North = 0 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 270 * ones(2,2,1), 'AbsTol', 1e-10); | ||
|
||
% Test South direction (270 degrees mathematical, 180 degrees compass) | ||
ds.vel.data(:,:,:,1) = 0; % East = 0 | ||
ds.vel.data(:,:,:,2) = -1; % North = -1 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 180 * ones(2,2,1), 'AbsTol', 1e-10); | ||
end | ||
|
||
function test_intercardinal_directions(testCase) | ||
% Create base dataset | ||
ds.vel.data = zeros(2,2,1,4); | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "earth"; | ||
|
||
% Test Northeast (45 degrees mathematical, 45 degrees compass) | ||
ds.vel.data(:,:,:,1) = 1; % East = 1 | ||
ds.vel.data(:,:,:,2) = 1; % North = 1 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 45 * ones(2,2,1), 'AbsTol', 1e-10); | ||
|
||
% Test Northwest (135 degrees mathematical, 315 degrees compass) | ||
ds.vel.data(:,:,:,1) = -1; % East = -1 | ||
ds.vel.data(:,:,:,2) = 1; % North = 1 | ||
result = calculate_horizontal_speed_and_direction(ds); | ||
testCase.verifyEqual(result.U_dir.data, 315 * ones(2,2,1), 'AbsTol', 1e-10); | ||
end | ||
|
||
function test_non_earth_coordinate_system(testCase) | ||
% Create base dataset | ||
ds.vel.data = zeros(2,2,1,4); | ||
ds.vel.coords.dir = {'E', 'N', 'U1', 'U2'}; | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
ds.vel.coords.time = 1:2; | ||
ds.vel.coords.range = 1:2; | ||
ds.attrs = struct(); | ||
ds.coord_sys = "principal"; % Change to non-earth coordinate system | ||
|
||
result = calculate_horizontal_speed_and_direction(ds); | ||
|
||
% Verify that U_dir field is present | ||
testCase.verifyTrue(isfield(result, 'U_dir')); | ||
testCase.verifyEqual(result.U_dir.units, "degrees clockwise from E"); | ||
|
||
% Verify that magnitude is still calculated | ||
testCase.verifyTrue(isfield(result, 'U_mag')); | ||
end | ||
|
||
function test_error_conditions(testCase) | ||
% Test missing vel field | ||
ds = struct('attrs', struct(), 'coord_sys', "earth"); | ||
testCase.verifyError(@() calculate_horizontal_speed_and_direction(ds), ... | ||
'MATLAB:assert:failed'); | ||
|
||
% Test missing coords.dir | ||
ds.vel.data = zeros(2,2,1,4); | ||
ds.vel.coords = struct(); | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
testCase.verifyError(@() calculate_horizontal_speed_and_direction(ds), ... | ||
'MATLAB:assert:failed'); | ||
|
||
% Test missing E component | ||
ds.vel.data = zeros(2,2,1,4); | ||
ds.vel.coords.dir = {'N', 'U1', 'U2'}; % Remove E | ||
ds.vel.dims = {'time', 'range', 'dir'}; | ||
testCase.verifyError(@() calculate_horizontal_speed_and_direction(ds), ... | ||
'MATLAB:assert:failed'); | ||
end | ||
|
||
end | ||
end |