


Takes the automatically identified river node positions generated by
get_EHYPE_rivers or get_FVCOM_rivers and splits or removes them based on
thresholds of discharge for the former and distance from the open
boundary join with the coastline.
Mobj = fix_river_nodes(Mobj)
DESCRIPTION:
The automatic identifcation of model nodes at which river inputs are
discharged sometimes leads to problems with model stability.
Specifically:
1. Nodes very close to the open boundary join with the coastline
can also cause high velocities to occur which if you have bounds
checking enabled, will stop the model.
2. Very large discharges into relatively small elements (e.g. the
Rhine discharge) cause the model to crash.
3. Rivers discharging into shallow elements can lead to
instabilities.
This function checks that:
1. Rivers are deleted if their distance from the open boundary join
with the coastline is less than the specified threshold.
2. Any rivers with discharges above the specified threshold are
split over a number of nodes such that each node has a maximum
discharge less than the treshold.
3. Optionally, each river is optimised to use the deepest node
within ths distance threshold specified.
This order is relatively important otherwise the splitting could put
nodes within the land/open boundary joint radius and reduce the river
discharge for a given river by eliminating only some of the river
nodes.
INPUT:
Mobj - struct generated by get_EHYPE_rivers or get_FVCOM_rivers with
the following fields:
nVerts - number of nodes in the model domain
nObs - number of open boundaries
lon, lat - nodal positions in spherical coordinates
tri - unstructured grid triangulation table
read_obc_nodes - open boundary node IDs
nRivers - number of rivers in the model domain
river_nodes - currently identified river nodes
river_names - currently identified river names
river_flux - river discharge time series
max_discharge - river discharge threshold above which rivers will be
split over several nodes (in m^{3}s^{-1}).
dist_thresh - distance from the open boundary nodes which connect with
land within which nodes will be removed from the river data arrays.
The following optional keyword-argument pairs are also supported:
'depth_optimise' - set to a depth beyond which a search for the deepest
node to use is triggered for the river within the distance
threshold (dist_thresh) specified. This increases the stability of
FVCOM. Defaults to false.
'debug' - set to true to plot adjusted river nodes from the depth
optimisation procedure. Defaults to false.
OUTPUT:
Mobj - struct with adjusted river_* fields listed above.
TODO:
- Check we don't split a river node into nodes which fall within the
distance threshold for the land/open boundary joint.
Author(s)
Pierre Cazenave (Plymouth Marine Laboratory)
Revision history:
2013-12-13 First version based on the EHYPE section of my
create_files_monthly.m script.
2014-01-30 Fix a bug revealed when running this script on a larger
model domain whereby the splitting of discharges across multiple
nodes when a threshold discharge is exceeded didn't work if more than
one river exceeded that threshold. Also add better exclusion of
candidate river nodes (those with two land boundaries only are now
excluded, as well as open ocean nodes and existing river nodes).
2015-09-24 Add check for whether we actually have any rivers to
process.
2016-05-03 Update the number of rivers after fixing river nodes.
2016-05-10 Add new option to pick the deepest node within the given
radius. This is done after the splitting of river nodes but before the
checks for a river input at a node connected to two land boundaries.
This is because a river at a node with two land boundaries is always
catastrophic, whereas a shallow node is sometimes catastrophic. This
approach should also mean we minimise the risk of putting a node back
onto a shallower node.
2016-05-13 Move the removal of invalid coastline nodes into the
function to read coastline nodes rather than having it in the splitting
function.
2016-08-11 Clarify the warning about the minimum water depth to
indicate which way is down.
2016-08-15 Make the depth optimisation take a depth as an argument
rather than a boolean so we can only deepen nodes which are shallower
than the given depth.
==========================================================================


