CVHM2 element single proc - giorgk/ichnos GitHub Wiki

CVHM2

Model description

The Central Valley Hydrologic Model 2 (CVHM2) is an update to CVHM which is a modflow based integrated surface - groundwater model that simulates groundwater flow, stream aquifer interaction, conjuctive water use and more etc.

Here we use the flow output information to built a particle tracking simulation using Ichnos.

Domain outline

For the domain outline we read the discretization shapefile (CVHM2_DIS.shp) which can be downloaded from the CVHM2 repository. In a GIS software we convert the projection system to EPSG:3310, keep only the active cells and join them to form an outline which we can read it as follows:

cvhm_outline = shaperead('path/to/cvhm_outline_3310.shp');

We split the shapefile into polygons and print them to a file

[Xs, Ys] = polysplit(cvhm_outline.X, cvhm_outline.Y);
fid = fopen('cvhm2_domain.ich','w');
for ii = 1:length(Xs)
    fprintf(fid, '%d %d\n',[length(Xs{ii,1}) ispolycw(Xs{ii,1},Ys{ii,1})]);
    fprintf(fid, '%.3f %.3f\n', [Xs{ii,1}' Ys{ii,1}']'); 
end
fclose(fid);

Convert Grid to Mesh

The CVHM2 is defined on a grid that is rotated about 30 degrees. Therefore we have to convert the grid into an equivalent mesh. The discretization gis layer DIS shapefile is in a different projection system than 3310 therefore we first converted it to the EPSG:3310. The following code reads the converted shapefile and keeps only the active cells

dis_shp = shaperead('D:/BoxFolder/Box/CVHMII/gis_data/CVHM2_DIS_3310.shp');
idx = find([dis_shp.active]' == 1);
dis_shp_act = dis_shp(idx,:);

To convert the grid into a mesh we need to identify the unique locations of the nodes of the grid assign ids and use those ids to form the elements. The following lengthy snippet does that and creates 2 variables the mesh nodes msh_nd and mesh elements msh_el

msh_nd = [];
msh_el = [];
for ii = 1:length(dis_shp_act)
    % Here we know that the elements are all quadrilateral so we will loop
    % from 1 to 4
    el = []; % This will hold the element ids if the ii element
    for j = 1:4
        nd = [dis_shp_act(ii,1).X(j) dis_shp_act(ii,1).Y(j)];
        if isempty(msh_nd)
            msh_nd = [msh_nd;nd];
            el = [el 1];
        else
            % calculate the distance with the nodes already in the list
            dst = sqrt(sum((nd - msh_nd).^2, 2));
            idx = find(dst < 1);
            if isempty(idx)
                % This is a new node and we append it in the list
                msh_nd = [msh_nd;nd];
                el = [el size(msh_nd,1)];
            else
                el = [el idx];
            end
        end
    end
    % last we append the indices of this element
    msh_el = [msh_el;el];
end

In Matlab its not easy to plot a quadrilateral mesh but we can plot the mesh nodes only.

plot(msh_nd(:,1),msh_nd(:,2),'.')
Streamlines around well

The mesh elements must be oriented in a counter clockwise manner. The following code checks the orientation of the elements and reverse the indices order if needed.

for ii = 1:size(msh_el,1)
    if ispolycw(msh_nd(msh_el(ii,:),1),msh_nd(msh_el(ii,:),2))
        msh_el(ii,:) = msh_el(ii,[1 4 3 2]);
    end
end

Top and Bottom

Typicaly the elevation data in CVHM2 are defined in the DIS file. The DIS file does not contain projection information therefore we will use the provided DIS shapefile which also contain the elevation information as well as the top and bottom active layer in each cell. The following snippet extracts cell centroids the elevations and the active layer flag

top_lay_act = [dis_shp.up_act]';
bot_lay_act = [dis_shp.low_act]';
Lay_XY = zeros(length(dis_shp),2);
for ii = 1:length(dis_shp)
    Lay_XY(ii,1) = mean(dis_shp(ii).X(1:4));
    Lay_XY(ii,2) = mean(dis_shp(ii).Y(1:4));
end
Lay_elev = zeros(length(dis_shp),14);
for ii = 1:13
    Lay_elev(:,ii) = [dis_shp.(['lay' num2str(ii) 't_m'])]';
end
Lay_elev(:,14) = [dis_shp.lay13b_m]';

Next we will gather the top and bottom elevation into a separate matrix from the appropriate layer. The first line identifies the cells that have valid layer information, which are all the active layers, and the next two lines extract the top and bottom layer values.

id_lay = find(top_lay_act ~=0 & bot_lay_act ~= 0);
top_lay_elev = Lay_elev(sub2ind(size(Lay_elev), id_lay, top_lay_act(id_lay)));
bot_lay_elev = Lay_elev(sub2ind(size(Lay_elev), id_lay, bot_lay_act(id_lay)));

Then we can create interpolants and interpolate the cell center elevations to the mesh nodes.

Ftop = scatteredInterpolant(Lay_XY(id_lay,1),Lay_XY(id_lay,2),top_lay_elev,'linear','nearest');
Fbot = scatteredInterpolant(Lay_XY(id_lay,1),Lay_XY(id_lay,2),bot_lay_elev,'linear','nearest');
msh_top = Ftop(msh_nd(:,1),msh_nd(:,2));
msh_bot = Fbot(msh_nd(:,1),msh_nd(:,2));

For the top and bottom elevation we will use the MESH2D option. We generated previously the mesh nodes and mesh elements. Here we append the top and bottom elevation and print into one file as the top and bottom are sharing the same nodes.

fid = fopen('cvhm2_top_bot.ich','w');
fprintf(fid,'MESH2D\n');
fprintf(fid, '%d %d %.2f\n',[size(msh_nd,1) size(msh_el,1) 2500]);
fprintf(fid, '%.3f %.3f %.3f %.3f\n', [msh_nd msh_top msh_bot]');
fprintf(fid, '%d %d %d %d\n',msh_el');
fclose(fid);

The radius parameter was set 2500 because we know that all elements are rectangles with 1 mile side. Therefore within a 2.5km radius from any point within the domain there will always be at least one element within the search distance.

Velocity Data

In this example will use the MESH2D support structure with ELEMENT interpolation. To this end we need to prepare three files that define the mesh.

Node file

We will use the same mesh as in the top and bottom elevations

fid = fopen("cvhm2_nodes.ich",'w');
fprintf(fid, '%.3f %.3f\n', msh_nd');
fclose(fid);

Mesh file

Similarly we already have the mesh data available from previous snippets

fid = fopen("cvhm2_elements.ich",'w');
fprintf(fid, '%d %d %d %d 0\n', msh_el');
fclose(fid);

Elevation file

While in modflow files the elevation is defined in the DIS file, the discretization shapefile that we have used previously already contains the layer elevation at the cell centers. In a similar way we can interpolate to the mesh nodes

msh_elev = zeros(size(msh_nd,1),14);
for ii = 1:size(Lay_elev,2)
    Felev = scatteredInterpolant(Lay_XY(:,1),Lay_XY(:,2), Lay_elev(:,ii),'linear','nearest');
    msh_elev(:,ii) = Felev(msh_nd(:,1),msh_nd(:,2));
end

It's always a good idea to check if there is any layer crossing. This can happend during interpolation when the layers are very thin. A quick way to check for errors is the following

any(diff(msh_elev,1,2)>0)

This should be all false.

Now we can print the mesh elevation file

fid = fopen("cvhm2_elevations.ich",'w');
fprintf(fid, [repmat('%.3f ',1,13) '%.3f\n'], msh_elev');
fclose(fid);

Velocity

Next we will print the velocity. In Modflow the velocity is printed as the volumetric flow that cross the grid cells. However to keep this document short we will convert the volumetric flows to velocity by dividing with the cross area and assign the velocity to the center of the cell grid and we will use the ELEMENT interpolation.

The Modflow velocity is printed into the binary Cell by Cell output file. For CVHM2 the *.cbc is about 10 GB and can be generated after the model is run or it can be downloaded from the CVHM repository (Output.zip). To bring the cbc data into Matlab we have prepared a script that reads the entire CBC and then we saved each component of the CBC flow into a separate *.mat file so that we can easily bring the data into matlab. The following code reads the entire cbc and saves the FLOW velocity terms (Note that the first line is going to take a few hours for the CVHM2)

CBC = readCVHMcbc('path\to\cbcf.out');
% Save each flow term
for ii = 1:length(CBC)
    FLOWRIGHTFACE{ii,1} = CBC{ii,1}{3,2};
end
save('path\to\FLOWRIGHTFACE.mat','FLOWRIGHTFACE','-v7.3')

for ii = 1:length(CBC)
    FLOWFRONTFACE{ii,1} = CBC{ii,1}{4,2};
end
save('path\to\FLOWFRONTFACE.mat','FLOWFRONTFACE','-v7.3')

for ii = 1:length(CBC)
    FLOWLOWERFACE{ii,1} = CBC{ii,1}{5,2};
end
save('path\to\FLOWLOWERFACE.mat','FLOWLOWERFACE','-v7.3')

Once we have those mat files we can retrieve the velocity information very quickly as:

load('path\to\FLOWFRONTFACE.mat')
load('path\to\FLOWRIGHTFACE.mat')
load('path\to\FLOWLOWERFACE.mat')

First let's reshape the velocity data so that the rows correspond to grid cells and the columns to time steps

% extract auxiliary variables
RC_act = [[dis_shp_act.ROW]' [dis_shp_act.COLUMN_]'];
RC_ind = sub2ind([441 98], [dis_shp_act.ROW]', [dis_shp_act.COLUMN_]');
% Initialize the variables
for j = 1:13
    QX{j,1} = zeros(size(RC_act,1),length(FLOWRIGHTFACE));
    QY{j,1} = zeros(size(RC_act,1),length(FLOWRIGHTFACE));
    QZ{j,1} = zeros(size(RC_act,1),length(FLOWRIGHTFACE));
end

for ii = 1:length(FLOWRIGHTFACE)
    for j = 1:13
        tmpx = FLOWRIGHTFACE{ii,1}(:,:,j);
        QX{j,1}(:,ii) = tmpx(RC_ind);
        tmpy = FLOWFRONTFACE{ii,1}(:,:,j);
        QY{j,1}(:,ii) = tmpy(RC_ind);
        tmpz = FLOWFRONTFACE{ii,1}(:,:,j);
        QZ{j,1}(:,ii) = tmpz(RC_ind);
    end
end

The units of QX QY QZ are m^3/day. To convert them to m/day we calculate the thickness of each layer.

layThick = zeros(length(RC_ind),13);
for ii = 1:13
    if ii == 13
        layThick(:,ii) = [dis_shp_act.lay13t_m]' - [dis_shp_act.lay13b_m]';
    else
        layThick(:,ii) = [dis_shp_act.(['lay' num2str(ii) 't_m'])]' - [dis_shp_act.(['lay' num2str(ii+1) 't_m'])]';
    end
end

Now we can calculate the velocity by dividing with the face area. The For the VX, VY the area is 1 mile times the thickness and for the vertical velocity the face area is 1 square mile

for ii = 1:13
    VX{ii,1} = QX{ii,1}./(layThick(:,ii)*1609.34);
    VY{ii,1} = QY{ii,1}./(layThick(:,ii)*1609.34);
    VZ{ii,1} = QZ{ii,1}./(1609.34*1609.34);
end

The velocities that we just computed have direction perpendicular to cell face. However the cells are rotated with respect the horizontal axis.

Rotate velocity sketch

The VX VY (black) components of velocity results in the velocity vector Vxy (orange). With Ichnos we have to calculate the components Vx` and VY` (green)

First we calculate the grid rotation. The first two lines below identify the left and right lower cells of the Modflow grid. Then we create a vector v1 from the second node of the left lower cell to the first node of the right lower cell and calculate the anlge between this vector and the horizontal axis v2. The calculated rotation is about 30.0652.

id1 = find([dis_shp.ROW]' == 441 & [dis_shp.COLUMN_]' == 1);
id2 = find([dis_shp.ROW]' == 441 & [dis_shp.COLUMN_]' == 98);
v1 = [dis_shp(id2,1).X(1) - dis_shp(id1,1).X(2) dis_shp(id2,1).Y(1) - dis_shp(id1,1).Y(2)];
v2 = [100000 0];
rot = acosd(sum(v1.*v2)/(sqrt(sum(v1.^2))*sqrt(sum(v2.^2))));

Now that we know the rotation angle we can calculate the unit vectors of the rotated grid projected on the cartesian axis. bx by are the cartesian unit vectors and bxrot byrot are the unit vectors of the rotated system

bx = [1 0];
by = [0 1];
bxrot(1) = bx(1)*cosd(rot) - bx(2)*sind(rot);
bxrot(2) = bx(1)*sind(rot) + bx(2)*cosd(rot);

byrot(1) = by(1)*cosd(rot) - by(2)*sind(rot);
byrot(2) = by(1)*sind(rot) + by(2)*cosd(rot);

We can plot those vectors:

figure()
clf
plot([0 bx(1)], [0 bx(2)],'b')
hold on
plot([0 bxrot(1)], [0 bxrot(2)],'r')

plot([0 by(1)], [0 by(2)],'b')
plot([0 byrot(1)], [0 byrot(2)],'r')
axis equal
grid on
Rotate velocity sketch

To understand the calculations needed for the rotation we will pick the velocity of one cell e.g. 10000 and layer 9

id = 10000;
lid = 9;
vx = VX{lid,1}(id,1)
vy = VY{lid,1}(id,1)
vz = VZ{lid,1}(id,1)

The values vx=-0.0252, vy=-0.0104, vz=-3.758e-4 show the magnitude of the velocity aligned with the rotated grid. To plot them we need to express them using the unit rotated vectors where vx_m vy_m are the velocity vectors with magnitude and direction defined on the rotated gridded system and a is the resultant vector.

vx_m = bxrot*vx;
vy_m = byrot*vy;
a = vx_m + vy_m;

Last we project the resultant vector a onto the unit cartesian vectors to calculate the x,y vecolity components to pass to ichnos

a1 = bx*dot(a,bx)/dot(bx,bx);
a2 = by*dot(a,by)/dot(by,by);

Let's plot the above vectors:

figure()
clf
plot([0 vx_m(1)], [0 vx_m(2)],'r')
hold on
plot([0 vy_m(1)], [0 vy_m(2)],'r')
plot([0 a(1)], [0 a(2)],'--r')
plot([0 a1(1)],[0 a1(2)],'g')
plot([0 a2(1)],[0 a2(2)],'g')
grid on
axis equal
Rotate velocity sketch

The red lines correspond to the Modflow velocities defined as perpendicular to the cell faces. The green vectors are defined so that their resultant velocity vector has same direction and magnitute as the original Modflow velocity.

Next we are going to repeat the above calculations in vectorized manner for all cells.

clear VXproj VYproj VZproj
for ii = 1:13
    vx_m1 = bxrot(1)*VX{ii,1};
    vx_m2 = bxrot(2)*VX{ii,1};
    vy_m1 = byrot(1)*VY{ii,1};
    vy_m2 = byrot(2)*VY{ii,1};

    VXproj{ii,1} = vx_m1 + vy_m1;
    VYproj{ii,1} = vx_m2 + vy_m2;
    VZproj{ii,1} = -VZ{ii,1};
end

For the vertical velocity we reverse the direction because in Modflow positive is the flow between a layer and the layer below

For this demonstration we will average the velocities of the last decade and print them as steady state.

VAVxyz = [];
for ii = 1:13
    VAVxyz = [VAVxyz; ...
              [mean(VXproj{ii,1}(:,end-119:end),2) ...
               mean(VYproj{ii,1}(:,end-119:end),2) ...
               mean(VZproj{ii,1}(:,end-119:end),2)]];
end
  • Print steady state as ASCII
fid = fopen('path\to\cvhm2_velSS_000.vel','w');
fprintf(fid,'%.5e %.5e %.5e\n',VAVxyz');
fclose(fid);
  • Print steady state as HDF5
fname = fullfile('G:\UCDAVIS\IchnosPaper\CVHM2\cvhm2_velSS_000.h5');
h5create(fname,'/VXYZ',[size(VAVxyz,1) 3], 'Datatype','single');
h5write(fname, '/VXYZ', VAVxyz);

For completness we provide the code snippets to prepare and print the transient state velocity which is very similar with the difference being that each component is printed into a different file or dataset.

VXtrs = [];
VYtrs = [];
VZtrs = [];
for ii = 1:13
    VXtrs = [VXtrs;VXproj{ii,1}];
    VYtrs = [VYtrs;VXproj{ii,1}];
    VZtrs = [VZtrs;VXproj{ii,1}];
end
  • Print transient state ASCII
fid = fopen('G:\UCDAVIS\IchnosPaper\CVHM2\cvhm2_velTR_VX_000.vel','w');
fprintf(fid,[repmat('%.5e ',1,701) '%.5e\n'], VXtrs');
fclose(fid);

fid = fopen('G:\UCDAVIS\IchnosPaper\CVHM2\cvhm2_velTR_VY_000.vel','w');
fprintf(fid,[repmat('%.5e ',1,701) '%.5e\n'], VYtrs');
fclose(fid);

fid = fopen('G:\UCDAVIS\IchnosPaper\CVHM2\cvhm2_velTR_VZ_000.vel','w');
fprintf(fid,[repmat('%.5e ',1,701) '%.5e\n'], VZtrs');
fclose(fid);
  • Print transient state HDF5
fnameTR = fullfile('G:\UCDAVIS\IchnosPaper\CVHM2\cvhm2_velTR_000.h5');
h5create(fnameTR,'/VX',[size(VXtrs,1) size(VXtrs,2)], 'Datatype','single');
h5write(fnameTR, '/VX', VXtrs);
h5create(fnameTR,'/VY',[size(VYtrs,1) size(VYtrs,2)], 'Datatype','single');
h5write(fnameTR, '/VY', VYtrs);
h5create(fnameTR,'/VZ',[size(VZtrs,1) size(VZtrs,2)], 'Datatype','single');
h5write(fnameTR, '/VZ', VZtrs);

Main configuration options

In the following we list the main configuration options

[Velocity]
XYZType = MESH2D
Type = DETRM
ConfigFile = path\to\velocityConfig.ini
[Domain]
Outline = path\to\cvhm2_domain.ich
TopFile = path\to\cvhm2_top_bot.ich
BottomFile = 
ProcessorPolys =
[StepConfig]
Method = RK4
Direction = 1
StepSize = 50
StepSizeTime = 100000
nSteps = 5
nStepsTime = 0
minExitStepSize = 1
MaxIterationsPerStreamline = 3000
MaxProcessorExchanges = 50
AgeLimit = 100
Stuckiter = 10
AttractFile = 
AttractRadius = 30
[InputOutput]
PartilceFile = path\to\cvhm2_particles.ich
WellFile = 
OutputFile = path\to\out_prefix_
PrintH5 = 0
PrintASCII = 1
ParticlesInParallel = 5000
GatherOneFile = 0
[Other]
Nrealizations = 1
nThreads = 1
RunAsThread = 0
Version = 0.5.06
OutFreq = 1

Make sure that the version number is the same as the output of the ichnos.exe -v

Velocity configuration options

Here we list the relevant options for hte steady state simulation

[Velocity]
Prefix = path\to\cvhm2_velSS_
LeadingZeros = 3
Suffix = .vel
Type = DETRM
[MESH2D]
NodeFile = path\to\cvhm2_nodes.ich
Meshfile = path\to\cvhm2_elements.ich
ElevationFile = path\to\cvhm2_elevations.ich
FaceIdFile = 
Nlayers = 13
INTERP = ELEMENT
[Porosity]
Value = 0.1

Run Simulation

Generate particles

To run particle tracking we need initial positions for particles. Here we will generate 1000 particles scattered randomly in Central Valley while their starting point will be 5 m below the top boundary.

cv_pp = polyshape(cvhm_outline.X, cvhm_outline.Y);
cv_pp_buff = cv_pp.polybuffer(-5000);
particles = zeros(1000,3);
for ii = 1:size(particles,1)
    while 1
        xrnd = cvhm_outline.BoundingBox(1,1) + (cvhm_outline.BoundingBox(2,1)-cvhm_outline.BoundingBox(1,1))*rand;
        yrnd = cvhm_outline.BoundingBox(1,2) + (cvhm_outline.BoundingBox(2,2)-cvhm_outline.BoundingBox(1,2))*rand;
        if cv_pp_buff.isinterior(xrnd,yrnd)
            particles(ii,1:2) = [xrnd yrnd];
            break;
        end
    end
end
particles(:,3) = Ftop(particles(:,1), particles(:,2))-5;

Print the particle file

fid = fopen('cvhm2_particles.ich','w');
fprintf(fid, '%d %d %.3f %.3f %.3f 0\n',[ones(1000,1) [1:1000]' particles]');
fclose(fid);

Run the simulation

To run the simulation make sure that all input files are in the same directory or the correct locations are specified. Note that you can use relative locations with respect the directory that is returned after running pwd on powershell or terminal. The simulation can be run as

> path\to\ichnos.exe -c cvhm2_MESH2D_ELEM_SS_config.ini

Post process

The output file can be ASCII or HDF5. Within Matlab both can be read with the following command

S = readICHNOStraj('path\to\cvhm2_MESH2D_SS_ELEM_iter_0000.traj');

To visualize the streamlines we can do the following

clf
plot(cv_pp)
hold on
for ii = 1:1000
    plot(S(ii,1).p(:,1), S(ii,1).p(:,2),'b')
end
axis equal off
CVHM2 streamlines example
⚠️ **GitHub.com Fallback** ⚠️