Stratigraphic Hex Mesh Tutorial

Written by Guoyan Jiang (gyjiang@whu.edu.cn) with technical support from Dylan Harp (dharp@lanl.gov) and Terry Miller (tamiller@lanl.gov). The associated LaGriT tutorial is here. This example script can be downloaded here.

# -*- coding: utf-8
# This script is the PyLaGriT version of LaGriT tutorial example at
# https://lanl.github.io/LaGriT/pages/tutorial/stratigraphy/index.html.
# Written by Guoyan Jiang (gyjiang@whu.edu.cn) with technical support
# from Dylan Harp (dharp@lanl.gov) and Terry Miller (tamiller@lanl.gov). 

# Import PyLaGriT class from pylagrit module
from pylagrit import PyLaGriT
import numpy

# Variables
maxX  = 4000              # Max value in x direction
maxY  = 4000              # Max value in y direction
maxZ  = 3000              # Max value in z direction
numX = 51                 # Number of points in x direction
numY = 51                 # Number of points in y direction
numZ = 26                 # Number of points in z direction

# Create PyLaGriT object
# This assumes that pylagritrc is being used so that lagrit_exe option does not need to be specified
lg = PyLaGriT()

#********************************************
# 01 Built HEX Mesh
#********************************************
# Create mesh object 
mo = lg.create_hex()
mo.createpts_brick_xyz((numX, numY, numZ), (0,0,0), (maxX, maxY, maxZ))

# Save the mesh object
mo.dump('tmp_hex_01.inp')

# Set vertices (imt) and cells (itetlcr) to 1
mo.setatt('imt', 1)
mo.setatt('itetclr', 1)

# Set node type from connectivity of mesh
mo.resetpts_itp()

#********************************************
# 02 Use pset’s to identify (for setting boundary conditions, 
# initial conditions, etc.) a set of vertices on the top 
# surface of the mesh
#********************************************
# Create a pset named p_top, which contains all nodes (stride = 1 0 0) 
# where the node’s Z value (zic) is greater than or equal to (ge) the top of the mesh (maxZ) 
pset0 = mo.pset_attribute('zic', maxZ, 'ge', (1,0,0), 'p_top')

# Define three cylindrical objects
pset1 = mo.pset_geom((0,0,-1), (1100,360,10000), (1500,1500,0), 'rtz', (1,0,0), 'p_circle1')
pset2 = mo.pset_geom((0,0,-1), (1100,360,10000), (2500,2500,0), 'rtz', (1,0,0), 'p_circle2')
pset3 = mo.pset_geom((0,0,-1), (1100,360,10000), (2500,1500,0), 'rtz', (1,0,0), 'p_circle3')

# Intersect four psets, points belonging to the union of all given sets are preserved into the pset p_region
pset4 = mo.pset_inter([pset0, pset1, pset2, pset3], 'p_region')

# Map psets to an attribute
mo.addatt('id_top_region', vtype='vint', rank='scalar')     # Creat a node-based attribute id_top_region within the mesh object
mo.setatt('id_top_region', 1)              #Fill the entire attribute with 1
pset1.setatt('id_top_region', 2)           #Color all nodes in the pset p_circle1 with the value 2
pset2.setatt('id_top_region', 3)
pset3.setatt('id_top_region', 4)
pset4.setatt('id_top_region', 5)

# Release the psets from memory
pset0.delete()
pset1.delete()
pset2.delete()
pset3.delete()
pset4.delete()

#********************************************
# 03 Build some surfaces to define stratigraphy.
# In a real model, the surfaces would come from some geologic framework model
# and would define geologic or hydro-geologic horizons and topography.
#********************************************
mosurf1 = lg.create_qua()       # Create the top surface
p1 = (-20, -20, 1000)
p2 = (4020, -20, 1500)
p3 = (4020, 4020, 2500)
p4 = (-20, 4020, 500)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf1.quadxy(nnodes, pts)
#mosurf1.paraview()
mosurf1.minmax_xyz()
mosurf1.dump('tmp_surf1_quad.inp')

