In this repository we provide information and the data for the estimation of the unsaturated travel time in Central Valley.
In addition to the methodology we provide matlab code snippets. Those snippets use a few matlab functions that are part of the gwtools package.
The travel time $\tau$ assuming vertical water movement can be estimated by the following equation:
$$\tau = \thetam \frac{D{gw}}{R}$$
where $\thetam$ is the deep vadose zone water content or mobile water content $[L^3/L^3]$, $D{gw}$ is the depth from soil surface to the water table $[L]$ and $R$ is the groundwater recharge $[L/T]$.
The mobile water content is a dimensionless coefficient that can be used to scale the overall travel time.
In the following paragraph we describe the approach to estimate the Depth and groundwater recharge.
For the groundwater recharge estimation we rely on groundwater modeling. In Central Valley there are 2 important regional models the Central Valley Hydrologic model (CVHM) and the California Central Valley Groundwater-Surface Simulation Model (C2VSim). Here we use the C2VSim model however the same analysis can be done with CVHM.
Set the C2VSim path. c2vsim_path
is the folders where the Simulation
and Results
folders are.
c2vsim_path = fullfile('path','to','c2vsim');
Read the groundwater recharge data for each element from the groundwater element zone budget file. See this post for detailed explanation of reading process
GBinfo = h5info(fullfile(c2vsim_path, "Results",'C2VSimFG_GW_ZBudget.hdf'));
colIDnames = h5read(GBinfo.Filename,...
[GBinfo.Groups(1).Name GBinfo.Name GBinfo.Groups(1).Datasets(5).Name]);
colIDs = h5read(GBinfo.Filename,...
[GBinfo.Groups(1).Name GBinfo.Name GBinfo.Groups(1).Datasets(6).Name]);
In C2VSim the groundwater recharge is divided into three components. See more about groundwater recharge in C2VSim here and the associated documentation
DeepPerc = h5read(GBinfo.Filename,...
[GBinfo.Groups(2).Name GBinfo.Name GBinfo.Groups(2).Datasets(5).Name])
DivLoss = zeros(size(DeepPerc));
tmp = h5read(GBinfo.Filename,...
[GBinfo.Groups(2).Name GBinfo.Name GBinfo.Groups(2).Datasets(7).Name]);
divElIds = find(colIDs(:,17) ~= 0);
DivLoss(divElIds,:) = tmp;
ByPassLoss = zeros(size(DeepPerc));
tmp = h5read(GBinfo.Filename,...
[GBinfo.Groups(2).Name GBinfo.Name GBinfo.Groups(2).Datasets(1).Name]);
bypassElIds = find(colIDs(:,19) ~= 0);
ByPassLoss(bypassElIds,:) = tmp;
Next we add the 3 recharge volumes
c2vsimRch = DeepPerc + DivLoss + ByPassLoss;
To calculate $R$ we will use two representative recharge rates that correspond to the average rates of spring 2000 and spring 2015. Next we find the indices for these periods
idx_2000 = find(c2vsimTime == datetime('31-Mar-2000')):find(c2vsimTime == datetime('31-May-2000'));
idx_2015 = find(c2vsimTime == datetime('31-Mar-2015')):find(c2vsimTime == datetime('31-May-2015'));
The units of recharge in the hdf output files are in cuft/month
. Here we calculate the total amount of spring recharge and divide it by the numbers of spring days and convert it to meter so the units now are m^3/day
.
RchVol_2000 = (0.3048^3)*sum(c2vsimRch(:,idx_2000),2)./sum(c2vsimTime(idx_2000).Day);
RchVol_2015 = (0.3048^3)*sum(c2vsimRch(:,idx_2015),2)./sum(c2vsimTime(idx_2015).Day);
Finaly to convert the recharge volume to rate we need the area of each element, which we could read from the groundwater zone budget output file
ElemArea = h5read(GBinfo.Filename,...
[GBinfo.Groups(1).Name GBinfo.Name GBinfo.Groups(1).Datasets(12).Name]);
ElemArea = ElemArea*(0.3048^2);
However because we will need the element barycenters at a later step we will calculate both the areas and barycenters using the element shapefile
ElemArea = zeros(length(c2vsim_mesh),1);
bc_elem = zeros(length(c2vsim_mesh),2);
for ii = 1:length(c2vsim_mesh)
ElemArea(ii,1) = polyarea(c2vsim_mesh(ii,1).X(1:end-1), c2vsim_mesh(ii,1).Y(1:end-1));
bc_elem(ii,:) = [mean(c2vsim_mesh(ii,1).X(1:end-2)) mean(c2vsim_mesh(ii,1).Y(1:end-2))];
end
Now we can calculate the recharge rates. Here convert them to mm/year
Rch_2000 = 1000*365*RchVol_2000./ElemArea;
Rch_2015 = 1000*365*RchVol_2015./ElemArea;
The histogram of groundwater recharge for the two time periods is show below
For the depth to water table we rely on water level measurments. The data we use in the following can be found under our cv unsat data folder. These are processed data. The original data were obtained by DWR.
gwl_data1 = readtable('gwl_file_part_1.xlsx');
gwl_data2 = readtable('gwl_file_part_2.xlsx');
gwl_data3 = readtable('gwl_file_part_3.xlsx');
Join the three tables but keep only the data we need
columns_to_keep = ["Var2","Var4","Var5","Var6","Var7","Var8"];
gwl_data = [gwl_data1(:,columns_to_keep)
gwl_data2(:,columns_to_keep)
gwl_data3(:,columns_to_keep)];
gwl_data.Properties.VariableNames = {'Section', 'Date','Var5','Var6','Var7','Var8'};
gwl_data.Section = categorical(gwl_data.Section);
Keep the data of spring 2000 and 2015
gwl_data = gwl_data((gwl_data.Date >= datetime(2000,2,1) & gwl_data.Date <= datetime(2000,5,31)) | ...
(gwl_data.Date >= datetime(2015,2,1) & gwl_data.Date <= datetime(2015,5,31)),:);
From those fields calculate the depth to groundwater
gwl_data.DGW = gwl_data.Var8 - (gwl_data.Var5 - gwl_data.Var6);
gwl_data(:,["Var5","Var6","Var7","Var8"]) = [];
gwl_data(isnan(gwl_data.DGW),:) = [];
Read the spreadsheet with the coordinate information
gst = readtable(fullfile('..','..','Box','cv-unsat','gst_file.xlsx'));
gst.SITE_CODE = categorical(gst.SITE_CODE);
Append coordinates to the groundwater level data table
[Lia, Locb] = ismember(gwl_data.Section, gst.SITE_CODE);
gwl_data.Lat(Lia) = gst.LATITUDE(Locb(Lia));
gwl_data.Lon(Lia) = gst.LONGITUDE(Locb(Lia));
gwl_data = gwl_data(Lia,:);
For each section it is possible to have multiple well records. Here we isolate a list of unique sections
trs_unique = unique(gwl_data.Section);
GWL = table(trs_unique,'VariableNames', {'Section'});
Loop through the wells and calculate the mean depth for 2000 and 2015
for ii = 1:size(GWL,1)
ind = find(gwl_data.Section == GWL.Section(ii));
if ~isempty(ind)
GWL.Lat(ii) = gwl_data.Lat(ind(1));
GWL.Lon(ii) = gwl_data.Lon(ind(1));
% find records for spring 2000
iyr = year(gwl_data.Date(ind)) == 2000;
GWL.DGW_2000(ii) = mean(gwl_data.DGW(ind(iyr)));
% find records for spring 2015
iyr = year(gwl_data.Date(ind)) == 2015;
GWL.DGW_2015(ii) = mean(gwl_data.DGW(ind(iyr)));
end
end
Isolate the records that are within the Central Valley. Read the Central Valley shapefile
CV_outline = shaperead(fullfile('path','to','gis_data','C2VSim_Outline_3310'));
Plot all record data
plot(CV_outline.X, CV_outline.Y)
[xx,yy] = projfwd(projcrs(3310),GWL.Lat, -GWL.Lon);
hold on
plot(xx,yy,'.')
title('All records')
hold off
Remove the wells outside Central Valley
CV_outline_shape = polyshape(CV_outline.X, CV_outline.Y);
in_cv = CV_outline_shape.isinterior(xx,yy);
GWL(~in_cv,:) = [];
Compare the well records between 2000 and 2015 years
subplot(1,2,1);
plot(-GWL.Lon(~isnan(GWL.DGW_2000)), GWL.Lat(~isnan(GWL.DGW_2000)),'.')
title({'Records with Spring',['2000 DGW (' num2str(sum(~isnan(GWL.DGW_2000))) ')']})
axis equal
axis off
subplot(1,2,2);
plot(-GWL.Lon(~isnan(GWL.DGW_2015)), GWL.Lat(~isnan(GWL.DGW_2015)),'.')
title({'Records with Spring',['2015 DGW (' num2str(sum(~isnan(GWL.DGW_2015))) ')']})
axis equal
axis off
Because the measured data have significant gaps we will use the simulated data which cover the CV however they contain errors. The goal here is to adjust the errors based on the water level measurment data
First read the simulated data. (This is going to take sometime)
C2VsimHead = readIWFM_headalloutput(fullfile(c2vsim_path,'Results','C2VSimFG_GW_HeadAll.out'), 30179, 4, 505, 1);
Calculate the simulated average water table for spring 2000 and 2015. The data are in feet therefore we convert the water table elevation in meters.
sim_wtbl_2000 = 0.3048 * (C2VsimHead{319,2}(:,1) + C2VsimHead{320,2}(:,1) + C2VsimHead{321,2}(:,1))/3;
sim_wtbl_2015 = 0.3048 * (C2VsimHead{499,2}(:,1) + C2VsimHead{500,2}(:,1) + C2VsimHead{501,2}(:,1))/3;
To calculate the depth to water we read the C2VSim groundwater surface elevation and convert it to meters
cv_nodes = readIWFM_Nodes(fullfile(c2vsim_path, 'Preprocessor','C2VSimFG_Nodes.dat'));
cv_gse = readIWFM_Stratigraphy(fullfile(c2vsim_path,'Preprocessor','C2VSimFG_Stratigraphy.dat'),30179, 4, 105);
cv_gse = 0.3048 * cv_gse(:,2);
The simulated depth to groundwater can now be calculated as:
sim_dgw_2000 = cv_gse - sim_wtbl_2000;
sim_dgw_2015 = cv_gse - sim_wtbl_2015;
The simulated and measured data have to be under the same coordinate system. In the following snippet we convert the measurment data from lat long to 3310 and the simulated data from 26910 to 3310
[GWL.X_3310, GWL.Y_3310] = projfwd(projcrs(3310),GWL.Lat, -GWL.Lon);
GWL.DGW_2000 = GWL.DGW_2000*0.3048;
GWL.DGW_2015 = GWL.DGW_2015*0.3048;
[lat,lon] = projinv(projcrs(26910), [c2vsim_nodes.X]', [c2vsim_nodes.Y]');
[simX3310, simY3310] = projfwd(projcrs(3310),lat, lon);
To make the conditioning process easier we will create interpolants for the measured and simulated data
Fmeas2000 = scatteredInterpolant(GWL.X_3310(~isnan(GWL.DGW_2000)), ...
GWL.Y_3310(~isnan(GWL.DGW_2000)), GWL.DGW_2000(~isnan(GWL.DGW_2000)), 'linear', 'nearest');
Fmeas2015 = scatteredInterpolant(GWL.X_3310(~isnan(GWL.DGW_2015)), ...
GWL.Y_3310(~isnan(GWL.DGW_2015)), GWL.DGW_2015(~isnan(GWL.DGW_2015)), 'linear', 'nearest');
Fsim2000 = scatteredInterpolant(simX3310, simY3310, sim_dgw_2000, 'linear', 'nearest');
Fsim2015 = scatteredInterpolant(simX3310, simY3310, sim_dgw_2015, 'linear', 'nearest');
DGW2000sim = Fsim2000(Fmeas2000.Points(:,1), Fmeas2000.Points(:,2));
DGW2015sim = Fsim2015(Fmeas2015.Points(:,1), Fmeas2015.Points(:,2));
Fmeas2000sim = scatteredInterpolant(Fmeas2000.Points(:,1), Fmeas2000.Points(:,2), DGW2000sim, 'linear','nearest');
Fmeas2015sim = scatteredInterpolant(Fmeas2015.Points(:,1), Fmeas2015.Points(:,2), DGW2015sim, 'linear','nearest');
SimMeas2000 = Fmeas2000sim(simX3310, simY3310);
SimMeas2015 = Fmeas2015sim(simX3310, simY3310);
measError2000 = Fsim2000.Values - SimMeas2000;
measError2015 = Fsim2015.Values - SimMeas2015;
MeasInterp2000 = Fmeas2000(simX3310, simY3310);
MeasInterp2015 = Fmeas2015(simX3310, simY3310);
DGW2000 = MeasInterp2000 + measError2000;
DGW2015 = MeasInterp2015 + measError2015;
In the following snippets we prepare a triangulation structure that we use is to visualize the data
% Triangulate the c2vsim nodes
DT = delaunayTriangulation(simX3310,simY3310);
% Calculate the barycenters of the triangles of the triangulation
bc_tr = zeros(size(DT.ConnectivityList,1),2);
for ii = 1:3
bc_tr = bc_tr + [DT.Points(DT.ConnectivityList(:,ii),1) DT.Points(DT.ConnectivityList(:,ii),2)];
end
bc_tr = bc_tr./3;
% Remove the triangles outside the Central Valley
in_cv = CV_outline_shape.isinterior(bc_tr(:,1), bc_tr(:,2));
tr = DT.ConnectivityList(in_cv,:);
Prepare a few data structures to help with ploting
tmp2000 = sign(DGW2000).*log10(abs(DGW2000));
tmp2015 = sign(DGW2015).*log10(abs(DGW2015));
cmin = min(min(tmp2000),min(tmp2015));
cmax = max(max(tmp2000),max(tmp2015));
color_res = 512; %Color resolution lcol = linspace(cmin,cmax,color_res)'; id = find(lcol > 0,1)-1; custom_map = [linspace(130, 255, id)' linspace(0, 255, id)' linspace(0, 255, id)'; ... linspace(255, 0,color_res-id)' linspace(255,0,color_res-id)' linspace(255, 130,color_res-id)'];
Plot the depth to groundwater for Spring 2000 and 2015
figure() clf subplot(1,2,1) trisurf(tr, simX3310, simY3310, tmp2000,'edgecolor','none'); clim([cmin cmax]); colormap(custom_map./255); view(0,90) axis equal axis off title('Depth to water table - 2000') h = colorbar; h.Label.String = 'm'; h.TickLabels = cellfun(@num2str, num2cell(10.^h.Ticks),'UniformOutput',false);
subplot(1,2,2) trisurf(tr, simX3310, simY3310, tmp2015,'edgecolor','none'); clim([cmin cmax]); colormap(custom_map./255); view(0,90) axis equal axis off title('Depth to water table - 2015') h = colorbar; h.Label.String = 'm'; h.TickLabels = cellfun(@num2str, num2cell(10.^h.Ticks),'UniformOutput',false);
<img src="https://github.com/UCD-GW-Nitrate/cv-unsat/raw/main/DGWspringMaps.png" width="70%">
## Calculate Travel time
In the previous sections we calculated a map of the depth to groundwater and a map of recharge.
To calculate the travel time we need to set up $\tau = 0.05$ value.
However due to simulation errors the $D_{gw}$ and $R$ maps contain values that are not reasonable. For example the depth map contains areas with negative depth. While the water table is very close to groundwater surface in those areas it should not be negative. To handle these situations we set two thresholds.
theta = 0.05; rch_threshold = 10; %mm/year depth_threshold = 1; %m;
The recharge threshold sets a limit to the minimum recharge rate. Here if the simulated recharge rate is lower than the 10 mm/year then is set equal to 10 mm/year. We also set a minimum depth equal to 1 m.
The depth to groundwater has been calculated at the nodes of the C2VSim mesh, while the recharge was calculated on the mesh elements. To make the two data consistent we will assign the recharge volumes to the element nodes. First we will extract a few data to assist in the process such as the element node ids and the element node area fractions
ElemNodeAreas = h5read(GBinfo.Filename, [GBinfo.Groups(1).Name GBinfo.Name GBinfo.Groups(1).Datasets(16).Name])'; ElemNodeFractions = ElemNodeAreas./sum(ElemNodeAreas,2); ElemNodes = h5read(GBinfo.Filename, [GBinfo.Groups(1).Name GBinfo.Name GBinfo.Groups(1).Datasets(17).Name])'; NodeIds = unique(ElemNodes); NodeIds(NodeIds == 0,:) = [];
Then we will loop through each node and calculate the recharge that correspond to that node
Rch_nodesVol_2000 = nan(length(NodeIds),1); Rch_nodesVol_2015 = nan(length(NodeIds),1); area_nodes = nan(length(NodeIds),1); for ii = 1:length(NodeIds) [II, JJ] = find(ElemNodes == NodeIds(ii)); irchVol2000 = 0; irchVol2015 = 0; iarea = 0; for j = 1:length(II) irchVol2000 = irchVol2000 + RchVol_2000(II(j))ElemNodeFractions(II(j), JJ(j)); irchVol2015 = irchVol2015 + RchVol_2015(II(j))ElemNodeFractions(II(j), JJ(j)); iarea = iarea + ElemArea(II(j))ElemNodeFractions(II(j), JJ(j)); end Rch_nodesVol_2000(NodeIds(ii),1) = irchVol2000; Rch_nodesVol_2015(NodeIds(ii),1) = irchVol2015; area_nodes(NodeIds(ii),1) = iarea; end Rch_nodes_2000 = 1000365Rch_nodesVol_2000./area_nodes; Rch_nodes_2015 = 1000365*Rch_nodesVol_2015./area_nodes;
Finally we can calculate the unsaturated travel time
tau_2000 = theta. max(DGW2000, depth_threshold)./ (max(rch_threshold, Rch_nodes_2000)/1000); tau_2015 = theta. max(DGW2015, depth_threshold)./ (max(rch_threshold, Rch_nodes_2015)/1000);
The empirical cumulative distribution of unsaturated travel time is shown in the folowing plot
<img src="https://github.com/UCD-GW-Nitrate/cv-unsat/raw/main/ECDFtravelTime.png" width="50%">
Last we map the travel time
cmin = min(min(log10(tau_2000)),min(log10(tau_2015))); cmax = max(max(log10(tau_2000)),max(log10(tau_2015))); figure() clf subplot(1,2,1) trisurf(tr, simX3310, simY3310, log10(tau_2000),'edgecolor','none'); clim([cmin cmax]); view(0,90) axis equal axis off title('Travel time Spring 2000') colormap parula h = colorbar; h.Label.String = 'Years'; h.TickLabels = cellfun(@num2str, num2cell(10.^h.Ticks),'UniformOutput',false);
subplot(1,2,2) trisurf(tr, simX3310, simY3310, log10(tau_2015),'edgecolor','none'); clim([cmin cmax]); view(0,90) axis equal axis off title('Travel time Spring 2015') colormap parula h = colorbar; h.Label.String = 'Years'; h.TickLabels = cellfun(@num2str, num2cell(10.^h.Ticks),'UniformOutput',false);
<img src="https://github.com/UCD-GW-Nitrate/cv-unsat/raw/main/TravelTimeMaps.png" width="70%">