0001 function Mobj = fix_river_nodes(Mobj, max_discharge, dist_thresh, varargin) 0002 % Takes the automatically identified river node positions generated by 0003 % get_EHYPE_rivers or get_FVCOM_rivers and splits or removes them based on 0004 % thresholds of discharge for the former and distance from the open 0005 % boundary join with the coastline. 0006 % 0007 % Mobj = fix_river_nodes(Mobj) 0008 % 0009 % DESCRIPTION: 0010 % The automatic identifcation of model nodes at which river inputs are 0011 % discharged sometimes leads to problems with model stability. 0012 % Specifically: 0013 % 1. Nodes very close to the open boundary join with the coastline 0014 % can also cause high velocities to occur which if you have bounds 0015 % checking enabled, will stop the model. 0016 % 2. Very large discharges into relatively small elements (e.g. the 0017 % Rhine discharge) cause the model to crash. 0018 % 3. Rivers discharging into shallow elements can lead to 0019 % instabilities. 0020 % 0021 % This function checks that: 0022 % 1. Rivers are deleted if their distance from the open boundary join 0023 % with the coastline is less than the specified threshold. 0024 % 2. Any rivers with discharges above the specified threshold are 0025 % split over a number of nodes such that each node has a maximum 0026 % discharge less than the treshold. 0027 % 3. Optionally, each river is optimised to use the deepest node 0028 % within ths distance threshold specified. 0029 % This order is relatively important otherwise the splitting could put 0030 % nodes within the land/open boundary joint radius and reduce the river 0031 % discharge for a given river by eliminating only some of the river 0032 % nodes. 0033 % 0034 % INPUT: 0035 % Mobj - struct generated by get_EHYPE_rivers or get_FVCOM_rivers with 0036 % the following fields: 0037 % nVerts - number of nodes in the model domain 0038 % nObs - number of open boundaries 0039 % lon, lat - nodal positions in spherical coordinates 0040 % tri - unstructured grid triangulation table 0041 % read_obc_nodes - open boundary node IDs 0042 % nRivers - number of rivers in the model domain 0043 % river_nodes - currently identified river nodes 0044 % river_names - currently identified river names 0045 % river_flux - river discharge time series 0046 % max_discharge - river discharge threshold above which rivers will be 0047 % split over several nodes (in m^{3}s^{-1}). 0048 % dist_thresh - distance from the open boundary nodes which connect with 0049 % land within which nodes will be removed from the river data arrays. 0050 % 0051 % The following optional keyword-argument pairs are also supported: 0052 % 'depth_optimise' - set to a depth beyond which a search for the deepest 0053 % node to use is triggered for the river within the distance 0054 % threshold (dist_thresh) specified. This increases the stability of 0055 % FVCOM. Defaults to false. 0056 % 'debug' - set to true to plot adjusted river nodes from the depth 0057 % optimisation procedure. Defaults to false. 0058 % 0059 % OUTPUT: 0060 % Mobj - struct with adjusted river_* fields listed above. 0061 % 0062 % TODO: 0063 % - Check we don't split a river node into nodes which fall within the 0064 % distance threshold for the land/open boundary joint. 0065 % 0066 % Author(s) 0067 % Pierre Cazenave (Plymouth Marine Laboratory) 0068 % 0069 % Revision history: 0070 % 2013-12-13 First version based on the EHYPE section of my 0071 % create_files_monthly.m script. 0072 % 2014-01-30 Fix a bug revealed when running this script on a larger 0073 % model domain whereby the splitting of discharges across multiple 0074 % nodes when a threshold discharge is exceeded didn't work if more than 0075 % one river exceeded that threshold. Also add better exclusion of 0076 % candidate river nodes (those with two land boundaries only are now 0077 % excluded, as well as open ocean nodes and existing river nodes). 0078 % 2015-09-24 Add check for whether we actually have any rivers to 0079 % process. 0080 % 2016-05-03 Update the number of rivers after fixing river nodes. 0081 % 2016-05-10 Add new option to pick the deepest node within the given 0082 % radius. This is done after the splitting of river nodes but before the 0083 % checks for a river input at a node connected to two land boundaries. 0084 % This is because a river at a node with two land boundaries is always 0085 % catastrophic, whereas a shallow node is sometimes catastrophic. This 0086 % approach should also mean we minimise the risk of putting a node back 0087 % onto a shallower node. 0088 % 2016-05-13 Move the removal of invalid coastline nodes into the 0089 % function to read coastline nodes rather than having it in the splitting 0090 % function. 0091 % 2016-08-11 Clarify the warning about the minimum water depth to 0092 % indicate which way is down. 0093 % 2016-08-15 Make the depth optimisation take a depth as an argument 0094 % rather than a boolean so we can only deepen nodes which are shallower 0095 % than the given depth. 0096 % 0097 %========================================================================== 0098 0099 [~, subname] = fileparts(mfilename('fullpath')); 0100 0101 global ftbverbose 0102 if ftbverbose 0103 fprintf('\nbegin : %s\n', subname) 0104 end 0105 0106 depth_optimise = false; 0107 debug = false; 0108 for aa = 1:2:length(varargin) 0109 switch varargin{aa} 0110 case 'depth_optimise' 0111 depth_optimise = true; 0112 depth_threshold = varargin{aa + 1}; 0113 case 'debug' 0114 debug = varargin{aa + 1}; 0115 end 0116 end 0117 0118 % Check we actually have some rivers to process. 0119 if Mobj.nRivers < 1 0120 warning('No rivers specified in the domain.') 0121 0122 if ftbverbose 0123 fprintf('end : %s\n', subname) 0124 end 0125 return 0126 end 0127 0128 % Generate names for the variables we're going to use. These may not all be 0129 % used if you are not running ERSEM, but we build them in case. 0130 evars = {'flux', 'temp', 'salt', 'nh4', 'no3', 'o', 'p', 'sio3', 'dic', 'bioalk', 'alt'}; 0131 enames = cell(length(evars)); 0132 fnames = cell(length(evars)); 0133 for e = 1:length(evars) 0134 enames{e} = sprintf('river_%s', evars{e}); 0135 fnames{e} = sprintf('fv_%s', evars{e}); 0136 end 0137 0138 % Find the model coastline. 0139 coast_nodes = get_coastline(Mobj); 0140 0141 % Remove river nodes close to the open boundaries. 0142 Mobj = clear_boundary_nodes(Mobj, dist_thresh, enames); 0143 0144 % Split big rivers over adjacent nodes. 0145 Mobj = split_big_rivers(Mobj, max_discharge, coast_nodes, enames, fnames); 0146 0147 % If we've been asked to optimise the depth of nodes, do that now. We may 0148 % have to rerun some of the checks above. I don't know yet. 0149 if depth_optimise 0150 Mobj = optimise_depth(Mobj, depth_threshold, dist_thresh, coast_nodes, debug); 0151 end 0152 0153 % Update the number of rivers we have. 0154 Mobj.nRivers = length(Mobj.river_nodes); 0155 0156 if ftbverbose 0157 fprintf('end : %s\n', subname) 0158 end 0159 0160 function coast_nodes_valid = get_coastline(Mobj) 0161 % Find the appropriate nodes from the coastline nodes. This is mostly 0162 % lifted from get_EHYPE_rivers.m. 0163 [~, ~, ~, bnd] = connectivity([Mobj.lon, Mobj.lat], Mobj.tri); 0164 boundary_nodes = 1:Mobj.nVerts; 0165 boundary_nodes = boundary_nodes(bnd); 0166 coast_nodes = boundary_nodes(~ismember(boundary_nodes, ... 0167 [Mobj.read_obc_nodes{:}])); 0168 0169 % Remove invalid coastline nodes (from the perspective of rivers). Those 0170 % are which are connected to two land nodes. I can't think of an elegant 0171 % way of doing this, so brute force it is. This is a bit slow (~10 seconds) 0172 % on my grid with ~13000 coastal nodes. 0173 nogood = nan(size(coast_nodes)); % clear out the nans later. 0174 for nn = 1:length(coast_nodes) 0175 [row, ~] = find(Mobj.tri == coast_nodes(nn)); 0176 if length(row) == 1 0177 nogood(nn) = coast_nodes(nn); 0178 end 0179 end 0180 nogood = nogood(~isnan(nogood)); 0181 coast_nodes_valid = setdiff(coast_nodes, nogood); 0182 0183 0184 function Mobj = clear_boundary_nodes(Mobj, dist_thresh, enames) 0185 % Remove nodes close to the open boundary joint with the coastline. 0186 % Identifying the coastline/open boundary joining nodes is simply a case of 0187 % taking the first and last node ID for each open boundary. Using that 0188 % position, we can find any river nodes which fall within that distance and 0189 % simply remove their data from the relevant Mobj.river_* arrays. 0190 0191 global ftbverbose 0192 0193 obc_land_nodes = nan(Mobj.nObs, 2); 0194 for n = 1:Mobj.nObs 0195 obc_land_nodes(n, :) = [Mobj.read_obc_nodes{n}(1), ... 0196 Mobj.read_obc_nodes{n}(end)]; 0197 for d = 1:2 0198 [dist, idx] = sort(sqrt(... 0199 (Mobj.lon(obc_land_nodes(n, d)) - Mobj.lon(Mobj.river_nodes)).^2 + ... 0200 (Mobj.lat(obc_land_nodes(n, d)) - Mobj.lat(Mobj.river_nodes)).^2 ... 0201 )); 0202 if min(dist) < dist_thresh 0203 % Delete the positions with indices less than the threshold. 0204 % This could be more than one river node. 0205 inds = find(dist < dist_thresh); 0206 if ftbverbose 0207 % Have to loop through the indices because fprint'ing a 0208 % cell array (the river names) is tough... 0209 for i = 1:length(inds) 0210 fprintf('Remove river %s at %.2f, %.2f\n', ... 0211 Mobj.river_names{idx(inds(i))}, ... 0212 Mobj.lon(Mobj.river_nodes(idx(inds(i)))), ... 0213 Mobj.lat(Mobj.river_nodes(idx(inds(i))))) 0214 end 0215 end 0216 Mobj.river_nodes(idx(inds)) = []; 0217 Mobj.river_flux(:, idx(inds)) = []; 0218 Mobj.river_names(idx(inds)) = []; 0219 % Also trim the temperature, salinity and ERSEM variables, 0220 % if we have them. 0221 for e = 1:length(enames) 0222 if isfield(Mobj, enames{e}) 0223 Mobj.(enames{e})(:, idx(inds)) = []; 0224 end 0225 end 0226 end 0227 end 0228 end 0229 0230 function Mobj = split_big_rivers(Mobj, max_discharge, coast_nodes, enames, fnames) 0231 % For some of the rivers, the discharge is very large and is the source of 0232 % model instability (e.g. the Rhine crashes my irish_sea_v20 grid). So, 0233 % identify discharges in excess of some value and split that discharge over 0234 % adjacent elements, making sure they're still valid nodes and not used for 0235 % another river. Do this second so we don't have to worry about removing 0236 % nodes based on their distance from the land/open boundary joint which 0237 % have been split, which is the case if these two steps are reversed. 0238 0239 global ftbverbose 0240 0241 riv_idx = 1:size(Mobj.river_flux, 2); 0242 riv_idx = riv_idx(max(Mobj.river_flux) > max_discharge); 0243 0244 if ftbverbose 0245 fprintf('%i river(s) exceed the specified discharge threshold (%.2f m^{3}s^{-1}).\n', length(riv_idx), max_discharge) 0246 end 0247 0248 for r = riv_idx 0249 % Based on the flux data, find adjacent nodes over which to split the 0250 % data and then split all variables (both physics and, optionally, 0251 % ERSEM data). 0252 0253 % Eliminate any existing river nodes from the list of candidates. 0254 candidates = setdiff(coast_nodes, Mobj.river_nodes); 0255 0256 % Extract the river data for the rivers in excess of the threshold so 0257 % we can remove them from the existing arrays. 0258 for e = 1:length(enames) 0259 if isfield(Mobj, enames{e}) 0260 tmp_struct.(enames{e}) = Mobj.(enames{e})(:, r); 0261 end 0262 end 0263 0264 % Save the nodes and names of this river. 0265 river_node = Mobj.river_nodes(r); 0266 river_names = Mobj.river_names(r); 0267 0268 % Replace the current time series with NaNs. We'll remove them after 0269 % we've split the rivers in riv_idx. If we remove them here, then the 0270 % indices in riv_idx get offset by some amount (1 position each time). 0271 % Doing that is hard to track, so we'll replace with NaNs and remove 0272 % afterwards. 0273 for e = 1:length(enames) 0274 if isfield(Mobj, enames{e}) 0275 Mobj.(enames{e})(:, r) = nan; 0276 end 0277 end 0278 Mobj.river_nodes(r) = nan; 0279 Mobj.river_names{r} = 'REMOVEME'; 0280 0281 % Split the discharge based on the number of times the specified 0282 % maximum fits into the actual maximum. So, if the maximum is 10000 0283 % m^{3}s^{-1} and max_discharge is 2000 m^{3}s^{-1}, then you split 0284 % over 5 nodes. 0285 nsplit = ceil(max(tmp_struct.river_flux) / max_discharge); 0286 tmp_struct.river_flux = tmp_struct.river_flux / nsplit; 0287 % Scale the data by nsplit. 0288 % for e = 1:length(enames) 0289 % if isfield(Mobj, enames{e}) 0290 % tmp_struct.(enames{e}) = tmp_struct.(enames{e}) / nsplit; 0291 % end 0292 % end 0293 0294 % We can keep the original node, but we need to find the 0295 % remaining nsplit-1 nodes. 0296 fv_obc = river_node; 0297 fv_names = {sprintf('%s_%i', river_names{1}, 1)}; 0298 for e = 1:length(fnames) 0299 if isfield(Mobj, enames{e}) 0300 tmp_struct.(fnames{e}) = repmat(tmp_struct.(enames{e}), [1, nsplit]); 0301 end 0302 end 0303 0304 for ff = 2:nsplit 0305 % Update the list of candidates to exclude those we've just found. 0306 candidates = setdiff(candidates, fv_obc); 0307 0308 [~, idx] = min(sqrt( ... 0309 (Mobj.lon(river_node) - Mobj.lon(candidates)).^2 + ... 0310 (Mobj.lat(river_node) - Mobj.lat(candidates)).^2)); 0311 0312 % Now we can check if this node is an FVCOM-compatible one 0313 % (element of which it's a part has no more than one land 0314 % boundary). 0315 [row, ~] = find(Mobj.tri == candidates(idx)); 0316 0317 if length(row) == 1 0318 % This is a bad node because it is a part of only one element. 0319 % The rivers need two adjacent elements to work reliably (?). 0320 % So, we need to repeat the process above until we find a node 0321 % that's connected to two elements. We'll try the other nodes 0322 % in the current element before searching the rest of the 0323 % coastline (which is computationally expensive). 0324 0325 % Remove the current node index from the list of candidates 0326 % (i.e. leave only the two other nodes in the element). 0327 mask = Mobj.tri(row, :) ~= candidates(idx); 0328 n_tri = Mobj.tri(row, mask); 0329 0330 % Remove values which aren't coastline values (we don't want to 0331 % set the river node to an open water node). 0332 n_tri = intersect(n_tri, candidates); 0333 0334 % Of the remaining nodes in the element, find the closest one 0335 % to the original river location. 0336 [~, n_idx] = sort(sqrt( ... 0337 (Mobj.rivers.positions(r, 1) - Mobj.lon(n_tri)).^2 ... 0338 + (Mobj.rivers.positions(r, 2) - Mobj.lon(n_tri)).^2)); 0339 0340 [row_2, ~] = find(Mobj.tri == n_tri(n_idx(1))); 0341 if length(n_idx) > 1 0342 [row_3, ~] = find(Mobj.tri == n_tri(n_idx(2))); 0343 end 0344 % Closest first 0345 if length(row_2) > 1 0346 idx = find(candidates == n_tri(n_idx(1))); 0347 % The other one (only if we have more than one node to 0348 % consider). 0349 elseif length(n_idx) > 1 && length(row_3) > 1 0350 idx = find(candidates == n_tri(n_idx(2))); 0351 % OK, we need to search across all the other coastline 0352 % nodes. 0353 else 0354 % TODO: Implement a search of all the other coastline 0355 % nodes. My testing indicates that we never get here (at 0356 % least for the grids I've tested). I'd be interested to 0357 % see the mesh which does get here... 0358 continue 0359 end 0360 fprintf('alternate node ') 0361 end 0362 0363 % Update the node ID list and the river names list. The flux we've 0364 % already done because we know it's just river_flux/nsplit in 0365 % nsplit columns. 0366 fv_obc(ff) = candidates(idx); 0367 fv_names{ff} = sprintf('%s_%i', river_names{1}, ff); 0368 end 0369 0370 if ftbverbose 0371 fprintf('Split river %s over %i nodes.\n', river_names{1}, nsplit) 0372 end 0373 0374 % Now we can append these new rivers to the existing list of 0375 % discharges, nodes and names. 0376 for e = 1:length(enames) 0377 if isfield(Mobj, enames{e}) 0378 Mobj.(enames{e}) = [Mobj.(enames{e}), tmp_struct.(fnames{e})]; 0379 end 0380 end 0381 Mobj.river_names = [Mobj.river_names; fv_names']; 0382 Mobj.river_nodes = [Mobj.river_nodes, fv_obc]; 0383 end 0384 0385 % Remove all the original river data for the split rivers. Check we're 0386 % doing the right columns by checking if the first row of the fluxes are 0387 % all NaNs for the riv_idx indices. 0388 if all(isnan(Mobj.river_flux(1, riv_idx))) 0389 for e = 1:length(enames) 0390 if isfield(Mobj, enames{e}) 0391 Mobj.(enames{e})(:, riv_idx) = []; 0392 end 0393 end 0394 Mobj.river_nodes(riv_idx) = []; 0395 Mobj.river_names(riv_idx) = []; 0396 end 0397 0398 function Mobj = optimise_depth(Mobj, depth_threshold, dist_thresh, coast_nodes, debug) 0399 % For each river node, search within the distance threshold given and pick 0400 % the deepest coastline node for that river. 0401 0402 global ftbverbose 0403 0404 coast_depth = Mobj.h(coast_nodes); 0405 coast_lon = Mobj.lon(coast_nodes); 0406 coast_lat = Mobj.lat(coast_nodes); 0407 0408 for r = 1:length(Mobj.river_nodes) 0409 0410 % The current river index in the global arrays. 0411 ri = Mobj.river_nodes(r); 0412 0413 % Find the nearest nodes. 0414 [distance, candidates] = sort(sqrt((coast_lon - Mobj.lon(ri)).^2 + ... 0415 (coast_lat - Mobj.lat(ri)).^2)); 0416 candidate_nodes = candidates(distance < dist_thresh); 0417 % Find the deepest node within the search radius. 0418 [deepest_depth, deepest_index] = max(coast_depth(candidates(distance < dist_thresh))); 0419 % Update if we've deepened this node. 0420 if ri ~= candidate_nodes(deepest_index) 0421 % Only update if we improve matters (deepen a river input node) and 0422 % are shallower than the threshold depth given. 0423 if deepest_depth > Mobj.h(ri) && Mobj.h(ri) < depth_threshold 0424 % Let everyone know what's going on. 0425 if ftbverbose 0426 fprintf(['Moving river %s to a node with depth %.2f', ... 0427 ' from one with a depth of %.2f (%.2fm deeper).\n'], ... 0428 Mobj.river_names{r}, ... 0429 Mobj.h(coast_nodes(candidate_nodes(deepest_index))), ... 0430 Mobj.h(Mobj.river_nodes(r)), ... 0431 Mobj.h(coast_nodes(candidate_nodes(deepest_index))) - Mobj.h(ri)) 0432 end 0433 0434 % Update the mesh object. 0435 Mobj.river_nodes(r) = coast_nodes(candidate_nodes(deepest_index)); 0436 0437 if debug 0438 figure(1) 0439 clf 0440 triplot(Mobj.tri, Mobj.lon, Mobj.lat, 'k') 0441 hold on 0442 plot(coast_lon, coast_lat, 'r.') 0443 scatter(coast_lon(candidates(distance < dist_thresh)), ... 0444 coast_lat(candidates(distance < dist_thresh)), ... 0445 30, ... 0446 -coast_depth(candidates(distance < dist_thresh)), ... 0447 'filled') 0448 colorbar 0449 plot(Mobj.lon(ri), Mobj.lat(ri), 'r^', 'MarkerSize', 20) 0450 plot(coast_lon(candidate_nodes(deepest_index)), ... 0451 coast_lat(candidate_nodes(deepest_index)), ... 0452 'b^', ... 0453 'MarkerSize', 20) 0454 axis('tight', 'equal') 0455 legend('grid', 'coastline', 'depths', 'original', 'new') 0456 legend('BoxOff') 0457 xlim([Mobj.lon(ri) - dist_thresh * 2, Mobj.lon(ri) + dist_thresh * 2]) 0458 ylim([Mobj.lat(ri) - dist_thresh * 2, Mobj.lat(ri) + dist_thresh * 2]) 0459 fprintf('Press any key to continue... \n') 0460 pause 0461 end 0462 else 0463 continue 0464 end 0465 end 0466 end 0467 if ftbverbose 0468 fprintf('Minimum river depth is: %.2f (positive down)\n', min(Mobj.h(Mobj.river_nodes))) 0469 end