mosurf2 = lg.create_qua()       # Create the bottom surface
p1 = (-20, -20, 1800)
p2 = (4020, -20, 2100)
p3 = (4020, 4020, 2800)
p4 = (-20, 4020, 800)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf2.quadxy(nnodes, pts)
#mosurf2.paraview()
mosurf2.minmax_xyz()
mosurf2.dump('tmp_surf2_quad.inp')

#********************************************
# 04 Use the surfaces to define regions and set
# vertex and cell ids
#********************************************
# Define Regions
sf1 = mosurf1.surface('sf1')
sf2 = mosurf2.surface('sf2')

r1 = mo.region('le ' + str(sf1))
r2 = mo.region('gt ' + str(sf1) + ' and ' + 'le ' + str(sf2))
r3 = mo.region('gt ' + str(sf2))

mosurf1.delete()
mosurf2.delete()

# Create Eltsets and PSets from Regions
pset1 = mo.pset_region(r1)
pset2 = mo.pset_region(r2)
pset3 = mo.pset_region(r3)

eltset1 = mo.eltset_region(r1)
eltset2 = mo.eltset_region(r2)
eltset3 = mo.eltset_region(r3)

#Set Attributes from Eltsets and PSets
pset1.setatt('imt', 1)
pset2.setatt('imt', 2)
pset3.setatt('imt', 3)

eltset1.setatt('itetclr', 1)
eltset2.setatt('itetclr', 2)
eltset3.setatt('itetclr', 3)

#********************************************
# 05 Build a fault surface and define stratigraphy 
# on each side of the fault
#********************************************
# Create fault surface and surfaces to either side of fault
mosurf1_fminus = lg.create_qua()       
p1 = (-20, -20, 1000)
p2 = (4020, -20, 1500)
p3 = (4020, 4020, 2500)
p4 = (-20, 4020, 500)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf1_fminus .quadxy(nnodes, pts)
#mosurf1_fminus .paraview()
mosurf1_fminus .minmax_xyz()
mosurf1_fminus .dump('tmp_s1_fm.inp')

mosurf2_fminus = lg.create_qua()      
p1 = (-20, -20, 1800)
p2 = (4020, -20, 2100)
p3 = (4020, 4020, 2800)
p4 = (-20, 4020, 800)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf2_fminus.quadxy(nnodes, pts)
#mosurf2_fminus.paraview()
mosurf2_fminus.minmax_xyz()
mosurf2_fminus.dump('tmp_s2_fm.inp')

mosurf1_fplus = lg.create_qua()      
p1 = (-20, -20, 1400)
p2 = (4020, -20, 1900)
p3 = (4020, 4020, 2900)
p4 = (-20, 4020, 900)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf1_fplus.quadxy(nnodes, pts)
#mosurf1_fplus.paraview()
mosurf1_fplus.minmax_xyz()
mosurf1_fplus.dump('mosurf1_fplus.inp')

mosurf2_fplus = lg.create_qua()      
p1 = (-20, -20, 2200)
p2 = (4020, -20, 2500)
p3 = (4020, 4020, 3200)
p4 = (-20, 4020, 1200)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf2_fplus.quadxy(nnodes, pts)
#mosurf2_fplus.paraview()
mosurf2_fplus.minmax_xyz()
mosurf2_fplus.dump('mosurf2_fplus.inp')

mosurf_fault = lg.create_qua()      
p1 = (-20, -20, -1.e4)
p2 = (4020, -20, -1.e4)
p3 = (4020, 4020, 1.e4)
p4 = (-20, 4020, 1.e4)
pts = [p1, p2, p3, p4]
nnodes = (numX, numY, 1)
mosurf_fault.quadxy(nnodes, pts)
#mosurf_fault.paraview()
mosurf_fault.minmax_xyz()
mosurf_fault.dump('mosurf_fault.inp')

