├── .gitignore ├── README.md ├── discretization ├── FEM.py ├── FVML.py ├── FVMO.py ├── TPFA.py ├── __init__.py ├── mesh.py └── operators.py ├── elliptic_convergence.py ├── poisson.py ├── richards_constant_hydraulic_conductivity.py ├── richards_non_linear.py └── utils ├── differentiation.py └── flux_error.py /.gitignore: -------------------------------------------------------------------------------- 1 | .pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # masterthesis 2 | This repo contains discretization methods for the [elliptic PDE](https://en.wikipedia.org/wiki/Elliptic_partial_differential_equation) 3 | 4 | , 5 | 6 | in two spatial dimensions that handle general quadrilateral grids. In particular, the MPFA-O and MPFA-L methods are implemented, they yield locally mass conservative discretizations that are consistent for rough grids. The MPFA-L method has particulary good monotonicity properties, i.e., the [maximum principle](https://en.wikipedia.org/wiki/Maximum_principle) is respected for a wide range of grids. It's therefore considered as the state of the art method for porous media flow problems on quadrilatereal grids. 7 | 8 | The code requires numpy for assembling discretization matrices, for running the convergence tests one also needs scipy and sympy. 9 | # Quick tutorial # 10 | The way to define a domain and discretize it into control volumes, is to discretize the unit square with rectangles, then perturb the grid points. This approach is very flexible and alows for complicated domains and control volume discretizations. 11 | 12 | ```python 13 | from discretization.mesh import Mesh 14 | import numpy as np 15 | 16 | nx = ny = 6 #Number of grid points in x and y direction on the unit square 17 | perturbation = lambda p:np.array([p[0],0.5*p[0]+p[1]]) #perturbation on every grid point p 18 | mesh = Mesh(nx,ny,perturbation,ghostboundary=True) #Creates a mesh object with a strip of ghostcells for boundary handling 19 | mesh.plot() 20 | ``` 21 | This would result in the parallelogram discretization, note that we have 8 grid points (in orange) in each direction, 2 more than 6, as we have a strip of ghost cells. 22 | 23 | ![Figure_2_small](https://user-images.githubusercontent.com/49365904/145256307-a9b73542-e4ff-4c44-b6ff-0c6f63c6d8c3.png) 24 | 25 | For solving the [Poisson equation ](https://en.wikipedia.org/wiki/Poisson%27s_equation) on this, one would define the problem data with numpy arrays and python functions, then pass it to the compute_matrix and compute_vector functions, together with the mesh object. In this example we have a homogeneous domain (permeability is a matrix of ones), and a isotropic medium (tensor is diagonal). 26 | ```python 27 | from discretization.FVML import compute_matrix,compute_vector 28 | import math 29 | 30 | source = lambda x , y : math.sin(y)*math.cos(x) 31 | boundary_condition = lambda x , y :0 32 | tensor = np.eye(2) 33 | permeability = np.ones(( mesh.num_unknowns,mesh.num_unknowns)) 34 | 35 | A = np.zeros((mesh.num_unknowns,mesh.num_unknowns))# stiffness matrix 36 | f = np.zeros(mesh . num_unknowns)# load vector 37 | 38 | compute_matrix(mesh,A,tensor,permeability) 39 | compute_vector(mesh,f,source,boundary_condition) 40 | 41 | u = np.linalg.solve(A,f) 42 | mesh.plot_vector(u) 43 | ``` 44 | This would result in the solution 45 | 46 | ![Figure_2_solution](https://user-images.githubusercontent.com/49365904/145258136-fcb74827-fa27-41f0-96aa-4711d4ca38c4.png) 47 | 48 | 49 | For more interesting equations, such as the time dependent, non-linear [Richards' equation](https://en.wikipedia.org/wiki/Richards_equation), see [this file](richards_non_linear.py). 50 | -------------------------------------------------------------------------------- /discretization/FEM.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | import sympy as sym 4 | def compute_matrix(mesh,matrix,K,k_global = None): 5 | """Assembles MPFA-L stiffness matrix. 6 | 7 | Parameters 8 | ---------- 9 | mesh : Mesh 10 | A mesh object with nodes, cellcenters, normal vectors, midpoints etc. 11 | 12 | matrix : NxN matrix handle. 13 | Could be numpy or scipy square matrix with number of rows, N equal to the degrees of freedom. 14 | K : 2x2 numpy array 15 | The permeability tensor. This is constant across the domain. 16 | k_global : N dimensional numpy vector. 17 | This specifies the scalar permeability at each point. Should be layd out in fortran ordering. 18 | """ 19 | elements = mesh.elements.astype(int) 20 | coordinates = np.reshape(mesh.cell_centers,(mesh.num_unknowns,2),order='C') 21 | boundary_elements_dirichlet = mesh.boundary_elements 22 | if k_global is None: 23 | k_global = np.ones((mesh.cell_centers.shape[0],mesh.cell_centers.shape[1])) 24 | k_global = np.ravel(k_global) 25 | shape_grad = np.array([np.matrix([[1],[0]]),np.matrix([[0],[1]]),np.matrix([[-1],[-1]])]) 26 | 27 | def local_to_reference_map(ele_num): 28 | mat_coords = np.array([[coordinates[elements[ele_num,0]][0],coordinates[elements[ele_num,0]][1],1],[coordinates[elements[ele_num,1]][0],coordinates[elements[ele_num,1]][1],1],[coordinates[elements[ele_num,2]][0],coordinates[elements[ele_num,2]][1],1]]) 29 | b1 = np.array([[1],[0],[0]]) 30 | b2 = np.array([[0],[1],[0]]) 31 | a1 = np.linalg.solve(mat_coords,b1) 32 | a2 = np.linalg.solve(mat_coords,b2) 33 | J = np.matrix([[a1[0][0],a1[1][0]],[a2[0][0],a2[1][0]]]) 34 | c = np.matrix([[a1[2][0]],[a2[2][0]]]) 35 | return [J,c] 36 | def reference_to_local_map(ele_num): 37 | mat_coords = np.array([[1,0,1],[0,1,1],[0,0,1]]) 38 | b1 = np.array([[coordinates[elements[ele_num][0]][0]],[coordinates[elements[ele_num][1]][0]],[coordinates[elements[ele_num][2]][0]]]) 39 | b2 = np.array([[coordinates[elements[ele_num][0]][1]],[coordinates[elements[ele_num][1]][1]],[coordinates[elements[ele_num][2]][1]]]) 40 | a1 = np.linalg.solve(mat_coords,b1) 41 | a2 = np.linalg.solve(mat_coords,b2) 42 | J = np.matrix([[a1[0][0],a1[1][0]],[a2[0][0],a2[1][0]]]) 43 | c = np.matrix([[a1[2][0]],[a2[2][0]]]) 44 | return [J,c] 45 | 46 | # Matrix assembly 47 | for e in range(len(elements)): 48 | # extract element information 49 | [J,c] = local_to_reference_map(e) 50 | [M,d] = reference_to_local_map(e) 51 | transform = J.dot(J.transpose()) #J*J^t; derivative transformation 52 | jac = np.linalg.det(J) #Determinant of tranformation matrix = inverse of area of local elements 53 | #Local assembler 54 | for j in range(3): 55 | for i in range(3): 56 | x_r = M@np.array([[1/3],[1/3]])+d 57 | K_int = k_global[elements[e][2]]*(1-x_r[0]-x_r[1])+k_global[elements[e][0]]*x_r[0]+k_global[elements[e][1]]*x_r[1] 58 | # K_int = (k_global[elements[e][2]]+k_global[elements[e][0]]+k_global[elements[e][1]])/3 59 | # K_int = 0.5*k_global[elements[e][j]] 60 | matrix[elements[e][i],elements[e][j]] += K_int*0.5*shape_grad[i].transpose().dot(K@transform.dot(shape_grad[j]))/jac 61 | for e in range(len(boundary_elements_dirichlet)): 62 | matrix[boundary_elements_dirichlet[e][0],:]=0 63 | matrix[boundary_elements_dirichlet[e][0],boundary_elements_dirichlet[e][0]]=1 64 | matrix[boundary_elements_dirichlet[e][1],:]=0 65 | matrix[boundary_elements_dirichlet[e][1],boundary_elements_dirichlet[e][1]]=1 66 | return matrix 67 | 68 | def compute_vector(mesh,vector,f,boundary): 69 | x = sym.Symbol('x') 70 | y = sym.Symbol('y') 71 | #s = sym.Symbol('s') 72 | 73 | elements = mesh.elements.astype(int) 74 | coordinates = np.reshape(mesh.cell_centers,(mesh.num_unknowns,2),order='C') 75 | shape_func = np.array([x,y,1-y-x]) 76 | #shape_func_1d = np.array([0.5-0.5*x,0.5+0.5*x]) 77 | 78 | boundary_elements_dirichlet = mesh.boundary_elements 79 | 80 | def local_to_reference_map(ele_num): 81 | mat_coords = np.array([[coordinates[elements[ele_num][0]][0],coordinates[elements[ele_num][0]][1],1],[coordinates[elements[ele_num][1]][0],coordinates[elements[ele_num][1]][1],1],[coordinates[elements[ele_num][2]][0],coordinates[elements[ele_num][2]][1],1]]) 82 | b1 = np.array([[1],[0],[0]]) 83 | b2 = np.array([[0],[1],[0]]) 84 | a1 = np.linalg.solve(mat_coords,b1) 85 | a2 = np.linalg.solve(mat_coords,b2) 86 | J = np.matrix([[a1[0][0],a1[1][0]],[a2[0][0],a2[1][0]]]) 87 | c = np.matrix([[a1[2][0]],[a2[2][0]]]) 88 | return [J,c] 89 | 90 | def reference_to_local_map(ele_num): 91 | mat_coords = np.array([[1,0,1],[0,1,1],[0,0,1]]) 92 | b1 = np.array([[coordinates[elements[ele_num][0]][0]],[coordinates[elements[ele_num][1]][0]],[coordinates[elements[ele_num][2]][0]]]) 93 | b2 = np.array([[coordinates[elements[ele_num][0]][1]],[coordinates[elements[ele_num][1]][1]],[coordinates[elements[ele_num][2]][1]]]) 94 | a1 = np.linalg.solve(mat_coords,b1) 95 | a2 = np.linalg.solve(mat_coords,b2) 96 | J = np.matrix([[a1[0][0],a1[1][0]],[a2[0][0],a2[1][0]]]) 97 | c = np.matrix([[a1[2][0]],[a2[2][0]]]) 98 | return [J,c] 99 | 100 | def quad_2d_2nd_order_shape(ele_num, f, loc_node): 101 | [J,c] = reference_to_local_map(ele_num) 102 | x_1 = J.dot(np.array([[1/6],[1/6]]))+c 103 | x_2 = J.dot(np.array([[2/3],[1/6]]))+c 104 | x_3 = J.dot(np.array([[1/6],[2/3]]))+c 105 | return (1/6)*(f(x_1[0][0],x_1[1][0])*shape_func[loc_node].subs([(x,1/6),(y,1/6)])+f(x_2[0][0],x_2[1][0])*shape_func[loc_node].subs([(x,2/3),(y,1/6)])+f(x_3[0][0],x_3[1][0])*shape_func[loc_node].subs([(x,1/6),(y,2/3)])) 106 | # #Parametrizes straight boundary segment to [-1,1] 107 | # def param_1d_ele(ele_num): 108 | # return np.array([[coordinates[boundary_elements_neumann[ele_num][0]][0]+(coordinates[boundary_elements_neumann[ele_num][1]][0]-coordinates[boundary_elements_neumann[ele_num][0]][0])*0.5*(s+1)],[coordinates[boundary_elements_neumann[ele_num][0]][1]+(coordinates[boundary_elements_neumann[ele_num][1]][1]-coordinates[boundary_elements_neumann[ele_num][0]][1])*0.5*(s+1)]]) 109 | 110 | # #Calculates length of 1d interval 111 | # def param_1d_ele_derivative(ele_num): 112 | # #return math.sqrt((coordinates[boundary_elements_neumann[ele_num][0]][0]-coordinates[boundary_elements_neumann[ele_num][0]][1])^2+(coordinates[boundary_elements_neumann[ele_num][1]][0]-coordinates[boundary_elements_neumann[ele_num][1]][1])^2) 113 | # return 0.5*math.sqrt((coordinates[boundary_elements_neumann[ele_num][0]][0]-coordinates[boundary_elements_neumann[ele_num][1]][0])**2+(coordinates[boundary_elements_neumann[ele_num][0]][1]-coordinates[boundary_elements_neumann[ele_num][1]][1])**2) 114 | # # Second order quadrature on boundary line integral 115 | # def quad_2nd_ord_line(f,ele_num,loc_node): 116 | # r = param_1d_ele(ele_num) 117 | # dr = param_1d_ele_derivative(ele_num) 118 | # x_1 = r[0][0].subs(s,-1/math.sqrt(3)) 119 | # x_2 = r[0][0].subs(s,1/math.sqrt(3)) 120 | # y_1 = r[1][0].subs(s,-1/math.sqrt(3)) 121 | # y_2 = r[1][0].subs(s,1/math.sqrt(3)) 122 | # return (f(x_1, y_1)*shape_func_1d[loc_node].subs(x,-1/math.sqrt(3))+f(x_2,y_2)*shape_func_1d[loc_node].subs(x,1/math.sqrt(3)))*dr 123 | for e in range(len(elements)): 124 | # extract element information 125 | [J,c] = local_to_reference_map(e) 126 | jac = np.linalg.det(J) #Determinant of tranformation matrix = inverse of area of local elements 127 | #Local assembler 128 | for j in range(3): 129 | vector[elements[e][j]] = float(vector[elements[e][j]]) + quad_2d_2nd_order_shape(e,f,j)/jac 130 | for e in range(len(boundary_elements_dirichlet)): 131 | vector[boundary_elements_dirichlet[e][0]]=boundary(coordinates[boundary_elements_dirichlet[e][0]][0], coordinates[boundary_elements_dirichlet[e][0]][1]) 132 | vector[boundary_elements_dirichlet[e][1]]=boundary(coordinates[boundary_elements_dirichlet[e][1]][0], coordinates[boundary_elements_dirichlet[e][1]][1]) 133 | return vector 134 | 135 | -------------------------------------------------------------------------------- /discretization/FVML.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def compute_matrix(mesh, matrix,K,k_global=None,flux_matrix = None): 4 | """Assembles MPFA-L stiffness matrix. 5 | 6 | Parameters 7 | ---------- 8 | mesh : Mesh 9 | A mesh object with nodes, cellcenters, normal vectors, midpoints etc. 10 | 11 | matrix : NxN matrix handle. 12 | Could be numpy or scipy square matrix with number of rows, N equal to the degrees of freedom. 13 | K : 2x2 numpy array 14 | The permeability tensor. This is constant across the domain. 15 | k_global : N dimensional numpy vector. 16 | This specifies the scalar permeability at each point. Should be layd out in fortran ordering. 17 | flux_matrix : two NxN matrix handles 18 | This is to recover the flux. 19 | 20 | """ 21 | nodes = mesh.nodes 22 | cell_centers = mesh.cell_centers 23 | if k_global is None: 24 | k_global = np.ones((cell_centers.shape[0],cell_centers.shape[1])) 25 | 26 | nx = nodes.shape[1] 27 | ny = nodes.shape[0] 28 | 29 | num_unknowns = cell_centers.shape[1]*cell_centers.shape[0] 30 | 31 | meshToVec = mesh.meshToVec 32 | if flux_matrix is not None: 33 | flux_matrix_x = flux_matrix['x'] 34 | flux_matrix_y = flux_matrix['y'] 35 | 36 | R = np.array([[0,1],[-1,0]]) 37 | 38 | T = np.zeros((4,2,3)) 39 | omega = np.zeros((2,3,7)) 40 | V = np.zeros((7,2)) 41 | 42 | interface = np.zeros((4,2)) 43 | centers = np.zeros((4,2)) 44 | n = np.zeros((4,2)) 45 | k_loc = np.zeros((4)) 46 | 47 | def local_assembler(j,i,vec,start, matrix_handle,index): 48 | global_vec = np.zeros(num_unknowns) 49 | 50 | indexes = [meshToVec(j-1,i-1),meshToVec(j-1,i),meshToVec(j,i),meshToVec(j,i-1)] 51 | 52 | 53 | for ii,jj in zip(range(start,start+2),range(2)): 54 | 55 | matrix_handle[index,indexes[ii%4]] += vec[jj] 56 | 57 | matrix_handle[index,indexes[(start-1)%4]] += vec[2] 58 | return global_vec 59 | 60 | def compute_triangle_normals(start_index, interface, centers, node_midpoint,V): 61 | V[0,:] = R@(interface[(start_index-1)%4,:]-centers[start_index%4]) 62 | V[1,:] = -R@(interface[start_index%4,:]-centers[start_index%4]) 63 | V[2,:] = R@(interface[start_index%4,:]-centers[(start_index+1)%4]) 64 | V[3,:] = -R@(nodes[i,j]-centers[(start_index+1)%4]) 65 | V[4,:] = R@(nodes[i,j]-centers[(start_index-1)%4]) 66 | V[5,:] = -R@(interface[(start_index-1)%4,:]-centers[(start_index-1)%4]) 67 | V[6,:] = R@(nodes[i,j]-centers[start_index%4]) 68 | 69 | 70 | # @jit(fastmath=True) #halfes the running time for matrix assembly, but no tensor permability 71 | def compute_omega(n,K,V,t,omega,center,k_loc_rel): 72 | for ii in range(2): 73 | for jj in range(3): 74 | for kk in range(7): 75 | if ii == 0: 76 | omega[ii,jj,kk] = n[center,:].T.dot(V[kk,:]*1/t[jj])*k_loc_rel[jj] 77 | else: 78 | omega[ii,jj,kk] = n[center-1,:].T.dot(V[kk,:]*1/t[jj])*k_loc_rel[jj] 79 | 80 | def compute_T(center,k_loc): 81 | compute_triangle_normals(center,interface,centers,nodes[i,j],V) 82 | t = np.array([V[0,:].T@R@V[1,:],V[2,:].T@R@V[3,:],V[4,:].T@R@V[5,:]]) 83 | k_loc_rel = np.array([k_loc[center],k_loc[(center+1)%4],k_loc[(center-1)%4]]) 84 | compute_omega(n,K,V,t,omega,center,k_loc_rel) 85 | 86 | xi_1 = (V[6,:].T@R@V[0,:])/(V[0,:].T@R@V[1,:]) 87 | xi_2 = (V[6,:].T@R@V[1,:])/(V[0,:].T@R@V[1,:]) 88 | 89 | C = np.array([[-omega[0,0,0], -omega[0,0,1]], 90 | [-omega[1,0,0], -omega[1,0,1]]]) 91 | 92 | D = np.array([[omega[0,0,0]+omega[0,0,1], 0, 0], 93 | [omega[1,0,0]+omega[1,0,1], 0, 0]]) 94 | 95 | A = np.array([[omega[0,0,0]-omega[0,1,3]-omega[0,1,2]*xi_1, omega[0,0,1]-omega[0,1,2]*xi_2], 96 | [omega[1,0,0]-omega[1,2,5]*xi_1, omega[1,0,1]-omega[1,2,4]-omega[1,2,5]*xi_2]]) 97 | 98 | B = np.array([[omega[0,0,0]+omega[0,0,1]+omega[0,1,2]*(1-xi_1-xi_2), -omega[0,1,2]-omega[0,1,3], 0], 99 | [omega[1,0,0]+omega[1,0,1]+omega[1,2,5]*(1-xi_1-xi_2), 0, -omega[1,2,4]-omega[1,2,5]]]) 100 | 101 | T = C@np.linalg.inv(A)@B+D 102 | return T 103 | 104 | def choose_triangle(T,i): 105 | if abs(T[i,0,0])cell_centers.shape[1],range(self.num_unknowns)): 130 | elements[e,:] = np.array([int(i),int(i+cell_centers.shape[1]+1),int(i+cell_centers.shape[1])],dtype=int) 131 | e = e + 1 132 | elements[e,:] = np.array([int(i),int(i+1),int(i+cell_centers.shape[1]+1)],dtype=int) 133 | e = e + 1 134 | boundary_elements = ['n','n','n','n'] 135 | for point, boundary_point,index in zip(points,boundary,range(points.shape[0])): 136 | if boundary_point != 0: 137 | boundary_loc = int(boundary_point-1) 138 | if type(boundary_elements[boundary_loc])==str: 139 | boundary_elements[boundary_loc] = int(index) 140 | elif isinstance(boundary_elements[boundary_loc],int): 141 | boundary_elements[boundary_loc] = np.array([[boundary_elements[boundary_loc],index]]) 142 | else: 143 | boundary_elements[boundary_loc] = np.concatenate((boundary_elements[boundary_loc],np.array([[boundary_elements[boundary_loc][boundary_elements[boundary_loc].shape[0]-1,1],index]]))) 144 | bottom = boundary_elements[0] 145 | right = boundary_elements[1] 146 | top = boundary_elements[2] 147 | left = boundary_elements[3] 148 | bottom = np.concatenate((np.array([[left[0,0],bottom[0,0]]]),bottom,np.array([[bottom[bottom.shape[0]-1,1],right[0,0]]]))) 149 | top= np.concatenate((np.array([[left[left.shape[0]-1,1],top[0,0]]]),top,np.array([[top[top.shape[0]-1,1],right[right.shape[0]-1,1]]]))) 150 | boundary_elements = np.concatenate((bottom,right,top,left)) 151 | return (elements,boundary_elements) 152 | T = Delaunay(points) 153 | boundary_elements = ['n','n','n','n'] 154 | for point, boundary_point,index in zip(points,boundary,range(points.shape[0])): 155 | if boundary_point != 0: 156 | boundary_loc = int(boundary_point-1) 157 | if type(boundary_elements[boundary_loc])==str: 158 | boundary_elements[boundary_loc] = int(index) 159 | elif isinstance(boundary_elements[boundary_loc],int): 160 | boundary_elements[boundary_loc] = np.array([[boundary_elements[boundary_loc],index]]) 161 | else: 162 | boundary_elements[boundary_loc] = np.concatenate((boundary_elements[boundary_loc],np.array([[boundary_elements[boundary_loc][boundary_elements[boundary_loc].shape[0]-1,1],index]]))) 163 | bottom = boundary_elements[0] 164 | right = boundary_elements[1] 165 | top = boundary_elements[2] 166 | left = boundary_elements[3] 167 | bottom = np.concatenate((np.array([[left[0,0],bottom[0,0]]]),bottom,np.array([[bottom[bottom.shape[0]-1,1],right[0,0]]]))) 168 | top= np.concatenate((np.array([[left[left.shape[0]-1,1],top[0,0]]]),top,np.array([[top[top.shape[0]-1,1],right[right.shape[0]-1,1]]]))) 169 | boundary_elements = np.concatenate((bottom,right,top,left)) 170 | boundary_set = set(boundary_elements.flatten()) 171 | elements = T.simplices 172 | remove_list = [] 173 | for i in range(elements.shape[0]): 174 | if elements[i,0] in boundary_set and elements[i,1] in boundary_set and elements[i,2] in boundary_set: 175 | remove_list.append(i) 176 | elements = np.delete(elements,remove_list,0) 177 | return (elements,boundary_elements) 178 | 179 | def max_h(self): 180 | m = 0 181 | for i in range(self.nodes.shape[0]-1): 182 | for j in range(self.nodes.shape[1]-1): 183 | up = np.linalg.norm(self.nodes[i+1,j,:]-self.nodes[i,j,:]) 184 | right = np.linalg.norm(self.nodes[i,j+1,:]-self.nodes[i,j,:]) 185 | diag1 = np.linalg.norm(self.nodes[i+1,j+1,:]-self.nodes[i,j,:]) 186 | diag2 = np.linalg.norm(self.nodes[i+1,j,:]-self.nodes[i,j+1,:]) 187 | m = max((m,up,right,diag1,diag2)) 188 | return m 189 | 190 | 191 | def plot(self): 192 | plt.scatter(self.cell_centers[:,:,0],self.cell_centers[:,:,1]) 193 | plt.scatter(self.nodes[:,:,0], self.nodes[:,:,1]) 194 | # plt.scatter(self.BC_nodes[:,:,0],self.BC_nodes[:,:,1]) 195 | # plt.scatter(self.BC_midpoints[:,:,0],self.BC_midpoints[:,:,1]) 196 | segs1 = np.stack((self.nodes[:,:,0],self.nodes[:,:,1]), axis=2) 197 | segs2 = segs1.transpose(1,0,2) 198 | plt.gca().add_collection(LineCollection(segs1)) 199 | plt.gca().add_collection(LineCollection(segs2)) 200 | # plt.quiver(*self.midpoints[1,1,0,:],self.normals[1,1,0,0],self.normals[1,1,0,1]) 201 | # plt.quiver(*self.midpoints[1,1,1,:],self.normals[1,1,1,0],self.normals[1,1,1,1]) 202 | #plt.savefig('perturbed_grid_aspect_0.2_mesh.pdf') 203 | # points = np.reshape(self.cell_centers,(self.cell_centers.shape[0]*self.cell_centers.shape[1],2)) 204 | # plt.triplot(points[:,0],points[:,1],self.elements) 205 | 206 | plt.show() 207 | 208 | def meshToVec(self,j:int,i:int)->int: 209 | return i*self.cell_centers.shape[0] + j 210 | 211 | def vecToMesh(self,h:int)->Tuple[int,int]: 212 | return (h % self.cell_centers.shape[0], math.floor(h/self.cell_centers.shape[0])) 213 | 214 | def plot_vector(self,vec,text = 'text'): 215 | vec_center = np.zeros((self.cell_centers.shape[0],self.cell_centers.shape[1])) 216 | num_unknowns = self.cell_centers.shape[1]*self.cell_centers.shape[0] 217 | for i in range(num_unknowns): 218 | vec_center[self.vecToMesh(i)] = vec[i] 219 | fig = plt.figure(figsize=plt.figaspect(0.5)) 220 | plt.contourf(self.cell_centers[:,:,0],self.cell_centers[:,:,1],vec_center,20,) 221 | plt.colorbar() 222 | plt.show() 223 | def plot_interaction(self): 224 | #plot nodes 225 | plt.scatter(self.nodes[:,:,0], self.nodes[:,:,1]) 226 | segs1 = np.stack((self.nodes[:,:,0],self.nodes[:,:,1]), axis=2) 227 | segs2 = segs1.transpose(1,0,2) 228 | plt.gca().add_collection(LineCollection(segs1)) 229 | plt.gca().add_collection(LineCollection(segs2)) 230 | 231 | centers = self.cell_centers 232 | points = np.zeros((2*centers.shape[0]-1,centers.shape[1],2)) 233 | for i in range(centers.shape[1]-1): 234 | points[2*i,:,:] = centers[i,:,:] 235 | points[2*i+1,:,:] = self.midpoints[i,:,0,:] 236 | segs1 = np.stack((points[:,:,0],points[:,:,1]),axis=2) 237 | segs2 = segs1.transpose(1,0,2) 238 | plt.gca().add_collection(LineCollection(segs2,color='g',linestyle='dashed')) 239 | plt.show() 240 | 241 | 242 | 243 | def plot_funtion(self,fun,text = 'text'): 244 | vec_center = np.zeros((self.cell_centers.shape[0],self.cell_centers.shape[1])) 245 | num_unknowns = self.cell_centers.shape[1]*self.cell_centers.shape[0] 246 | for i in range(num_unknowns): 247 | xx,yy = self.cell_centers[self.vecToMesh(i)] 248 | vec_center[self.vecToMesh(i)] = fun(xx,yy) 249 | fig = plt.figure(figsize=plt.figaspect(0.5)) 250 | plt.contourf(self.cell_centers[:,:,0],self.cell_centers[:,:,1],vec_center,20,) 251 | plt.colorbar() 252 | # fig.suptitle(text) 253 | 254 | plt.show() 255 | 256 | def interpolate(self,fun): 257 | u = np.zeros(self.num_unknowns) 258 | for i in range(self.cell_centers.shape[0]): 259 | for j in range(self.cell_centers.shape[1]): 260 | x = self.cell_centers[i,j,0] 261 | y = self.cell_centers[i,j,1] 262 | u[self.meshToVec(i,j)] = fun(x,y) 263 | return u 264 | 265 | def compute_error(self,u,u_exact): 266 | u_exact_vec = u.copy() 267 | volumes = u.copy() 268 | for i in range(self.cell_centers.shape[0]): 269 | for j in range(self.cell_centers.shape[1]): 270 | u_exact_vec[self.meshToVec(i,j)] = u_exact(self.cell_centers[i,j,0],self.cell_centers[i,j,1]) 271 | volumes[self.meshToVec(i,j)] = self.volumes[i,j] 272 | L2_error = math.sqrt(np.square(u-u_exact_vec).T@volumes/(np.ones(volumes.shape)@volumes)) 273 | max_error = np.max(np.abs(u-u_exact_vec)) 274 | return(L2_error,max_error) 275 | 276 | if __name__=="__main__": 277 | mesh = Mesh(5,5,lambda p:np.array(p[1]**2+[p[0],p[1]])) 278 | print(mesh.BC_nodes) 279 | mesh.plot() 280 | 281 | -------------------------------------------------------------------------------- /discretization/operators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.sparse import csr_matrix,lil_matrix, diags 3 | from scipy.sparse.linalg import spsolve 4 | 5 | def mass_matrix(mesh, sparse = False): 6 | """ 7 | Constructs diagonal mass matrix 8 | """ 9 | if sparse: 10 | mass = diags(np.ravel(mesh.volumes,order='F'),format='lil') 11 | else: 12 | mass = np.diag(np.ravel(mesh.volumes,order='F')) 13 | for i in range(mesh.cell_centers.shape[0]): 14 | for j in range(mesh.cell_centers.shape[1]): 15 | if (i==0) or (i==mesh.num_nodes_x-2) or (j==0) or (j==mesh.num_nodes_y-2): 16 | mass[mesh.meshToVec(i,j),:] = 0 17 | if sparse: 18 | return csr_matrix(mass,dtype=float) 19 | else: 20 | return mass 21 | 22 | def gravitation_matrix(mesh,sparse = False): 23 | if sparse: 24 | matrix = lil_matrix((mesh.num_unknowns,mesh.num_unknowns)) 25 | else: 26 | matrix = np.zeros((mesh.num_unknowns,mesh.num_unknowns)) 27 | nodes = mesh.nodes 28 | normals = mesh.normals 29 | gravity = np.array([0,1]) 30 | meshToVec = mesh.meshToVec 31 | for i in range(1,nodes.shape[0]-2): 32 | for j in range(1,nodes.shape[1]-2): 33 | flux_e = 2*normals[i,j,0,:].T@gravity 34 | flux_s = 2*normals[i,j,1,:].T@gravity 35 | flux_w = 2*normals[i,j+1,0,:].T@gravity 36 | flux_n = 2*normals[i+1,j,1,:].T@gravity 37 | matrix[meshToVec(i,j),meshToVec(i,j)] += -flux_e - flux_s + flux_n + flux_w 38 | matrix[meshToVec(i,j),meshToVec(i-1,j)] += flux_s 39 | matrix[meshToVec(i,j),meshToVec(i,j-1)] += flux_e 40 | matrix[meshToVec(i,j),meshToVec(i,j+1)] -= flux_w 41 | matrix[meshToVec(i,j),meshToVec(i+1,j)] -= flux_n 42 | if (i==0) or (i==nodes.shape[0]-2) or (j==0) or (j==nodes.shape[1]-2): 43 | matrix[meshToVec(i,j),:] = 0 44 | if sparse: 45 | return csr_matrix(matrix,dtype=float) 46 | else: 47 | return matrix -------------------------------------------------------------------------------- /elliptic_convergence.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sympy as sym 3 | import math 4 | import random 5 | from scipy.sparse import csr_matrix,lil_matrix 6 | from scipy.sparse.linalg import spsolve 7 | import matplotlib.pyplot as plt 8 | 9 | from discretization.FVML import compute_matrix as compute_matrix_l 10 | from discretization.FVMO import compute_matrix as compute_matrix_o 11 | from discretization.TPFA import compute_matrix as compute_matrix_tpfa 12 | from discretization.FVMO import compute_vector 13 | from discretization.FEM import compute_matrix as compute_matrix_FEM 14 | from discretization.FEM import compute_vector as compute_vector_FEM 15 | from discretization.mesh import Mesh 16 | 17 | from utils.flux_error import compute_flux_error 18 | from utils.differentiation import divergence, gradient 19 | """ 20 | The code used for the figures in section 5.2 21 | """ 22 | 23 | def compute_error(mesh,u,u_fabric): 24 | cx = mesh.cell_centers.shape[1] 25 | cy = mesh.cell_centers.shape[0] 26 | u_fabric_vec = u.copy() 27 | volumes = u.copy() 28 | 29 | for i in range(cy): 30 | for j in range(cx): 31 | u_fabric_vec[mesh.meshToVec(i,j)] = u_fabric(mesh.cell_centers[i,j,0],mesh.cell_centers[i,j,1]) 32 | volumes[mesh.meshToVec(i,j)] = mesh.volumes[i,j] 33 | #mesh.plot_vector(u-u_fabric_vec,'error') 34 | L2err = math.sqrt(np.square(u-u_fabric_vec).T@volumes/(np.ones(volumes.shape).T@volumes)) 35 | maxerr = np.max(np.abs(u-u_fabric_vec)) 36 | return (L2err,maxerr) 37 | 38 | def random_perturbation(h,aspect): 39 | return lambda p: np.array([random.uniform(0,h)*random.choice([-1,1]) + p[0] - 0.5*p[1],(1/aspect)*random.uniform(0,h)*random.choice([-1,1]) + p[1]]) 40 | 41 | def chevron_perturbation(n): 42 | return lambda p: np.array([p[0],p[1]*0.5 + 0.5*(p[1]-1)*p[1]*0.3*math.sin(n*p[0])]) 43 | 44 | x = sym.Symbol('x') 45 | y = sym.Symbol('y') 46 | K = np.array([[1,0],[0,1]]) 47 | u_fabric = sym.cos(y*math.pi)*sym.cosh(x*math.pi) 48 | source = -divergence(gradient(u_fabric,[x,y]),[x,y],permability_tensor=K) 49 | source = sym.lambdify([x,y],source) 50 | u_lam = sym.lambdify([x,y],u_fabric) 51 | 52 | 53 | 54 | 55 | 56 | start = 3 57 | end = 5 58 | aspect = 1 59 | result_pressure = np.zeros((end-start,9)) 60 | result_flux = np.zeros((end-start,9)) 61 | 62 | for i in range(start,end): 63 | result_pressure[i-start,0] = i 64 | result_flux[i-start,0] = i 65 | mesh = Mesh(2**i,aspect*2**i,random_perturbation(1/(4*2**i),aspect),ghostboundary=True) 66 | # mesh = Mesh(2**i,2*2**i,chevron_perturbation(2*2**i),ghostboundary=True) 67 | #mesh = Mesh(2**i,2**i,T) 68 | num_unknowns = mesh.num_unknowns 69 | 70 | 71 | #FVM-L 72 | A = lil_matrix((mesh.num_unknowns,mesh.num_unknowns)) 73 | F = np.zeros(num_unknowns) 74 | flux_matrix = {'x': lil_matrix((num_unknowns,num_unknowns)),'y':lil_matrix((num_unknowns,num_unknowns))} 75 | compute_matrix_l(mesh,A,K,k_global=None,flux_matrix = flux_matrix) 76 | A = csr_matrix(A,dtype=float) 77 | compute_vector(mesh,F,source,u_lam) 78 | u = spsolve(A,F) 79 | fx = csr_matrix(flux_matrix['x'],dtype=float) 80 | fy = csr_matrix(flux_matrix['y'],dtype=float) 81 | l2err_flux,maxerr_flux = compute_flux_error(fx.dot(u),fy.dot(u),u_fabric,mesh) 82 | l2err,maxerr = compute_error(mesh,u,u_lam) 83 | result_pressure[i-start,1] = math.log(l2err,2) 84 | result_pressure[i-start,2] = math.log(maxerr,2) 85 | result_flux[i-start,1] = math.log(l2err_flux,2) 86 | result_flux[i-start,2] = math.log(maxerr_flux,2) 87 | 88 | 89 | #FEM 90 | A = lil_matrix((mesh.num_unknowns,mesh.num_unknowns)) 91 | F = np.zeros(num_unknowns) 92 | compute_matrix_FEM(mesh,A,K,k_global=None) 93 | A = csr_matrix(A,dtype=float) 94 | f = compute_vector_FEM(mesh,F,source,u_lam) 95 | u = spsolve(A,f) 96 | u = np.reshape(u,(mesh.cell_centers.shape[0],mesh.cell_centers.shape[1])) 97 | u = np.ravel(u,order='F') 98 | 99 | l2err_flux,maxerr_flux = compute_flux_error(fx.dot(u),fy.dot(u),u_fabric,mesh) 100 | l2err,maxerr = compute_error(mesh,u,u_lam) 101 | result_pressure[i-start,7] = math.log(l2err,2) 102 | result_pressure[i-start,8] = math.log(maxerr,2) 103 | result_flux[i-start,7] = math.log(l2err_flux,2) 104 | result_flux[i-start,8] = math.log(maxerr_flux,2) 105 | 106 | #FVM-O 107 | A = lil_matrix((mesh.num_unknowns,mesh.num_unknowns)) 108 | F = np.zeros(num_unknowns) 109 | flux_matrix = {'x': lil_matrix((num_unknowns,num_unknowns)),'y':lil_matrix((num_unknowns,num_unknowns))} 110 | compute_matrix_o(mesh,A,K,k_global=None,flux_matrix = flux_matrix) 111 | A = csr_matrix(A,dtype=float) 112 | compute_vector(mesh,F,source,u_lam) 113 | u = spsolve(A,F) 114 | fx = csr_matrix(flux_matrix['x'],dtype=float) 115 | fy = csr_matrix(flux_matrix['y'],dtype=float) 116 | l2err_flux,maxerr_flux = compute_flux_error(fx.dot(u),fy.dot(u),u_fabric,mesh) 117 | l2err,maxerr = compute_error(mesh,u,u_lam) 118 | result_pressure[i-start,3] = math.log(l2err,2) 119 | result_pressure[i-start,4] = math.log(maxerr,2) 120 | result_flux[i-start,3] = math.log(l2err_flux,2) 121 | result_flux[i-start,4] = math.log(maxerr_flux,2) 122 | 123 | 124 | #TPFA 125 | A = lil_matrix((mesh.num_unknowns,mesh.num_unknowns)) 126 | F = np.zeros(num_unknowns) 127 | flux_matrix = {'x': lil_matrix((num_unknowns,num_unknowns)),'y':lil_matrix((num_unknowns,num_unknowns))} 128 | compute_matrix_tpfa(mesh,A,K,k_global=None,flux_matrix = flux_matrix) 129 | A = csr_matrix(A,dtype=float) 130 | compute_vector(mesh,F,source,u_lam) 131 | u = spsolve(A,F) 132 | fx = csr_matrix(flux_matrix['x'],dtype=float) 133 | fy = csr_matrix(flux_matrix['y'],dtype=float) 134 | l2err_flux,maxerr_flux = compute_flux_error(fx.dot(u),fy.dot(u),u_fabric,mesh) 135 | l2err,maxerr = compute_error(mesh,u,u_lam) 136 | result_pressure[i-start,5] = math.log(l2err,2) 137 | result_pressure[i-start,6] = math.log(maxerr,2) 138 | result_flux[i-start,5] = math.log(l2err_flux,2) 139 | result_flux[i-start,6] = math.log(maxerr_flux,2) 140 | 141 | 142 | 143 | 144 | print(result_pressure) 145 | print(result_flux) 146 | 147 | fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,5)) 148 | fig.suptitle('potential error') 149 | p3, = ax1.plot(result_pressure[:,0],result_pressure[:,7],'-o',color='red') 150 | p3, = ax1.plot(result_pressure[:,0],result_pressure[:,3],'-v',color= 'g') 151 | 152 | p1, = ax1.plot(result_pressure[:,0],result_pressure[:,1],'--x',color='k') 153 | p3, = ax1.plot(result_pressure[:,0],result_pressure[:,5],'-*',color='y') 154 | ax1.set_title('$L_2$ error') 155 | ax1.grid() 156 | ax1.set(xlabel='$log_2 n$',ylabel='$log_2 e$') 157 | 158 | 159 | p4,=ax2.plot(result_pressure[:,0],result_pressure[:,8],'-o',color='red') 160 | p4.set_label('FEM') 161 | p4,=ax2.plot(result_pressure[:,0],result_pressure[:,4],'-v',color='g') 162 | p4.set_label('O-method') 163 | p2, = ax2.plot(result_pressure[:,0],result_pressure[:,2],'--x',color='k') 164 | p2.set_label('L-method') 165 | p4,=ax2.plot(result_pressure[:,0],result_pressure[:,6],'-*',color='y') 166 | p4.set_label('TPFA-method') 167 | ax2.grid() 168 | ax2.set_title('$max$ error') 169 | 170 | ax2.set(xlabel='$log_2 n$',ylabel='$log_2 e$') 171 | plt.legend(loc='lower center',bbox_to_anchor=(0.0, -0.3),ncol=4) 172 | fig.subplots_adjust(bottom=0.20) 173 | #plt.savefig('figs/pressure_chevron_grid.pdf') 174 | 175 | plt.show() 176 | 177 | fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,5)) 178 | fig.suptitle('normal flow error') 179 | p3, = ax1.plot(result_flux[:,0],result_flux[:,7],'-o',color='r') 180 | p3, = ax1.plot(result_flux[:,0],result_flux[:,3],'-v',color='g') 181 | 182 | p1, = ax1.plot(result_flux[:,0],result_flux[:,1],'--x',color='k') 183 | p3, = ax1.plot(result_flux[:,0],result_flux[:,5],'-*',color='y') 184 | 185 | ax1.set_title('$L_2$ error') 186 | ax1.grid() 187 | ax1.set(xlabel='$log_2 n$',ylabel='$log_2 e$') 188 | 189 | 190 | p4,=ax2.plot(result_flux[:,0],result_flux[:,8],'-o',color='r') 191 | p4.set_label('FEM') 192 | p4,=ax2.plot(result_flux[:,0],result_flux[:,4],'-v',color='g') 193 | p4.set_label('O-method') 194 | p2, = ax2.plot(result_flux[:,0],result_flux[:,2],'--x',color='k') 195 | p2.set_label('L-method') 196 | 197 | p4,=ax2.plot(result_flux[:,0],result_flux[:,6],'-*',color='y') 198 | p4.set_label('TPFA-method') 199 | 200 | ax2.grid() 201 | ax2.set_title('$max$ error') 202 | 203 | ax2.set(xlabel='$log_2 n$',ylabel='$log_2 e$') 204 | plt.legend(loc='lower center',bbox_to_anchor=(0.0, -0.3),ncol=4) 205 | fig.subplots_adjust(bottom=0.20) 206 | #plt.savefig('figs/flow_chevron_grid.pdf') 207 | 208 | plt.show() 209 | -------------------------------------------------------------------------------- /poisson.py: -------------------------------------------------------------------------------- 1 | from discretization.mesh import Mesh 2 | import numpy as np 3 | from discretization.FVML import compute_matrix,compute_vector 4 | import math 5 | 6 | nx = ny = 6 7 | perturbation = lambda p:np.array([p[0],0.5*p[0]+p[1]]) 8 | mesh = Mesh(nx,ny,perturbation,ghostboundary=True) 9 | 10 | source = lambda x , y : math.sin(y)*math.cos(x) 11 | boundary_condition = lambda x , y :0 12 | tensor = np.eye(2) 13 | permeability = np.ones(( mesh.num_unknowns,mesh.num_unknowns)) 14 | 15 | A = np.zeros((mesh.num_unknowns,mesh.num_unknowns))# stiffness matrix 16 | f = np.zeros(mesh . num_unknowns)# load vector 17 | 18 | compute_matrix(mesh,A,tensor,permeability) 19 | compute_vector(mesh,f,source,boundary_condition) 20 | 21 | u = np.linalg.solve(A,f) 22 | mesh.plot_vector(u) -------------------------------------------------------------------------------- /richards_constant_hydraulic_conductivity.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sympy as sym 3 | import math 4 | 5 | from discretization.FVML import compute_matrix , compute_vector 6 | from utils.differentiation import gradient, divergence 7 | from discretization.mesh import Mesh 8 | from discretization.operators import mass_matrix 9 | """ 10 | The code used for table 5.1-5.3 11 | """ 12 | def solve_richards(mesh: Mesh,timestep=None): 13 | #construct solution 14 | K = np.array([[1,0],[0,1]])#permeability tensor 15 | x = sym.Symbol('x') 16 | y = sym.Symbol('y') 17 | t = sym.Symbol('t') 18 | p = sym.Symbol('p') 19 | theta = 1/(1-p) #parametrized saturation 20 | u_exact = -t*x*(1-x)*y*(1-y)-1#constructed hydraulic head 21 | 22 | f = sym.diff(theta.subs(p,u_exact),t)-divergence(gradient(u_exact ,[x,y]),[x,y])#construct the force function given the exact solution 23 | 24 | #convert from sympy to regular python functions 25 | f = sym.lambdify([x,y,t],f) 26 | u_exact = sym.lambdify([x,y,t],u_exact) 27 | theta = sym.lambdify([p],theta) 28 | 29 | #time discretization of 0-1 30 | time_partition = np.linspace(0,1,math.ceil(1/timestep)) 31 | tau = time_partition[1]-time_partition[0] 32 | 33 | #L-scheme parameters 34 | L = 1.2 35 | TOL = 0.0000000005 36 | 37 | u = mesh.interpolate(lambda x,y:u_exact(x,y,0))#initial condition 38 | 39 | #allocate storage 40 | u_l = u.copy() #L-scheme iterate 41 | u_t = u.copy() #timestep iterate 42 | F = u.copy() #source vector 43 | F.fill(0) 44 | A = np.zeros((mesh.num_unknowns,mesh.num_unknowns)) #stiffness matrix 45 | B = mass_matrix(mesh) 46 | compute_matrix(mesh, A, K)#compute stiffnes matrix 47 | lhs = L*B+tau*A 48 | transform = np.linalg.inv(lhs) 49 | #time iteration 50 | for t in time_partition[1:]: 51 | F.fill(0)#empty load vector 52 | compute_vector(mesh,F,lambda x,y: f(x,y,t),lambda x,y:u_exact(x,y,t))#compute load vector 53 | #L-scheme iteration 54 | while True: 55 | rhs = L*B@u_l + B@theta(u_t) - B@theta(u_l) + tau*F 56 | u = transform@rhs 57 | if np.linalg.norm(u-u_l)<=TOL+TOL*np.linalg.norm(u_l): 58 | break 59 | else: 60 | u_l = u 61 | u_t = u 62 | u_l = u 63 | 64 | return (mesh.max_h(),timestep,mesh.compute_error(u,lambda x,y : u_exact(x,y,1))[0]) 65 | 66 | if __name__=="__main__": 67 | number_of_tests = 4 68 | 69 | result = [] 70 | for i in range(1,number_of_tests+1): 71 | num_nodes = 2*2**i 72 | mesh = Mesh(1+num_nodes,1+num_nodes,lambda p: np.array([p[0]-0.5*p[1],p[1]]),ghostboundary=True) 73 | result.append(solve_richards(mesh,mesh.max_h())) 74 | output='' 75 | for i in range(number_of_tests): 76 | row = result[i] 77 | if i==0: 78 | output = f'{i+1}&${row[0]:.5f}$&${row[1]:.5f}$&${row[2]:.6f}$&-\\\ \n' 79 | else: 80 | old_row = result[i-1] 81 | improvement = old_row[2]/row[2] 82 | output += f'{i+1}&${row[0]:.5f}$&${row[1]:.5f}$&${row[2]:.6f}$&${improvement:.5f}$\\\ \n' 83 | print(output) -------------------------------------------------------------------------------- /richards_non_linear.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sympy as sym 3 | import math 4 | import matplotlib.pyplot as plt 5 | 6 | from discretization.FVML import compute_matrix , compute_vector 7 | from utils.differentiation import gradient, divergence 8 | from discretization.mesh import Mesh 9 | from discretization.operators import mass_matrix 10 | """ 11 | The code used for table 5.4 and 5.5 12 | """ 13 | def solve_richards(mesh: Mesh,timestep=None): 14 | #Van Genuchten parameteres 15 | a_g = 0.1844 16 | n_g = 3 17 | k_abs = 0.03 18 | my = 1 19 | exp_1 = n_g/(n_g-1) 20 | exp_2 = (n_g-1)/n_g 21 | 22 | #construct solution 23 | K = np.array([[1,0],[0,1]])#permeability tensor 24 | x = sym.Symbol('x') 25 | y = sym.Symbol('y') 26 | t = sym.Symbol('t') 27 | p = sym.Symbol('p') 28 | s = sym.Symbol('s') 29 | theta = sym.Piecewise( 30 | ((1+(-a_g*p)**n_g)**(-exp_2),p<=0), 31 | (1,p>0) 32 | ) #parametrized saturation 33 | kappa = ((k_abs/my)*s**(-1/2))*(1-(1-s**exp_1)**exp_2)**2#parametrized hydraulic conductivity as a function of saturation 34 | u_exact = -3*t*x*(1-x)*y*(1-y)-1#constructed hydraulic head 35 | f = sym.diff(theta.subs(p,u_exact),t)-divergence(gradient(u_exact ,[x,y]),[x,y],kappa.subs(s,theta.subs(p,u_exact)))#construct the force function given the exact solutio 36 | 37 | #convert from sympy to regular python functions 38 | f = sym.lambdify([x,y,t],f) 39 | u_exact = sym.lambdify([x,y,t],u_exact) 40 | kappa = sym.lambdify([p],kappa.subs(s,theta)) 41 | theta = sym.lambdify([p],theta) 42 | 43 | #time discretization of 0-1 44 | time_partition = np.linspace(0,1,math.ceil(1/timestep)) 45 | tau = time_partition[1]-time_partition[0] 46 | 47 | #L-scheme parameters 48 | L = 0.3 49 | TOL = 0.000000005 50 | 51 | u = mesh.interpolate(lambda x,y:u_exact(x,y,0))#initial condition 52 | 53 | #allocate storage 54 | u_l = u.copy() #L-scheme iterate 55 | u_t = u.copy() #timestep iterate 56 | F = u.copy() #source vector 57 | F.fill(0) 58 | A = np.zeros((mesh.num_unknowns,mesh.num_unknowns)) #stiffness matrix 59 | B = mass_matrix(mesh) 60 | #time iteration 61 | for t in time_partition[1:]: 62 | count=0 63 | F.fill(0)#empty load vector 64 | compute_vector(mesh,F,lambda x,y: f(x,y,t),lambda x,y:u_exact(x,y,t))#compute load vector 65 | #L-scheme iteration 66 | while True: 67 | count = count + 1 68 | conductivity = kappa(np.reshape(u_l, (mesh.cell_centers.shape[0],mesh.cell_centers.shape[1]),order='F')) 69 | A.fill(0)#empty the stiffness matrix 70 | compute_matrix(mesh, A, K,conductivity)#compute stiffnes matrix 71 | lhs = L*B+tau*A 72 | rhs = L*B@u_l + B@theta(u_t) - B@theta(u_l) + tau*F 73 | u = np.linalg.solve(lhs,rhs) 74 | if np.linalg.norm(u-u_l)<=TOL+TOL*np.linalg.norm(u_l): 75 | break 76 | else: 77 | u_l = u 78 | err = mesh.compute_error(u,lambda x,y : u_exact(x,y,t)) 79 | print('error at time ',t," : ",err[0]) 80 | print('L-scheme iterations: ',count) 81 | u_t = u 82 | u_l = u 83 | 84 | return (mesh.max_h(),timestep,mesh.compute_error(u,lambda x,y : u_exact(x,y,1))[0]) 85 | 86 | if __name__=="__main__": 87 | number_of_tests = 3 88 | 89 | result = [] 90 | for i in range(1,number_of_tests+1): 91 | num_nodes = 2*2**i 92 | mesh = Mesh(1+num_nodes,1+num_nodes,lambda p: np.array([p[0]-0.5*p[1],p[1]]),ghostboundary=True) 93 | result.append(solve_richards(mesh,mesh.max_h())) 94 | output='' 95 | for i in range(number_of_tests): 96 | row = result[i] 97 | if i==0: 98 | output = f'{i+1}&${row[0]:.5f}$&${row[1]:.5f}$&${row[2]:.6f}$&-\\\ \n' 99 | else: 100 | old_row = result[i-1] 101 | improvement = old_row[2]/row[2] 102 | output += f'{i+1}&${row[0]:.5f}$&${row[1]:.5f}$&${row[2]:.6f}$&${improvement:.5f}$\\\ \n' 103 | print(output) -------------------------------------------------------------------------------- /utils/differentiation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sympy as sym 3 | import unittest 4 | 5 | def gradient(f, variables: list): 6 | vec = [] 7 | for variable in variables: 8 | vec.append(sym.diff(f,variable)) 9 | return vec 10 | def divergence(gradient: list,variables: list, permability = None,permability_tensor = None): 11 | if permability_tensor is not None: 12 | K_gradient = [] 13 | for row in permability_tensor: 14 | r = 0 15 | for e,p in zip(gradient,row): 16 | r = r+e*p 17 | K_gradient.append(r) 18 | return divergence(K_gradient,variables,permability) 19 | if permability is None: 20 | return sum(map(lambda t: sym.diff(t[0],t[1]),zip(gradient,variables))) 21 | else: 22 | return sum(map(lambda t: sym.diff(permability*t[0],t[1]),zip(gradient,variables))) 23 | class TestDiffMethods(unittest.TestCase): 24 | def test_gradient(self): 25 | x = sym.Symbol('x') 26 | y = sym.Symbol('y') 27 | res = [2*x,2*y] 28 | var = list([x,y]) 29 | f = x**2 + y**2 30 | self.assertEqual(gradient(f,var),res) 31 | def test_divergence(self): 32 | x = sym.Symbol('x') 33 | y = sym.Symbol('y') 34 | var = [x,y] 35 | res = 2*x+2*y 36 | self.assertEqual(divergence([x**2,y**2],var),res) 37 | def test_divergence_permability_tensor(self): 38 | x = sym.Symbol('x') 39 | y = sym.Symbol('y') 40 | K = np.array([[0,1],[1,0]]) 41 | var = [x,y] 42 | res = 0 43 | self.assertEqual(divergence([x**2,y**2],var,permability_tensor=K),res) 44 | def test_divergence_permability(self): 45 | x = sym.Symbol('x') 46 | y = sym.Symbol('y') 47 | var = [x,y] 48 | res = 3*x**2 + 2*x*y 49 | self.assertEqual(divergence([x**2,y**2],var,permability = x),res) 50 | def test_combination(self): 51 | z = sym.Symbol('z') 52 | y = sym.Symbol('y') 53 | res = 6*z 54 | f = z**2+y**2 55 | self.assertEqual(divergence(gradient(f,[z,y]),[z,y],z),res) 56 | if __name__ =='__main__': 57 | unittest.main() 58 | 59 | -------------------------------------------------------------------------------- /utils/flux_error.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from discretization.mesh import Mesh 3 | import sympy as sym 4 | import math 5 | x = sym.Symbol('x') 6 | y = sym.Symbol('y') 7 | 8 | 9 | def compute_flux_error(fx,fy,u_fabric,mesh: Mesh): 10 | u_x = sym.diff(u_fabric,x) 11 | u_x = sym.lambdify([x,y],u_x) 12 | u_y = sym.diff(u_fabric,y) 13 | u_y = sym.lambdify([x,y],u_y) 14 | 15 | normals = mesh.normals 16 | midpoints = mesh.midpoints 17 | meshToVec = mesh.meshToVec 18 | vecToMesh = mesh.vecToMesh 19 | volumes = mesh.volumes 20 | 21 | fx_exact = np.zeros((midpoints.shape[0]*(midpoints.shape[1]-1))) 22 | vec = fx 23 | 24 | for i in range(normals.shape[0]): 25 | for j in range(0,normals.shape[1]-1): 26 | if i==0 or i == normals.shape[0]-1: 27 | fx_exact[meshToVec(i,j)] = vec[meshToVec(i,j)] 28 | else: 29 | grad_u = np.array([[u_x(midpoints[i,j+1,0,0],midpoints[i,j+1,0,1])],[u_y(midpoints[i,j+1,0,0],midpoints[i,j+1,0,1])]]) 30 | fx_exact[meshToVec(i,j)] = -normals[i,j+1,0,:].T@grad_u/np.linalg.norm(normals[i,j+1,0,:]) 31 | fx[meshToVec(i,j)] = fx[meshToVec(i,j)]/np.linalg.norm(2*normals[i,j+1,0,:]) 32 | error_x = vec[:midpoints.shape[0]*(midpoints.shape[1]-1)] - fx_exact 33 | 34 | fy_exact = np.zeros((midpoints.shape[1]*(midpoints.shape[0]-1))) 35 | vec = fy 36 | for i in range(normals.shape[0]-1): 37 | for j in range(normals.shape[1]-1): 38 | if j==0 or j == normals.shape[1]-1: 39 | fy_exact[meshToVec(i,j)] = vec[meshToVec(i,j)] 40 | else: 41 | grad_u = np.array([[u_x(midpoints[i+1,j,1,0],midpoints[i+1,j+1,1,1])],[u_y(midpoints[i+1,j,1,0],midpoints[i+1,j+1,1,1])]]) 42 | fy_exact[meshToVec(i,j)] = -normals[i+1,j,1,:].T@grad_u/np.linalg.norm(normals[i+1,j,1,:]) 43 | fy[meshToVec(i,j)] = fy[meshToVec(i,j)]/np.linalg.norm(2*normals[i+1,j,1,:]) 44 | error_y = vec[:midpoints.shape[1]*(midpoints.shape[0]-1)] - fy_exact 45 | 46 | 47 | max_error_x = np.max(np.abs(error_x)) 48 | error_x = np.square(error_x) 49 | volumes_x = np.zeros(error_x.shape) 50 | for i in range(error_x.shape[0]): 51 | a,b = vecToMesh(i) 52 | if a==0 or a == normals.shape[0]-1: 53 | volumes_x[i] = 0 54 | else: 55 | volumes_x[i] = (volumes[a,b]+volumes[a,b-1]) 56 | 57 | max_error_y = np.max(np.abs(error_y)) 58 | error_y = np.square(error_y) 59 | volumes_y = np.zeros(error_y.shape) 60 | for i in range(error_y.shape[0]): 61 | a,b = vecToMesh(i) 62 | if b == normals.shape[1]-1 or a==normals.shape[0]-1: 63 | volumes_y[i] = 0 64 | else: 65 | volumes_y[i] = (volumes[a,b]+volumes[a-1,b]) 66 | l2_error = math.sqrt((volumes_x.T@error_x+volumes_y.T@error_y)/(np.ones(volumes_x.shape).T@volumes_x+np.ones(volumes_y.shape).T@volumes_y)) 67 | 68 | return l2_error,max(max_error_x,max_error_y) --------------------------------------------------------------------------------