# Define geometry of hydrostratigraphic model
sf1_fm = mosurf1_fminus.surface('sf1_fm')
sf2_fm = mosurf2_fminus.surface('sf2_fm')
sf1_fp = mosurf1_fplus.surface('sf1_fp')
sf2_fp = mosurf2_fplus.surface('sf2_fp')
sf_f = mosurf_fault.surface('sf_f')

r1_fm = mo.region('le ' + str(sf1_fm) + ' and ' + 'le ' + str(sf_f))
r2_fm = mo.region('gt ' + str(sf1_fm) + ' and ' + 'le ' + str(sf2_fm) + ' and ' + 'le ' + str(sf_f))
r3_fm = mo.region('gt ' + str(sf2_fm) + ' and ' + 'le ' + str(sf_f))
r1_fp = mo.region('le ' + str(sf1_fp) + ' and ' + 'gt ' + str(sf_f))
r2_fp = mo.region('gt ' + str(sf1_fp) + ' and ' + 'le ' + str(sf2_fp) + ' and ' + 'gt ' + str(sf_f))
r3_fp = mo.region('gt ' + str(sf2_fp) + ' and ' + 'gt ' + str(sf_f))

mosurf1_fminus.delete()
mosurf2_fminus.delete()
mosurf1_fplus.delete()
mosurf2_fplus.delete()
mosurf_fault.delete()

# Set fault node and element materials
pset1_fm = mo.pset_region(r1_fm)
pset2_fm = mo.pset_region(r2_fm)
pset3_fm = mo.pset_region(r3_fm)
pset1_fp = mo.pset_region(r1_fp)
pset2_fp = mo.pset_region(r2_fp)
pset3_fp = mo.pset_region(r3_fp)

eltset1_fm = mo.eltset_region(r1_fm)
eltset2_fm = mo.eltset_region(r2_fm)
eltset3_fm = mo.eltset_region(r3_fm)
eltset1_fp = mo.eltset_region(r1_fp)
eltset2_fp = mo.eltset_region(r2_fp)
eltset3_fp = mo.eltset_region(r3_fp)

#Set Attributes from Eltsets and PSets
mo.setatt('imt', 7)
mo.setatt('itetclr', 7)

pset1_fm.setatt('imt', 1)
pset2_fm.setatt('imt', 2)
pset3_fm.setatt('imt', 3)
pset1_fp.setatt('imt', 4)
pset2_fp.setatt('imt', 5)
pset3_fp.setatt('imt', 6)

eltset1_fm.setatt('itetclr', 1)
eltset2_fm.setatt('itetclr', 2)
eltset3_fm.setatt('itetclr', 3)
eltset1_fp.setatt('itetclr', 4)
eltset2_fp.setatt('itetclr', 5)
eltset3_fp.setatt('itetclr', 6)

#********************************************
# 06 Define a polyline and truncate the exterior boundary of the mesh with the polyline
#********************************************
# Read boundary polygon file
mobndry = lg.read('basin_bnd_ply_rescale.inp')

# Extrude the polyline into a vertical surface
mofence = mobndry.extrude(3200, 'const', 'volume', [0, 0, -1])

mobndry.minmax_xyz()
mofence.minmax_xyz()

# Translate the extrusion to make it cover the vertical extent of the hex mesh
mofence.trans((0, 0, -3100), (0, 0 ,0))

mofence.minmax_xyz()
#mofence.paraview()
#mofence.dump('3D_vertical_surface.inp')
#mo.dump('cube.inp')

# Truncate mesh
sf_bndry = mofence.surface('sf_bndry')
r_bndry = mo.region('ge ' + str(sf_bndry))
pset_bndry = mo.pset_region(r_bndry)

mobndry.delete()
mofence.delete()

# Method 1: Only remove a cell if ALL vertices are outside
e_delete1 = pset_bndry.eltset('exclusive')

# Method 2: Remove a cell if the centroid (average of all vertices) is outside
e_delete2 = mo.eltset_region(r_bndry)

# Method 3: Remove a cell if one or more vertices are outside
e_delete3 = pset_bndry.eltset('inclusive')

#mo.addatt('id_in_out_bndry', vtype='vint', rank='scalar', length='nelements')
mo.add_element_attribute('id_in_out_bndry', vtype='vint')
mo.setatt('id_in_out_bndry', 4)              #Fill the entire attribute with 4
e_delete3.setatt('id_in_out_bndry', 3)          
e_delete2.setatt('id_in_out_bndry', 2)
e_delete1.setatt('id_in_out_bndry', 1)

eltset4 = mo.eltset_attribute('id_in_out_bndry', 4, 'eq')
eltset3 = mo.eltset_attribute('id_in_out_bndry', 3, 'eq')
#eltset2 = mo.eltset_attribute('id_in_out_bndry', 2, 'eq')
#eltset1 = mo.eltset_attribute('id_in_out_bndry', 1, 'eq')

mo.rmpoint_eltset(eltset4, False, False)
mo.rmpoint_eltset(eltset3, True, True)

#********************************************
# 07 Refine the mesh around the fault
#********************************************
f_zone = mo.intersect_elements(sf_f, 'f_zone')
fz_i = mo.eltset_attribute('f_zone', 0, 'gt')     #Non-zero indicates intersection
fz_i.refine()

mo.delatt('f_zone')

mo.status (brief=True)

#sf_f.delete()
#sf1_fm.delete()
#sf2_fm.delete()
#sf1_fp.delete()
#sf1_fp.delete()
#sf_bndry.delete()

#********************************************
# 08 Insert a couple of 'wells' by refining the mesh and identifying a line of nodes
# that will be the well source/sink for boundary conditions.
#********************************************
Well1X = 1234.56
Well1Y = 1987.65
Well2X = 2243.21
Well2Y = 1212.34
Radius = 25
NRadius = 2

#Well 1
mowell1 = lg.create_tet()
mowell1.createpts_rtz((NRadius, 9, numZ), (0, 0, 3100), (Radius, 360, 1500))   #Create a cylindrical point cloud 

mowell1.filter()              # Filter (delete) points that are too close ( default distance <=1.e-16) or duplicate points
mowell1.rmpoint_compress()    # Remove all marked nodes and correct the itet array
mowell1.setatt('imt', 1)

mowell1.connect()             # Connect the point cloud
mowell1.resetpts_itp()
mowell1.minmax_xyz()

mowell1.trans((0, 0, 0), (Well1X, Well1Y, 0))
mowell1.minmax_xyz()
#mowell1.paraview()
mowell1.dump('tmp_well1.inp')

#Well 2
mowell2 = lg.create_tet()
mowell2.createpts_rtz((NRadius, 9, numZ), (0, 0, 3100), (Radius, 360, 2200))   

mowell2.filter()              
mowell2.rmpoint_compress()    
mowell2.setatt('imt', 1)

mowell2.connect()             
mowell2.resetpts_itp()
mowell2.minmax_xyz()

mowell2.trans((0, 0, 0), (Well2X, Well2Y, 0))
mowell2.minmax_xyz()
#mowell2.paraview()
mowell2.dump('tmp_well2.inp')

# Join the two distinct wells into a single mesh object 
mowells = lg.merge([mowell1, mowell2])
mowells.dump('tmp_wells.inp') 
#mowells.paraview()

# Refine the mo around the wells
# First pass refinement
w_zone = mo.intersect_elements(mowells, 'w_zone')
wz_i = mo.eltset_attribute('w_zone', 0, 'gt')     #Non-zero indicates intersection
wz_i.refine()
mo.setatt('w_zone', 0)
#wz_i.delete()

# Second pass refinement
w_zone = mo.intersect_elements(mowells, 'w_zone')
wz_i = mo.eltset_attribute('w_zone', 0, 'gt')     #Non-zero indicates intersection
wz_i.refine()
mo.setatt('w_zone', 0)
#wz_i.delete()

mohex = mo.grid2grid_tree_to_fe()                         #Quadtree or octree grid to grid

#mo.status (brief=True)

# Identify the column of vertices closest to the well center.
#Well1
mo_pts1 = lg.create()
mo_pts1.createpts_rtz((2, 2, 1000), (0, 0, 3100), (Radius, 360, 2200))   
mo_pts1.trans((0, 0, 0), (Well1X, Well1Y, 0))

#Well2
mo_pts2 = lg.create()
mo_pts2.createpts_rtz((2, 2, 1000), (0, 0, 3100), (Radius, 360, 2200))   
mo_pts2.trans((0, 0, 0), (Well2X, Well2Y, 0))

mo_pts = lg.merge([mo_pts1, mo_pts2])
mo_pts.filter()              
mo_pts.rmpoint_compress()   

# Compute a distance field attribute
mo.compute_distance(mo_pts, option='distance_field', attname='dfield_well')

mo_pts1.delete()
mo_pts2.delete()
mo_pts.delete()
mowell1.delete()
mowell2.delete()
mowells.delete()

# Describe all nodes within 32, 16, 8, 4, 2 and 1 meters of the wells.
pwell = mo.pset_attribute('dfield_well', 1.0, 'le', (1,0,0), 'pwell1')
pwell.dump('zone_radius_01.0.zone')

pwell = mo.pset_attribute('dfield_well', 2.0, 'le', (1,0,0), 'pwell2')
pwell.dump('zone_radius_02.0.zone')

pwell = mo.pset_attribute('dfield_well', 4.0, 'le', (1,0,0), 'pwell4')
pwell.dump('zone_radius_04.0.zone')

pwell = mo.pset_attribute('dfield_well', 8.0, 'le', (1,0,0), 'pwell8')
pwell.dump('zone_radius_08.0.zone')

pwell = mo.pset_attribute('dfield_well', 16.0, 'le', (1,0,0), 'pwell16')
pwell.dump('zone_radius_16.0.zone')

pwell = mo.pset_attribute('dfield_well', 32.0, 'le', (1,0,0), 'pwell32')
pwell.dump('zone_radius_32.0.zone')

mo.dump('Hex_mesh.inp') 

#********************************************
# 09 Convert hex mesh to tet mesh
#********************************************
motet = mohex.copypts()
motet.setatt('imt', 1)
motet.setatt('itp', 0)
motet.connect(option1='check_interface')
motet.resetpts_itp()

motet.interpolate_voronoi('imt', mohex, 'imt')
motet.interpolate_map('itetclr', mohex, 'itetclr')

#Remove all nodes and elements with imt and itetclr values of 7
motet.rmmat(7)
#pset7 = motet.pset_attribute('imt', 7, 'eq', (1,0,0), 'pset7')
#motet.rmpoint_pset(pset7)
#eltset7 = motet.eltset_attribute('itetclr', 7, 'eq')
#motet.rmpoint_eltset(eltset7, True, True)

motet.rmpoint_compress() 
motet.resetpts_itp()

# Visualize connected mesh using ParaView
# This assumes that pylagritrc is being used so that exe option does not need to be specified
#motet.paraview()

motet.dump('Tet_mesh.inp')

#********************************************
# 10 Write tet mesh files for FEHM
# FEHM uses node based materials and properties
#********************************************
#motet.resetpts_parent()
motet.filter() 
motet.rmpoint_compress() 
motet.resetpts_itp()
motet.minmax('imt')
motet.setatt('itetclr', 1)
#motet. tri_mesh_output_prep()
motet.dump_fehm('Example3')



The final result:

images/stratigraphy.png